Merge branch 'master' into fix-randall-munroe
This commit is contained in:
35 files changed
+26324
-23133
No files matched your search
@@ -1,104 +1,123 @@
|
||||
|
||||
|
||||
<div align="right" style="text-align:right"><i>Peter Norvig<br><a href="https://github.com/norvig/pytudes/blob/master/LICENSE">MIT License</a><br>2015-2020</i></div>
|
||||
|
||||
# pytudes
|
||||
|
||||
"An *étude* (a French word meaning *study*) is an instrumental musical composition, usually short, of considerable difficulty, and designed to provide practice material for perfecting a particular musical skill." — [Wikipedia](https://en.wikipedia.org/wiki/%C3%89tude)
|
||||
|
||||
This project contains **pytudes**—Python programs, usually short, for perfecting programming skills.
|
||||
This project contains **pytudes**—Python programs, usually short, for perfecting particular programming skills.
|
||||
Some programs are in Jupyter (`.ipynb`) notebooks, some in `.py` files. For each notebook you can:
|
||||
- Click on [co](https://colab.research.google.com) to **run** the file on Colab
|
||||
- Click on [dn](https://deepnote.com) to **run** the notebook on DeepNote
|
||||
- Click on [my](https://mybinder.org) to **run** the notebook on MyBinder
|
||||
- Click on [nb](https://nbviewer.jupyter.org/) to **view** the notebook on NBViewer
|
||||
- Click on the title to **view** the notebook on github.
|
||||
- Hover over the title to **view** a description.
|
||||
|
||||
Some are in Jupyter (IPython) notebooks, some in `.py` files. You can view the files here on github, or click the **NB** link to view them on [nbviewer](http://nbviewer.jupyter.org/) (which sometimes works better). If you want to *run* the notebooks, not just view them, you can clone the project, or run all the notebooks online by clicking this button: [](https://mybinder.org/v2/gh/norvig/pytudes/master), or click the **DN** link to run each individual notebook on [deepnote](https://beta.deepnote.org/).
|
||||
|
||||
# Index of Jupyter (IPython) Notebooks
|
||||
|
||||
|
||||
|Run|Year|Programming Examples|
|
||||
|---|----|---|
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Advent-2018.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FAdvent-2018.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FAdvent-2018.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Advent-2018.ipynb) | 2018 | <b><a href="ipynb/Advent-2018.ipynb" title="Puzzle site with a coding puzzle each day for Advent 2018 ">Advent of Code 2018</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Advent%202017.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FAdvent%202017.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FAdvent%202017.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Advent%202017.ipynb) | 2017 | <b><a href="ipynb/Advent%202017.ipynb" title="Puzzle site with a coding puzzle each day for Advent 2017">Advent of Code 2017</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Advent%20of%20Code.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FAdvent%20of%20Code.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FAdvent%20of%20Code.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Advent%20of%20Code.ipynb) | 2016 | <b><a href="ipynb/Advent%20of%20Code.ipynb" title="Puzzle site with a coding puzzle each day for Advent 2016*">Advent of Code 2016</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Beal.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FBeal.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FBeal.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Beal.ipynb) | 2018 | <b><a href="ipynb/Beal.ipynb" title="A search for counterexamples to Beal's Conjecture">Beal's Conjecture Revisited</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Bike%20Speed%20versus%20Grade.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FBike%20Speed%20versus%20Grade.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FBike%20Speed%20versus%20Grade.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Bike%20Speed%20versus%20Grade.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/Bike%20Speed%20versus%20Grade.ipynb" title="How fast can I bike as the route gets steeper?">Bike Speed Versus Grade</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Cant-Stop.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCant-Stop.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FCant-Stop.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Cant-Stop.ipynb) | 2018 | <b><a href="ipynb/Cant-Stop.ipynb" title="Optimal play in a dice board game">Can't Stop</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Sierpinski.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FSierpinski.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FSierpinski.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Sierpinski.ipynb) | 2019 | <b><a href="ipynb/Sierpinski.ipynb" title="A surprising appearance of the Sierpinski triangle in a random walk between vertexes">Chaos with Triangles</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Life.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FLife.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FLife.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Life.ipynb) | 2017 | <b><a href="ipynb/Life.ipynb" title="The cellular automata zero-player game">Conway's Game of Life</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Dice%20Baseball.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FDice%20Baseball.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FDice%20Baseball.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Dice%20Baseball.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/Dice%20Baseball.ipynb" title="Simulating baseball games">Dice Baseball</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Maze.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FMaze.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FMaze.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Maze.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/Maze.ipynb" title="Make a maze by generating a random tree superimposed on a grid and solve it.">Generating and Solving Mazes</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/PhotoFocalLengths.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FPhotoFocalLengths.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FPhotoFocalLengths.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/PhotoFocalLengths.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/PhotoFocalLengths.ipynb" title="Generate charts of what focal lengths were used on a photo trip.">Photo Focal Lengths</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Pickleball.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FPickleball.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FPickleball.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Pickleball.ipynb) | 2018 | <b><a href="ipynb/Pickleball.ipynb" title="Scheduling a doubles tournament fairly and efficiently">Pickleball Tournament</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Project%20Euler%20Utils.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FProject%20Euler%20Utils.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FProject%20Euler%20Utils.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Project%20Euler%20Utils.ipynb) | 2017 | <b><a href="ipynb/Project%20Euler%20Utils.ipynb" title="My utility functions for the Project Euler problems, including `Primes` and `Factors`">Project Euler Utilities</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Orderable%20Cards.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FOrderable%20Cards.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FOrderable%20Cards.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Orderable%20Cards.ipynb) | 2018 | <b><a href="ipynb/Orderable%20Cards.ipynb" title="Can you get your hand of cards into a nice order with just one move?">Properly Ordered Card Hands</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Electoral%20Votes.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FElectoral%20Votes.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FElectoral%20Votes.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Electoral%20Votes.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/Electoral%20Votes.ipynb" title="How many electoral votes would Trump get if he wins the state where he has positive net approval?">Tracking Trump: Electoral Votes</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/WWW.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FWWW.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FWWW.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/WWW.ipynb) | 2019 | <b><a href="ipynb/WWW.ipynb" title="Computing the probability of winning the NBA title, for my home town Warriors, or any other team">WWW: Who Will Win (NBA Title)?</a></b> |
|
||||
|
||||
|Programming Examples|
|
||||
|---|
|
||||
|[Advent of Code 2018](ipynb/Advent-2018.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Advent-2018.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FAdvent-2018.ipynb)**<br>*Puzzle site with a coding puzzle each day for Advent 2018 .*|
|
||||
|[Advent of Code 2017](ipynb/Advent%202017.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Advent%202017.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FAdvent%25202017.ipynb)**<br>*Puzzle site with a coding puzzle each day for Advent 2017.*|
|
||||
|[Advent of Code 2016](ipynb/Advent%20of%20Code.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Advent%20of%20Code.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FAdvent%2520of%2520Code.ipynb)**<br>*Puzzle site with a coding puzzle each day for Advent 2016*.|
|
||||
|[Project Euler Utilities](ipynb/Project%20Euler%20Utils.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Project%20Euler%20Utils.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FProject%2520Euler%2520Utils.ipynb)**<br>*My utility functions for the Project Euler problems, including `Primes` and `Factors`.*|
|
||||
|[Translating English Sentences into Propositional Logic Statements](ipynb/PropositionalLogic.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/PropositionalLogic.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FPropositionalLogic.ipynb)**<br>*Automatically converting informal English sentences into formal Propositional Logic.*|
|
||||
|[Beal's Conjecture Revisited](ipynb/Beal.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Beal.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FBeal.ipynb)**<br>*A search for counterexamples to Beal's Conjecture*|
|
||||
|[WWW: Who Will Win (NBA Title)?](ipynb/WWW.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/WWW.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FWWW.ipynb)**<br>*Computing the probability of winning the NBA title, for my home town Warriors, or any other team.*|
|
||||
|[Pickleball Tournament](ipynb/Pickleball.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Pickleball.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FPickleball.ipynb)**<br>*Scheduling a doubles tournament fairly and efficiently.*|
|
||||
|[Dice Baseball](ipynb/Dice%20Baseball.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Dice%2520Baseball.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FDice%2520Baseball.ipynb)**<br>*Simulating baseball games.*|
|
||||
|[Conway's Game of Life](ipynb/Life.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Life.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FLife.ipynb)**<br>*The cellular automata zero-player game.*|
|
||||
|[A Chaos Game with Triangles](ipynb/Sierpinski.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Sierpinski.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FSierpinski.ipynb)**<br>*A surprising appearance of the Sierpinski triangle in a random walk between vertexes.*|
|
||||
|[Generating Mazes](ipynb/Maze.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Maze.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FMaze.ipynb)**<br>*Make a maze by generating a random tree superimposed on a grid.*|
|
||||
|[Weighing Twelve Balls](ipynb/TwelveBalls.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/TwelveBalls.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FTwelveBalls.ipynb)**<br>*A puzzle where you are given some billiard balls and a balance scale, and asked to find the one ball that is heavier or lighter, in a limited number of weighings.*|
|
||||
|[Can't Stop](ipynb/Cant-Stop.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Cant-Stop.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCant-Stop.ipynb)**<br>*Optimal play in a dice board game.*|
|
||||
|[Bike Speed Versus Grade](ipynb/Bike%20Speed%20versus%20Grade.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Bike%20Speed%20versus%20Grade.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FBike%2520Speed%2520versus%2520Grade.ipynb)**<br>*How fast can I bike as the route gets steeper?*|
|
||||
|[Properly Ordered Card Hands](ipynb/Orderable%20Cards.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Orderable%20Cards.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FOrderable%2520Cards.ipynb)**<br>*Can you get your hand of cards into a nice order with just one move?*|
|
||||
|[Tracking Trump: Electoral Votes](ipynb/Electoral%20Votes.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Electoral%20Votes.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FElectoral%20Votes.ipynb)**<br>*How many electoral votes would Trump get if he wins the state where he has positive net approval?*|
|
||||
|Run|Year|Logic and Number Puzzles|
|
||||
|---|----|---|
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Cryptarithmetic.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCryptarithmetic.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FCryptarithmetic.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Cryptarithmetic.ipynb) | 2014 | <b><a href="ipynb/Cryptarithmetic.ipynb" title="Substitute digits for letters and make NUM + BER = PLAY">Cryptarithmetic</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Countdown.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCountdown.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FCountdown.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Countdown.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/Countdown.ipynb" title="Solving the equation 10 _ 9 _ 8 _ 7 _ 6 _ 5 _ 4 _ 3 _ 2 _ 1 = 2016. From an Alex Bellos puzzle">Four 4s, Five 5s, Equilength Numbers, and Countdown to 2016</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/flipping.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2Fflipping.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2Fflipping.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/flipping.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/flipping.ipynb" title="Can you go through a deck of cards, guessing higher or lower correctly for each card?">Flipping Cards: A Guessing Game</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/NightKing.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FNightKing.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FNightKing.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/NightKing.ipynb) | 2019 | <b><a href="ipynb/NightKing.ipynb" title="Investigasting a battle between the army of the dead and the army of the living">How Many Soldiers to Beat the Night King?</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Fred%20Buns.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FFred%20Buns.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FFred%20Buns.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Fred%20Buns.ipynb) | 2015 | <b><a href="ipynb/Fred%20Buns.ipynb" title="A tale of a bicycle combination lock that uses letters instead of digits. Inspired by Bike Snob NYC">Let's Code About Bike Locks</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Socks.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FSocks.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FSocks.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Socks.ipynb) | 2019 | <b><a href="ipynb/Socks.ipynb" title="What is the probability that you will be able to pair up socks as you randomly pull them out of the dryer?">Pairing Socks</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Mean%20Misanthrope%20Density.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FMean%20Misanthrope%20Density.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FMean%20Misanthrope%20Density.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Mean%20Misanthrope%20Density.ipynb) | 2017 | <b><a href="ipynb/Mean%20Misanthrope%20Density.ipynb" title="How crowded will this neighborhood be, if nobody wants to live next door to anyone else?">The Puzzle of the Misanthropic Neighbors</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Riddler%20Battle%20Royale.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FRiddler%20Battle%20Royale.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FRiddler%20Battle%20Royale.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Riddler%20Battle%20Royale.ipynb) | 2017 | <b><a href="ipynb/Riddler%20Battle%20Royale.ipynb" title="A puzzle involving allocating your troops and going up against an opponent">Riddler: Battle Royale</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/RiddlerLottery.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FRiddlerLottery.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FRiddlerLottery.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/RiddlerLottery.ipynb) | 2019 | <b><a href="ipynb/RiddlerLottery.ipynb" title="Can you find what lottery number tickets these five friends picked?">Riddler Lottery</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/TourDe538.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FTourDe538.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FTourDe538.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/TourDe538.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/TourDe538.ipynb" title="Solve a puzzle involving the best pace for a bicycle race.">Riddler: Tour de 538</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Sicherman%20Dice.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FSicherman%20Dice.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FSicherman%20Dice.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Sicherman%20Dice.ipynb) | 2018 | <b><a href="ipynb/Sicherman%20Dice.ipynb" title="Find a pair of dice that is like a regular pair of dice, only different">Sicherman Dice</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Golomb-Puzzle.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FGolomb-Puzzle.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FGolomb-Puzzle.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Golomb-Puzzle.ipynb) | 2014 | <b><a href="ipynb/Golomb-Puzzle.ipynb" title="A Puzzle involving placing rectangles of different sizes inside a square">Sol Golomb's Rectangle Puzzle</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Cheryl.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCheryl.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FCheryl.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Cheryl.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/Cheryl.ipynb" title="Solving the *Cheryl's Birthday* logic puzzle">When is Cheryl's Birthday? (new: Mad Cheryl)</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Cheryl-and-Eve.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCheryl-and-Eve.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FCheryl-and-Eve.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Cheryl-and-Eve.ipynb) | 2015 | <b><a href="ipynb/Cheryl-and-Eve.ipynb" title="Inventing new puzzles in the Style of Cheryl's Birthday">When Cheryl Met Eve: A Birthday Story</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/xkcd1313.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2Fxkcd1313.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2Fxkcd1313.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/xkcd1313.ipynb) | 2015 | <b><a href="ipynb/xkcd1313.ipynb" title="Find the smallest regular expression; inspired by Randall Munroe">xkcd 1313: Regex Golf</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/xkcd1313-part2.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2Fxkcd1313-part2.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2Fxkcd1313-part2.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/xkcd1313-part2.ipynb) | 2015 | <b><a href="ipynb/xkcd1313-part2.ipynb" title="Regex Golf: better, faster, funner. With Stefan Pochmann.">xkcd 1313: Regex Golf (Part 2: Infinite Problems)</a></b> |
|
||||
|
||||
|Logic and Number Puzzles|
|
||||
|---|
|
||||
|[When is Cheryl's Birthday?](ipynb/Cheryl.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Cheryl.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCheryl.ipynb)**<br>*Solving the "Cheryl's Birthday" logic puzzle.*|
|
||||
|[When Cheryl Met Eve: A Birthday Story](ipynb/Cheryl-and-Eve.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Cheryl-and-Eve.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCheryl-and-Eve.ipynb)**<br>*Inventing new puzzles in the Style of Cheryl's Birthday.*|
|
||||
|[How Many Soldiers Do You Need to Beat the Night King?](ipynb/NightKing.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/NightKing.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FNightKing.ipynb)**<br>*Investigasting a battle between the army of the dead and the army of the living.*|
|
||||
|[The Devil and the Coin Flip Game](ipynb/Coin%20Flip.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Coin%20Flip.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCoin%2520Flip.ipynb)**<br>*How to beat the Devil at his own game.*|
|
||||
|[The Puzzle of the Misanthropic Neighbors](ipynb/Mean%20Misanthrope%20Density.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Mean%20Misanthrope%20Density.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FMean%2520Misanthrope%2520Density.ipynb)**<br>*How crowded will this neighborhood be, if nobody wants to live next door to anyone else?*|
|
||||
|[Four 4s, Five 5s, and Countdown to 2016](ipynb/Countdown.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Countdown.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCountdown.ipynb)**<br>*Solving the equation 10 _ 9 _ 8 _ 7 _ 6 _ 5 _ 4 _ 3 _ 2 _ 1 = 2016. From an Alex Bellos puzzle.*|
|
||||
|[Sicherman Dice](ipynb/Sicherman%20Dice.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Sicherman%20Dice.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FSicherman%2520Dice.ipynb)**<br>*Find a pair of dice that is like a regular pair of dice, only different.*|
|
||||
|[Sol Golomb's Rectangle Puzzle](ipynb/Golomb-Puzzle.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Golomb-Puzzle.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FGolomb-Puzzle.ipynb)**<br>*A Puzzle involving placing rectangles of different sizes inside a square.*|
|
||||
|[Cryptarithmetic](ipynb/Cryptarithmetic.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Cryptarithmetic.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCryptarithmetic.ipynb)**<br>*Substitute digits for letters and make NUM + BER = PLAY.*|
|
||||
|[The Riddler: Battle Royale](ipynb/Riddler%20Battle%20Royale.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Riddler%20Battle%20Royale.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FRiddler%2520Battle%2520Royale.ipynb)**<br>*A puzzle involving allocating your troops and going up against an opponent.*|
|
||||
|Run|Year|Word Puzzles|
|
||||
|---|----|---|
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Boggle.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FBoggle.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FBoggle.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Boggle.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/Boggle.ipynb" title="Find all the words on a Boggle board; then find a board with a lot of words">Boggle / Inverse Boggle</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Scrabble.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FScrabble.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FScrabble.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Scrabble.ipynb) | 2017 | <b><a href="ipynb/Scrabble.ipynb" title="Refactoring the Scrabble / Word with Friends game from Udacity 212">Crossword Game : Refactoring a Scrabble Program</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/ElementSpelling.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FElementSpelling.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FElementSpelling.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/ElementSpelling.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/ElementSpelling.ipynb" title="Spelling words using the chemical element symbols, like CoIn">Chemical Element Spelling</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Gesture%20Typing.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FGesture%20Typing.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FGesture%20Typing.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Gesture%20Typing.ipynb) | 2017 | <b><a href="ipynb/Gesture%20Typing.ipynb" title="What word has the longest path on a gesture-typing smartphone keyboard?">Gesture Typing</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Ghost.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FGhost.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FGhost.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Ghost.ipynb) | 2017 | <b><a href="ipynb/Ghost.ipynb" title="The word game Ghost (add letters, try to avoid making a word)">Ghost: A Word game</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/How%20to%20Do%20Things%20with%20Words.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FHow%20to%20Do%20Things%20with%20Words.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FHow%20to%20Do%20Things%20with%20Words.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/How%20to%20Do%20Things%20with%20Words.ipynb) | 2018 | <b><a href="ipynb/How%20to%20Do%20Things%20with%20Words.ipynb" title="Spelling Correction, Secret Codes, Word Segmentation, and more">How to Do Things with Words: NLP in Python</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/SpellingBee.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FSpellingBee.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FSpellingBee.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/SpellingBee.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/SpellingBee.ipynb" title="Find the highest-scoring board for the NY Times Spelling Bee puzzle">Spelling Bee</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/PropositionalLogic.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FPropositionalLogic.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FPropositionalLogic.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/PropositionalLogic.ipynb) | 2017 | <b><a href="ipynb/PropositionalLogic.ipynb" title="Automatically convert informal English sentences into formal Propositional Logic">Translating English into Propositional Logic</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/pal3.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2Fpal3.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2Fpal3.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/pal3.ipynb) | 2017 | <b><a href="ipynb/pal3.ipynb" title="Searching for a long Panama-style palindrome, this time letter-by-letter">World's Longest Palindrome</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Portmantout.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FPortmantout.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FPortmantout.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Portmantout.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/Portmantout.ipynb" title="Find a 1word that squishes together a bunch of words">World's Shortest Portmantout Word</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/xkcd-Name-Dominoes.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2Fxkcd-Name-Dominoes.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2Fxkcd-Name-Dominoes.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/xkcd-Name-Dominoes.ipynb) | 2018 | <b><a href="ipynb/xkcd-Name-Dominoes.ipynb" title="Lay out dominoes legally; the dominoes have people names, not numbers">xkcd 1970: Name Dominoes</a></b> |
|
||||
|
||||
|Word Games|
|
||||
|---|
|
||||
|[Portmantout Words](ipynb/Portmantout.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Portmantout.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FPortmantout.ipynb)**<br>*Find a long word that squishes together a bunch of words.*|
|
||||
|[xkcd 1970: Name Dominoes](ipynb/xkcd-Name-Dominoes.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/xkcd-Name-Dominoes.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2Fxkcd-Name-Dominoes.ipynb)**<br>*Lay out dominoes legally; the dominoes have people names, not numbers.*|
|
||||
|[Ghost](ipynb/Ghost.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Ghost.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FGhost.ipynb)**<br>*The word game Ghost (add letters, try to avoid making a word).*|
|
||||
|[World's Longest Palindrome](ipynb/pal3.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/pal3.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2Fpal3.ipynb)**<br>*Searching for a long Panama-style palindrome, this time letter-by-letter.*|
|
||||
|[Refactoring a Crossword Game Program](ipynb/Scrabble.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Scrabble.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FScrabble.ipynb)**<br>*Refactoring the Scrabble / Word with Friends game from Udacity 212.*|
|
||||
|[xkcd 1313: Regex Golf](ipynb/xkcd1313.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/xkcd1313.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2Fxkcd1313.ipynb)**<br>*Find the smallest regular expression; inspired by Randall Munroe.*|
|
||||
|[xkcd 1313: Regex Golf (Part 2: Infinite Problems)](ipynb/xkcd1313-part2.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/xkcd1313-part2.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2Fxkcd1313-part2.ipynb)**<br>*Regex Golf: better, faster, funner. With Stefan Pochmann.*|
|
||||
|[Let's Code About Bike Locks](ipynb/Fred%20Buns.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Fred%20Buns.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FFred%2520Buns.ipynb)**<br>*A tale of a bicycle combination lock that uses letters instead of digits. Inspired by Bike Snob NYC.*|
|
||||
|[Gesture Typing](ipynb/Gesture%20Typing.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Gesture%20Typing.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FGesture%2520Typing.ipynb)**<br>*What word has the longest path on a gesture-typing smartphone keyboard?*|
|
||||
|[How to Do Things with Words, or Statistical Natural Language Processing in Python](ipynb/How%20to%20Do%20Things%20with%20Words.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/How%20to%20Do%20Things%20with%20Words.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FHow%2520to%2520Do%2520Things%2520with%2520Words.ipynb)**<br>*Spelling Correction, Secret Codes, Word Segmentation, and more: grab your bag of words.*|
|
||||
|Run|Year|Math Concepts: Probability, Uncertainty, Counting, etc.|
|
||||
|---|----|---|
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Probability.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FProbability.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FProbability.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Probability.ipynb) | 2018 | <b><a href="ipynb/Probability.ipynb" title="Code and examples of the basic principles of Probability Theory">A Concrete Introduction to Probability</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/ProbabilityParadox.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FProbabilityParadox.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FProbabilityParadox.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/ProbabilityParadox.ipynb) | 2016 | <b><a href="ipynb/ProbabilityParadox.ipynb" title="Some classic paradoxes in Probability Theory, and how to think about disagreements">Probability, Paradox, and the Reasonable Person Principle</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/ProbabilitySimulation.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FProbabilitySimulation.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FProbabilitySimulation.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/ProbabilitySimulation.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/ProbabilitySimulation.ipynb" title="When the sample space is too complex, simulations can estimate probabilities">Estimating Probabilities with Simulations</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Coin%20Flip.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FCoin%20Flip.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FCoin%20Flip.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Coin%20Flip.ipynb) | 2019 | <b><a href="ipynb/Coin%20Flip.ipynb" title="How to beat the Devil at his own game">The Devil and the Coin Flip Game</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Economics.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FEconomics.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FEconomics.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Economics.ipynb) | 2018 | <b><a href="ipynb/Economics.ipynb" title="A simulation of a simple economic game">Economics Simulation</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Euler's%20Conjecture.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FEuler's%20Conjecture.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FEuler's%20Conjecture.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Euler's%20Conjecture.ipynb) | 2018 | <b><a href="ipynb/Euler's%20Conjecture.ipynb" title="Solving a 200-year-old puzzle by finding integers that satisfy a<sup>5</sup> + b<sup>5</sup> + c<sup>5</sup> + d<sup>5</sup> = e<sup>5</sup>">Euler's Sum of Powers Conjecture</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/How%20To%20Count%20Things.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FHow%20To%20Count%20Things.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FHow%20To%20Count%20Things.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/How%20To%20Count%20Things.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/How%20To%20Count%20Things.ipynb" title="Combinatorial math: how to count how many things there are, when there are a lot of them">How to Count Things</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/TwelveBalls.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FTwelveBalls.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FTwelveBalls.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/TwelveBalls.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/TwelveBalls.ipynb" title="A puzzle where you are given some billiard balls and a balance scale, and asked to find the one ball that is heavier or lighter, in a limited number of weighings">Weighing Twelve Balls</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Differentiation.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FDifferentiation.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FDifferentiation.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Differentiation.ipynb) | 2017 | <b><a href="ipynb/Differentiation.ipynb" title="A computer algebra system that manipulates expressions, including symbolic differentiation">Symbolic Algebra, Simplification, and Differentiation</a></b> |
|
||||
|
||||
|Math Concepts|
|
||||
|---|
|
||||
|[A Concrete Introduction to Probability](ipynb/Probability.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Probability.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FProbability.ipynb)**<br>*Code and examples of the basic principles of Probability Theory.*|
|
||||
|[Probability, Paradox, and the Reasonable Person Principle](ipynb/ProbabilityParadox.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/ProbabilityParadox.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FProbabilityParadox.ipynb)**<br>*Some classic paradoxes in Probability Theory, and how to think about disagreements.*|
|
||||
|[Symbolic Algebra, Simplification, and Differentiation](ipynb/Differentiation.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Differentiation.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FDifferentiation.ipynb)**<br>*A computer algebra system that manipulates expressions, including symbolic differentiation.*|
|
||||
|[Economics Simulation](ipynb/Economics.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Economics.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FEconomics.ipynb)**<br>*A simulation of a simple economic game.*|
|
||||
|[How to Count Things](ipynb/How%20To%20Count%20Things.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/How%20To%20Count%20Things.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FHow%2520To%2520Count%2520Things.ipynb)**<br>*Combinatorial math: how to count how many things there are, when there are a lot of them.*|
|
||||
|[Pairing Socks](ipynb/Socks.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Socks.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FSocks.ipynb)**<br>*What is the probability that you will be able to pair up socks as you randomly pull them out of the dryer?*|
|
||||
|[Euler's Sum of Powers Conjecture](ipynb/Euler's%20Conjecture.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Euler's%20Conjecture.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FEuler's%2520Conjecture.ipynb)**<br>*Solving a 200-year-old puzzle by finding integers that satisfy a<sup>5</sup> + b<sup>5</sup> + c<sup>5</sup> + d<sup>5</sup> = e<sup>5</sup>.*|
|
||||
|
||||
|Computer Science Algorithms and Concepts|
|
||||
|---|
|
||||
|[BASIC Interpreter](ipynb/BASIC.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/BASIC.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FBASIC.ipynb)**<br>*How to write an interpreter for the BASIC programming language.*|
|
||||
|[Bad Grade, Good Experience](ipynb/Snobol.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Snobol.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FSnobol.ipynb)**<br>*As a student, did you ever get a bad grade on a programming assignment? (Snobol, Concordance)*|
|
||||
|[The Convex Hull Problem](ipynb/Convex%20Hull.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Convex%20Hull.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FConvex%2520Hull.ipynb)**<br>*A classic Computer Science Algorithm.*|
|
||||
|[The Traveling Salesperson Problem](ipynb/TSP.ipynb) **[NB](http:/nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/TSP.ipynb)**, **[DN](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FTSP.ipynb)**<br>*Another of the classics.*|
|
||||
|Run|Year|Computer Science Algorithms and Concepts|
|
||||
|---|----|---|
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Snobol.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FSnobol.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FSnobol.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Snobol.ipynb) | 2017 | <b><a href="ipynb/Snobol.ipynb" title="As a student, did you ever get a bad grade on a programming assignment? (Snobol, Concordance)">Bad Grade, Good Experience</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/BASIC.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FBASIC.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FBASIC.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/BASIC.ipynb) | 2017 | <b><a href="ipynb/BASIC.ipynb" title="How to write an interpreter for the BASIC programming language">BASIC Interpreter</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/Convex%20Hull.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FConvex%20Hull.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FConvex%20Hull.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/Convex%20Hull.ipynb) | 2017 | <b><a href="ipynb/Convex%20Hull.ipynb" title="A classic Computer Science Algorithm">The Convex Hull Problem</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/StableMatching.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FStableMatching.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FStableMatching.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/StableMatching.ipynb) | <b><u>2020</b></u> | <b><a href="ipynb/StableMatching.ipynb" title="What is the best way to pair up two groups with each other, obeying preferences?">The Stable Matching Problem</a></b> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/master/ipynb/TSP.ipynb) [dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2FTSP.ipynb) [my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2FTSP.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/master/ipynb/TSP.ipynb) | 2018 | <b><a href="ipynb/TSP.ipynb" title="Another of the classics">The Traveling Salesperson Problem</a></b> |
|
||||
|
||||
# Index of Python Files
|
||||
|
||||
| **File** | **Description** | **Documentation**|
|
||||
|:--------|:-------------------|----|
|
||||
|[SET.py](https://github.com/norvig/pytudes/blob/master/py/SET.py)|Analyze the card game [SET](http://www.setgame.com/set).|[SET.html](http://norvig.com/SET.html)|
|
||||
|[beal.py](https://github.com/norvig/pytudes/blob/master/py/beal.py)|Search for counterexamples to Beal's Conjecture|[beal.html](http://norvig.com/beal.html)
|
||||
|[docex.py](https://github.com/norvig/pytudes/blob/master/py/docex.py)|A framework for running unit tests, similar to `doctest`.|
|
||||
|[ibol.py](https://github.com/norvig/pytudes/blob/master/py/ibol.py)|An Exercise in Species Barcoding|[ibol.html](http://norvig.com/ibol.html)
|
||||
|[lettercount.py](https://github.com/norvig/pytudes/blob/master/py/lettercount.py)|Convert Google Ngram Counts to Letter Counts|[mayzner.html](http://norvig.com/mayzner.html)
|
||||
|[lis.py](https://github.com/norvig/pytudes/blob/master/py/lis.py)|Lisp Interpreter written in Python|[lispy.html](http://norvig.com/lispy.html)
|
||||
|[lispy.py](https://github.com/norvig/pytudes/blob/master/py/lispy.py)|Even Better Lisp Interpreter written in Python|[lispy2.html](http://norvig.com/lispy2.html)
|
||||
|[lispytest.py](https://github.com/norvig/pytudes/blob/master/py/lispytest.py)|Tests for Lisp Interpreters|
|
||||
|[pal.py](https://github.com/norvig/pytudes/blob/master/py/pal.py)|Find long palindromes|[palindrome.html](http://norvig.com/palindrome.html)
|
||||
|[pal2.py](https://github.com/norvig/pytudes/blob/master/py/pal2.py)|Find longer palindromes|[palindrome.html](http://norvig.com/palindrome.html)
|
||||
|[pal3.py](https://github.com/norvig/pytudes/blob/master/py/pal3.py)|Find even longer palindromes|[palindrome.html](http://norvig.com/palindrome.html)
|
||||
|[py2html.py](https://github.com/norvig/pytudes/blob/master/py/py2html.py)|Pretty-printer to format Python files as html|
|
||||
|[spell.py](https://github.com/norvig/pytudes/blob/master/py/spell.py)|Spelling corrector|[spell-correct.html](http://norvig.com/spell-correct.html)
|
||||
|[sudoku.py](https://github.com/norvig/pytudes/blob/master/py/sudoku.py)|Program to solve sudoku puzzles|[sudoku.html](http://norvig.com/sudoku.html)
|
||||
|[testaccum.py](https://github.com/norvig/pytudes/blob/master/py/testaccum.py)|Tests for my failed Python `accumulation display` proposal|[pyacc.html](http://norvig.com/pyacc.html)
|
||||
|[yaptu.py](https://github.com/norvig/pytudes/blob/master/py/yaptu.py)|Yet Another Python Templating Utility|
|
||||
| File | Description | Documentation |
|
||||
|:--|:----|----|
|
||||
|[beal.py](/blob/master/py/beal.py)|*Search for counterexamples to Beal's Conjecture*|[documentation](http://norvig.com/beal.html)|
|
||||
|[docex.py](/blob/master/py/docex.py)|*A framework for running unit tests, similar to `doctest`*||
|
||||
|[ibol.py](/blob/master/py/ibol.py)|*An Exercise in Species Barcoding*|[documentation](http://norvig.com/ibol.html)|
|
||||
|[lettercount.py](/blob/master/py/lettercount.py)|*Convert Google Ngram Counts to Letter Counts*|[documentation](http://norvig.com/mayzner.html)|
|
||||
|[lis.py](/blob/master/py/lis.py)|*Lisp Interpreter written in Python*|[documentation](http://norvig.com/lispy.html)|
|
||||
|[lispy.py](/blob/master/py/lispy.py)|*Even Better Lisp Interpreter written in Python*|[documentation](http://norvig.com/lispy2.html)|
|
||||
|[lispytest.py](/blob/master/py/lispytest.py)|*Tests for Lisp Interpreters*||
|
||||
|[pal.py](/blob/master/py/pal.py)|*Find long palindromes*|[documentation](http://norvig.com/palindrome.html)|
|
||||
|[pal2.py](/blob/master/py/pal2.py)|*Find longer palindromes*|[documentation](http://norvig.com/palindrome.html)|
|
||||
|[pal3.py](/blob/master/py/pal3.py)|*Find even longer palindromes*|[documentation](http://norvig.com/palindrome.html)|
|
||||
|[pytudes.py](/blob/master/py/pytudes.py)|*Pre-process text to generate this README.md file.*||
|
||||
|[py2html.py](/blob/master/py/py2html.py)|*Pretty-printer to format Python files as html*||
|
||||
|[SET.py](/blob/master/py/SET.py)|*Analyze the card game SET*|[documentation](http://norvig.com/SET.html)|
|
||||
|[spell.py](/blob/master/py/spell.py)|*Spelling corrector*|[documentation](http://norvig.com/spell-correct.html)|
|
||||
|[sudoku.py](/blob/master/py/sudoku.py)|*Program to solve sudoku puzzles*|[documentation](http://norvig.com/sudoku.html)|
|
||||
|[testaccum.py](/blob/master/py/testaccum.py)|*Tests for my failed Python `accumulation display` proposal*|[documentation](http://norvig.com/pyacc.html)|
|
||||
|[yaptu.py](/blob/master/py/yaptu.py)|*Yet Another Python Templating Utility*||
|
||||
|
||||
# Etudes for Programmers
|
||||
|
||||
I got the idea for the "etudes" part of the name from
|
||||
I got the idea for the *"etudes"* part of the name from
|
||||
this [1978 book](https://books.google.com/books/about/Etudes_for_programmers.html?id=u89WAAAAMAAJ)
|
||||
by [Charles Wetherell](http://demin.ws/blog/english/2012/08/25/interview-with-charles-wetherell/)
|
||||
that was very influential to me when I was first learning to program.
|
||||
by [Charles Wetherell](http://demin.ws/blog/english/2012/08/25/interview-with-charles-wetherell)
|
||||
that was very influential to me when I was first learning to program. I still have my copy.
|
||||
|
||||

|
||||
@@ -4,12 +4,12 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div align=\"right\"><i>Peter Norvig, 23 Oct 2017<br>Revised 2 June 2019</i></div>\n",
|
||||
"<div align=\"right\"><i>Peter Norvig, Oct 2017<br>Revised Jan 2020</i></div>\n",
|
||||
"\n",
|
||||
"# Bicycling: Speed v. Grade, VAM, Eddington, Hill-index\n",
|
||||
"\n",
|
||||
"Like most people, I bike slower when I'm going up a steep hill than on a flat road. But how much slower?\n",
|
||||
"To answer that, I downloaded all my recorded [Strava](https://www.strava.com/athletes/575579) rides longer than 25 miles as [`bikerides25.tsv`](bikerides25.tsv). I parse the file into `rides`, a list of `Ride` structures. I filter out all the rides that have exactly 0 ft climbing (i.e. stationary bike rides) and that have `'MTB'` in the title (i.e. mountain bike rides that are not relevant to road bike riding)."
|
||||
"To answer that, I downloaded a bunch of my recorded [Strava](https://www.strava.com/athletes/575579) rides longer than 25 miles as [`bikerides25.tsv`](bikerides25.tsv). I parse the file into `rides`, a list of `Ride` structures. I filter out all the rides that have exactly 0 ft climbing (i.e. stationary bike rides) or that have `'MTB'` in the title (i.e. mountain bike rides that are not relevant to road bike riding) or are blank or are comment lines (start with `#`)."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -37,7 +37,7 @@
|
||||
"def hours(sec, min, hour=0) -> float: return int(sec)/3600 + int(min)/60 + int(hour)\n",
|
||||
"\n",
|
||||
"rides = [parse_ride(line) for line in open('bikerides25.tsv') \n",
|
||||
" if 'MTB' not in line and '\t0 ft' not in line]"
|
||||
" if line.strip() and not line.startswith('#') and 'MTB' not in line and '\t0 ft' not in line]"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -59,7 +59,7 @@
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
@@ -124,7 +124,7 @@
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAykAAAKSCAYAAADWEJRoAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvnQurowAAIABJREFUeJzsvX18W3Xd///q/Vh307GVtlu3UGCyTeDy8gL5AXYC4rhxrFWogANRoYiCPkBUJmQTWGTcKHo94Hu52SlwyUAoN4UqgpcK10XH0G0MNxmMjnXpepMsLU3SJCQhSX9/1BObk5zkJDnnk0/eez8fjz2kyUle5/1+fU487/O5K3G73RNgGIZhGIZhGIaRhNJCnwDDMAzDMAzDMMxUuEhhGIZhGIZhGEYquEhhGIZhGIZhGEYquEhhGIZhGIZhGEYquEhhGIZhGIZhGEYquEhhGIZhGIZhGEYquEhhGIZhipqTTz4ZJ598su7j7XY7ampq8M1vftPEs2IYhmHygYsUhmGYI5ienh4cffTRqKmpwR133JH0vtfrxZo1a3DhhRdiyZIlqKurwwknnIBzzz0X//Vf/wW/35/ye71eL37605+iubkZFosFixYtwhlnnIE777wThw8fNjkqhmEYptgpL/QJMAzDMIXB4/Hg+uuvx/Tp0+Hz+VIeMzY2hkcffRSf/OQnsWLFCsybNw9erxf/93//h9tuuw3//d//jZdffhmzZ89O+N7Pfvaz2L9/P/7t3/4NX/7yl1FSUoJt27bhZz/7GR5//HG88sormD9/viFxvPDCC4Z8D8MwDCMPXKQwDMMcoXz/+9+H3+/Hd7/7Xdx1110pj2lsbER/fz8qKiqS3rvuuuvw1FNPYfPmzbjlllvirz/66KPYv38/LrvsMmzatCnhMzfeeCMee+wxbN68GevWrTMkjqamJkO+h2EYhpEHHu7FMAxzBNLV1YWnnnoK999/P+rq6jSPKysrS1mgAEBLSwsA4P333094/eDBgwCACy+8MOkzF110EQBgZGRE13m+9tpr8fkjvb29uPrqq3H88cdjzpw52L17NwDtOSnj4+O47bbbsGzZMtTV1eG0007DQw89hImJCU29YDCIBx98EJ/5zGewYMECzJ8/H2effTZ+/etfp/zcH/7wB7S0tGDJkiU45phjcOKJJ+L888/HT3/6U13xMQzDMKnhnhSGYZgjjOHhYdx8881oaWnBpZdeii1btuT0PS+99BIA4KSTTkp4fcmSJQCAl19+Ga2trQnvvfzyywCAs88+Oyutvr4+fPazn8WJJ56Iyy67DOPj45g+fbrm8aFQCC0tLXjzzTexbNkytLW1wev14ic/+Qm2bt2a8jPj4+NobW3Fzp07ccopp+DLX/4yAODPf/4zvvvd72L79u34xS9+ET/+kUcewU033YRjjjkG559/PmprazE6Oop9+/bh4YcfTuhdYhiGYbKDixSGYZgjiImJCXzrW99CRUUFHnjgAd2fi0QiuP/++wFMzlPZtm0b9uzZg+XLl+PrX/96wrFf+cpX8Mwzz+CJJ57Au+++izPOOAMlJSV44403sHfvXtx666344he/mNV5v/HGG7jllluwdu1aXcc/9NBDePPNN3HRRRfhscceQ2np5MCBm2++WbNAuu2227Bz507ccccduOmmm+Kvh0IhXHXVVXjiiSewatWqeA/Rww8/jMrKSvT09OCYY45J+K7R0dGs4mMYhmES4SKFYRjmCGLTpk145ZVXsGXLFsydO1f35yKRCO69996E1y6//HLcf//9mDZtWsLr06ZNwwsvvIDbb78dmzdvxq5du+Lvff7zn48P+cqGY445Bj/4wQ90H79lyxaUlJTgzjvvjBcoALBo0SJ84xvfwIYNGxKOHxsbwxNPPIFTTjkloUABgKqqKqxbtw5//OMf8eSTTyYMYysvL085HC6b3DIMwzDJcJHCMAxzhLBv3z7ccccduPzyy/H5z38+q89OmzYNbrcbExMTcDgcePXVV3HXXXfhnHPOwdNPP41jjz02fuwHH3yAq666Cvv27cMvf/lLnHvuuSgvL8cbb7yBNWvWYMWKFXjsscfwuc99Trf+SSedhKqqKl3Hjo+P48CBA6ivr8fixYuT3j/rrLOSXtu5cycikQhKS0uTChhgskgDgPfeey/+WltbG6xWK04//XR84QtfwJlnnonTTz8d9fX1esNiGIZhNOAihWEY5gjhuuuuw9y5c5N6RLKhpKQEDQ0NuOKKK7B48WKcd955uOWWW/DMM8/Ej7n99tuxdetWPPbYY1i5cmX89QsuuADz58/H8uXL8f3vfx9vvfWWbl31cKp0eL1eAEBtba3u7/rggw8AAG+99Vba85q6VPONN96I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 truncated
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 864x720 with 1 Axes>"
|
||||
]
|
||||
@@ -141,7 +141,7 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"So, I average a little under 14 mph when the overall route is fairly flat, with a lot of variability from 12 to 16 mph, depending more on my level of effort than on the grade of the road. But from around 50 ft/mile grade speed falls off quickly: down to 12mph at 80 ft/mile; 11 mph at 100 ft/mile; and under 10 mph at 120 ft/mile. Note that even 120 ft/mile is only 2.3% grade, but if you figure a typical route is 1/3 up, 1/3 down, and 1/3 flat, then that's 6.8% average grade on the up part."
|
||||
"So, I average a little under 14 mph when the overall route is fairly flat, with a lot of variability from 12 to 16 mph, depending more on my level of effort than on the grade of the road. But from around 50 ft/mile grade speed falls off quickly: down to 12mph at 80 ft/mile; 11 mph at 100 ft/mile; and around 10 mph at 120 ft/mile. Note that even 120 ft/mile is only 2.3% grade, but if you figure a typical route is 1/3 up, 1/3 down, and 1/3 flat, then that's 6.9% average grade on the up part."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -172,7 +172,7 @@
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"(69.10185351454508, 62.471852498446204)"
|
||||
"'Coast: 69.1 min, Creek: 62.5 min.'"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
@@ -181,7 +181,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"duration(15.7, 344), duration(13.5, 735)"
|
||||
"f'Coast: {duration(15.7, 344):.1f} min, Creek: {duration(13.5, 735):.1f} min.'"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -197,7 +197,7 @@
|
||||
"source": [
|
||||
"# Segments\n",
|
||||
"\n",
|
||||
"The data I have so far helps me estimate the duration for a long ride over varying up-and-down terrain. But what if I just want to know how long it will take to climb the next hill? To answer that I'll go back to Strava and retrieve data from individual segments, some only a fraction of a mile long, some several miles long, but most with a relatively constant grade. I picked some representative climbing segments ([`bikesegments.csv`](bikesegments.csv)) with the segment length in miles and climb in feet, along with several of my times on the segment. (Strava shows my times for up to 10 rides on the segment; I picked the first, middle, and last of the rides shown.) "
|
||||
"The data I have so far helps me estimate the duration for a long ride over varying up-and-down terrain. But what if I just want to know how long it will take to climb the next hill? To answer that I'll go back to Strava and retrieve data from individual segments, some only a fraction of a mile long, some several miles long, but most with a relatively constant grade. I picked some representative climbing segments ([`bikesegments.csv`](bikesegments.csv)) with the segment length in miles and climb in feet, along with several of my times on the segment. (Strava shows my times for up to 10 rides on each segment; I picked the first, middle, and last of the rides shown.) "
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -234,7 +234,7 @@
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAykAAAKSCAYAAADWEJRoAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvnQurowAAIABJREFUeJzsnXt0nFW5/7+TTJI2adq0aUjSlg4BQqVc9YBoIREBWy4pBDWCIiJKUBDOQdCjB8PtELmJB5dwFCkiChQhWlJaLsKPc4SmgrZcLBfF9BAmaTKZJulcMjOdmczl90eckMvkTebN+zx79vB81mItTSaf7P2Z3aGb92bzer1JCIIgCIIgCIIgZAl5qgcgCIIgCIIgCIIwHtmkCIIgCIIgCIKQVcgmRRAEQRAEQRCErEI2KYIgCIIgCIIgZBWySREEQRAEQRAEIauQTYogCIIgCIIgCFmFbFIEQRAErTnqqKNw1FFHzfr1TqcTZWVluOyyywhHJQiCIMwF2aQIgiB8iNi8eTO++93v4owzzsCBBx6IsrIyfO1rX5v29X19fbjvvvvwhS98AccccwwqKyvhcDhw5plnYuPGjUgmpz5q67LLLkNZWZnhP2effTblNAVBEATNsasegCAIgsDHj370I7z11ltYsGABli1bhuHhYcPX33ffffjJT36CFStW4KSTTsKyZcvgcrmwdetWXH755Xjuuefw4IMPTviZs846CytXrkzre+yxx/D+++/jM5/5jFVTwpNPPmmZSxAEQcgObPLEeUEQhA8PL730EpYvX46DDz4YHR0dWL9+PT772c/igQceSPv6J598EmVlZairq4PNZhv7usvlwmmnnYbe3l785je/mdWREa/Xi8MPPxzxeBx/+9vfUF5ebtm8MsHpdOKYY47BF7/4Rfz85z9XMgZBEATBGDndSxAE4UNEfX09DjnkkAkbDiPOPvts1NfXT3l9dXX12Gli27Ztm5Xrsccew/79+7F+/fpZb1C2bds2dv1IZ2cnLrroIhxyyCFYvHgxdu3aBWD6a1KGh4dx7bXXYvXq1aisrMTxxx+Pe+65J+0painC4TDuvvtufOpTn8Ly5cuxbNkynHzyyXjggQfS/twzzzyDc845Bx/5yEdwwAEHYNWqVVi3bh1+/OMfz2p+giAIQnrkdC9BEATBFAUFBQAAu312/yr59a9/DQC46KKLMv5dXV1dOPXUU7Fq1Sqcd955GB4eRnFx8bSvj0QiOOecc/Daa69h9erVaGpqgt/vx5133ont27en/Znh4WE0Njbi1VdfxdFHH40vfelLAIAXXngBV199NXbs2DHhyMuDDz6Iq666CgcccADWrVuHiooKDA0N4d1338WvfvUrXHPNNRnPUxAEQRhFNimCIAhCxsRiMfz2t78FAJx22mkzvv4vf/kL3nnnHRx66KGor6/P+Pe98soruOaaa3DdddfN6vX33HMPXnvtNZx55pl4+OGHkZc3euLAt7/9bZx88slpf+baa6/Fq6++ihtvvBFXXXXV2NcjkQguvPBCPProozj77LNxxhlnAAB+9atfobCwEB0dHTjggAMmuIaGhjKeoyAIgvABcrqXIAiCkDEtLS145513sG7dOpx66qkzvj51cb2ZoygAcMABB+Df//3fZ/36Rx55BDabDTfddNPYBgUAVq5ciW984xtTXu/xePDoo4/i6KOPnrBBAYCioiJcf/31AEZPWRuP3W4fO6I0HlXX2wiCIOQKciRFEARByIj/+q//wr333ovDDz8cv/jFL2Z8vc/nQ3t7OwoLC8dOocqUI488EkVFRbN67fDwMN577z1UVVWhtrZ2yvdPPPHEKV979dVXEYvFkJeXh1tvvXXK92OxGADgH//4x9jXmpqa0NLSghNOOAHnnnsu1qxZgxNOOAFVVVWznZYgCIIwDbJJEQRBEGbNnXfeidbWVhxxxBHYvHkzysrKZvyZxx9/HKFQCJ/73OdMH2GYfDqVEX6/HwBQUVExa9e+ffsAAG+88QbeeOONad2BQGDsf19xxRWoqKjAAw88gPvvv39sw3b88cfj+uuvR11d3azHLAiCIExETvcSBEEQZkVraytaW1tx7LHHYuvWrVi6dOmsfi51wfxXv/pV0797tncjA4CFCxcCAAYGBtJ+f+/evdP+zKWXXgqv1zvtP6k7iqU477zz8Ic//AFdXV3YtGkTmpub8de//hVNTU3YvXv3rMcsCIIgTEQ2KYIgCMKMXHvttbjzzjtx/PHHY/PmzVi8ePGsfm7nzp146623cOihh7IdWSgtLcXBBx8Mt9uddqOQ7u5exx13HPLy8vDyyy+b+p0LFy7EKaecgh/96Ee44oorEA6H8fzzz5tyCYIgCLJJEQRB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 truncated
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAykAAAKSCAYAAADWEJRoAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvnQurowAAIABJREFUeJzsvX1822W9//9KmrZbb9Z2Xdd2d6FCGewoKqAcBx2IuAFurNxUQOR4BAqiU4eI8sM6UMqN6BG/D1DgFEHFMbEKxU1EOSqwDTjcjLGBR+ygpmxtsrbLTZMsSXPz+6Ok9iZNk08/7/eVK7yfj4ePxzlt8ux1PT9Xs14kn8/H4vF4EhAEQRAEQRAEQcgRrKoHIAiCIAiCIAiCMB7ZpAiCIAiCIAiCkFPIJkUQBEEQBEEQhJxCNimCIAiCIAiCIOQUskkRBEEQBEEQBCGnkE2KIAiCIAiCIAg5hWxSBEEQBK35wAc+gA984AMZP97hcKCyshJXX3014agEQRCE2SCbFEEQhPcQjz/+OK677jqcddZZWLp0KSorK3HZZZdN+3ifz4frr78eZ511Fo455hjU1tbiqKOOwumnn46f/OQnCAQCGf3cDRs2oLKyEpWVlfjHP/5h1nQEQRCEPMWmegCCIAgCH9///vfx+uuvo6ysDIsWLcLw8HDax7vdbvz85z/H8ccfj9WrV2PBggXw+Xx49tlnccMNN+AXv/gF/vjHP6KiomJax7Zt2/DLX/4SZWVl8Pv9Zk8Jv/vd70x3CoIgCGqRTYogCMJ7iFtvvRWLFy/G+973PuzYsQPr1q1L+/glS5agt7cXhYWFU7535ZVX4te//jXuv/9+XHvttSmf73K58NWvfhXnn38+nE4ndu7caco8xtPQ0GC6UxAEQVCLfNxLEAThPcSqVatw5JFHwmKxZPT4goKClBsUAFi/fj0A4K233pr2+V/+8pdRWFiIH/zgB9kPFsD27dvHzh/p7u7G5z73ORx55JGoqqrCnj17AEx/Tsrw8DBuuOEGrFixArW1tfjIRz6Cu+++G4lEYtqfFwqFcNddd+HUU0/F4sWLsWjRIpx22ml44IEHUj7vD3/4A9avX49jjjkGCxcuxPLly7FmzRr813/9l6H5CoIgCKPIOymCIAiCIZ588kkAwPvf//6U33/ggQfwpz/9CY888giqqqpm9bN6enrwiU98AsuXL8eFF16I4eFhlJSUTPv4cDiM9evXY9euXVixYgVaWlrg8/nwgx/8YNp3c4aHh9Hc3IxXXnkFxx13HD7zmc8AAP785z/ja1/7Gl566SXcc889Y4//2c9+ho0bN2LhwoVYs2YNampqMDQ0hDfffBMPPvjgtO8uCYIgCDMjmxRBEARhRqLRKL7//e8DGD1P5fnnn8fevXuxatWqlCfe79u3D21tbfjsZz+LNWvWzPrnv/DCC7j22mvx7W9/O6PH33333di1axfOPvts/PKXv4TVOvrBgWuuuQannXZayufccMMNeOWVV3DTTTdh48aNY18Ph8O49NJLsWXLFpxzzjk466yzAAAPPvggioqKsGPHDixcuHCCa2hoyMAsBUEQhCTycS9BEARhRqLRKL73ve/he9/7Hv77v/8be/fuxUUXXYTNmzdjzpw5Ux571VVXobq6GrfeeqspP3/hwoX4xje+kfHjN2/eDIvFgu985ztjGxQAWLZsGa666qopj3e73diyZQuOO+64CRsUACguLsamTZsAAI888siE79lstpQfh6uurs54rIIgCMJU5J0UQRAEYUbmzJkDj8eDRCIBp9OJp59+Gt/97nfx8Y9/HL/5zW9wxBFHjD32jjvuwK5du9DV1YV58+aZ8vPf//73o7i4OKPHDg8P4+2330ZdXR0aGxunfP/kk0+e8rVXXnkF0WgUVqsVt91225TvR6NRAJhw+eSWlha0tbXhpJNOwrnnnouVK1fipJNOQl1dXabTEgRBEKZBNimCIAhCxlgsFtTX1+Piiy9GY2MjzjjjDFx77bX47W9/CwB47bXX8MMf/hBXXHEFTj31VNN+7uSPU6XD5/MBAGpqajJ2HTp0CACwe/du7N69e1r3+Esob9iwATU1NXjggQdw//3347777gMAfOQjH8GmTZvQ1NSU8ZgFQRCEicjHvQRBEARDnHjiiaioqMDOnTvHrnz1+uuvIxqNoqOjY+zmjcn/JU9Y/+hHP4rKykps27Yt45+V6dXIAIy9ezMwMJDy+wcPHpz2OVdeeSU8Hs+0/0teUSzJhRdeiD/+8Y/o6enBo48+itbWVrz22mtoaWnBvn37Mh6zIAiCMBF5J0UQBEEwxPDwMIaHhzF37tyxTcRRRx2FSy+9NOXj//SnP8HlcmH9+vWYN28eli1bRjKu8vJyvO9970NPTw/27duHo446asL3U13d68QTT4TVasXzzz9v6GfOmzcPp59+Ok4//XSUl5fjhz/8IZ566qkpP1sQBEHIDNmkCIIgCNPyxhtv4Mgjj5xycnwkEsF1112HeDyO1atXj339pJNOwkknnZTS9alPfQoulwvf+ta3cPTRR5OO+5JLLsHNN9+MTZs2Tbi6V29v79jHssazYMECXHjhhdiyZQtuu+02XHfddbDZJv4TeeDAAQQCgbGxP/PMM1i1atWUd3lcLhcApL1EsiAIgpAe2aQIgiC8h9i2bRt+//vfA/jXx55efvllXH311WOPGX8vkIceeggPP/wwTjrpJCxduhQVFRXo7+/HX//6V7hcLhx55JG45ZZbeCeRARs2bMDvf/97PPHEE2hqasIZZ5wBn8+Hxx57DB/72Mfwhz/8Ycpz7rjjDrz99tv43ve+h0ceeQQrV65EbW0tXC4X9u3bh5deegm33HLL2Cbl0ksvRWlpKU488UQsW7YMFosFr7zyCp5//nk0NDSgubmZe9qCIAh5g2xSBEEQ3kPs3bsXW7ZsmfC13t5e9Pb2jv3/4zcpzc3NCAQCeOmll/Diiy/C7/ejvLwcxxxzDL70pS/h8ssvR2lpKdv4M6W4uBhdXV24/fbb8dhjj+Hee+/FsmXLcO2112LdunUpNynl5eXYtm0bHnroIXR2dmLbtm0IhUKoqamB3W7HjTfeiHPPPXfs8TfddBP+8pe/YO/evfjzn/8Mm82GJUuW4Jvf/CauuuoqVFRUcE5ZEAQhr7B4PJ6E6kEIgiAIgiAIgiAkkat7CYIgCIIgCIKQU8gmRRAEQRAEQRCEnEI2KYIgCIIgCIIg5BSySREEQRAEQRAEIaeQTYogCIIgCIIgCDmFbFIEQRAEQRAEQcgpZJMiCIIgCIIgCEJOIZuUNLhcLi3dANDd3U3m1rkLpZ+yOaBvd1nravyy1vnd1H7pzu8G5DVGhVvWuhp/rq112aSkobq6Wks3NTp3ke755aZG5/Uo3fndHH5KdO0uzdX4pTu/m8NPhZFxyyYlDfF4XEs3NTp3ke755aZG5/Uo3fndHH5KdO0uzdX4pTu/m8NPhZFx5+UmxeVyIRqNIhKJYGBgAADg9Xrh9/sBAE6nE7FYDOFwGIODgwAAj8eDQCAAAOjv70c8HsfQ0BCGhoYAAG63G8FgEADQ19cHAAgGg3C73QCAoaEhhEIhxONx9Pf3AwACgQA8Hg8AYHBwEOFwGLFYDE6nc2w8Xq8XADAwMIBIJIJoNDr2lpjP54PP5zN1TqFQaNZzOnjwYMo5AZj1nA4ePEg6J5fLldVxMmNOZh0nr9drytpLNadDhw6Rzamvr8+0tZdqTkkojtOhQ4dIf58GBwfJ1p7X6yV7jUhixtpLNafkMaH4fXK5XKa+lk+eU39/vzav5ZPn5Ha7yV733G432ZyS36d6LU9CcZyGhoZMfS2fPKeBgQGytef1ek3/2yg5pyRU/+Ym/2+K3yen00n6d0RfXx/Za0Ty+Zkep2zm5Ha7p8xpJiwejycx46ME7eju7kZjY6PqYbynkOZqkO78SHM1SHc1SHd+pLkacq17Xr6TYhaZ7PJy0U2Nzl2ke365qdF5PUp3fjeHnxJdu0tzNX7pzu/m8FNhZNyySUlDQUGBlm5qdO4i3fPLTY3O61G687s5/JTo2l2aq/FLd343h58KI+OWTUoaysrKtHRTo3MX6Z5fbmp0Xo/Snd/N4adE1+7SXI1fuvO7OfxUGBm3bFLSkDw5SDc3NTp3ke755aZG5/Uo3fndHH5KdO0uzdX4pTu/m8NPhZFxyyYlDTU1NVq6qdG5i3TPLzc1Oq9H6c7v5vBTomt3aa7GL9353Rx+KoyMWzYpaYhGo1q6qdG5i3TPLzc1Oq9H6c7v5vBTomt3aa7GL9353Rx+KoyMWzYpaRgeHtbSTY3OXaR7frmp0Xk9Snd+N4efEl27S3M1funO7+bwU2Fk3LJJScOCBQu0dFOjcxfpnl9uanRej9Kd383hp0TX7tJcjV+687s5/FQYGbdsUtKQvNOmbm5qdO4i3fPLTY3O61G687s5/JTo2l2aq/FLd343h58KI+OWTUoaCgsLtXRTo3MX6Z5fbmp0Xo/Snd/N4adE1+7SXI1fuvO7OfxUGBm3bFLSUFpaqqWbGp27SPf8clOj83qU7vxuDj8lunaX5mr80p3fzeGnwsi4ZZOShv7+fi3d1OjcRbrnl5sandejdOd3c/gp0bW7NFfjl+78bg4/FUbGLZuUNNTW1mrppkbnLtI9v9zU6LwepTu/m8NPia7dpbkav3Tnd3P4qTAybtmkpCESiWjppkbnLtI9v9zU6LwepTu/m8NPia7dpbkav3Tnd3P4qTAybtmkpCEQCGjppkbnLtI9v9zU6LwepTu/m8NPia7dpbkav3Tnd3P4qTAy7rzcpLhcLkSjUUQiEQwMDAAAvF4v/H4/AMDpdCIWiyEcDmNwcBDA6KXRkgH7+/sRj8dRWlqKoaEhAIDb7UYwGAQA9PX1AQCCwSDcbjcAYGhoCKFQCPF4fOxzd4FAYOySa4OLine truncated
|
||||
"text/plain": [
|
||||
"<Figure size 864x720 with 1 Axes>"
|
||||
]
|
||||
@@ -253,11 +253,11 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"So I can cruise at 17 mph on a 1% grade, but only a bit over 6 mph at 6% grade, and around 5 mph on 8% grades.\n",
|
||||
"So I can cruise at 16 or 17 mph on a 1% grade, but only about 8 mph at 5% grade, and around 5 mph on 8% grades.\n",
|
||||
"\n",
|
||||
"# VAM\n",
|
||||
"\n",
|
||||
"The number of vertical feet (or meters) climbed per hour (regardless of horizontal distance travelled) is known as [VAM](https://en.wikipedia.org/wiki/VAM_%28bicycling%29), which stands for *velocità ascensionale media* (for native Campagnolo speakers) or *mean ascent velocity* (for SRAM) or 平均上昇率 (for Shimano). Legend has it that at grades between about 6% to 12%, most of your power is going into lifting against gravity, so your VAM should be about constant no matter what the grade in that range. (I guess that below that grade, power is spent on wind and rolling resistance, and above it, power goes largely to cursing *sotto voce*, as they say in Italian.) Here's a plot of VAM versus grades (in percent):"
|
||||
"The number of vertical feet (or meters) climbed per hour (regardless of horizontal distance travelled) is known as [VAM](https://en.wikipedia.org/wiki/VAM_%28bicycling%29), which stands for *velocità ascensionale media* (for native Campagnolo speakers) or *mean ascent velocity* (for SRAM) or 平均上昇率 (for Shimano). The theory is that for medium-steepish climbs, most of your power is going into lifting against gravity, so your VAM should be about constant no matter what the grade in that range. (For flatish rides power is spent on wind and rolling resistance, and for super-steep rides power goes largely to cursing *sotto voce*, as they say in Italian.) Here's a plot of my VAM versus grades (in percent):"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -267,7 +267,7 @@
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 864x720 with 1 Axes>"
|
||||
]
|
||||
@@ -288,7 +288,7 @@
|
||||
"source": [
|
||||
"I don't see the flattening effect; I see a wide spread of VAM numbers; at about 8% I range from 1500 to 3000 VAM. Champions can do 6000. \n",
|
||||
"\n",
|
||||
"Another way to look at it: at what speed am I getting the most VAM (implicitly assuming I'm giving a similar level of effort at all times, which is not really true)? The curve says 5½ mph, but the data is pretty noisy.\n",
|
||||
"Another way to look at it: at what speed am I getting the most VAM (implicitly assuming I'm giving a similar level of effort at all times, which is not really true)? The curve says 5 or 6 mph, but the data is pretty noisy.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
@@ -299,7 +299,7 @@
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAzoAAAKSCAYAAAAaia6DAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvnQurowAAIABJREFUeJzsvXt4VNW9//8OAQIESGgIuQiEoBQVsZxW8VusFgXvgPEoxUvVak+seDzWO1UjaDs9ilhra73iraf2tIjVKKi1toIVa0X0oV5QmnOMwyXJJBkymWTCzDAz+f3Bb+Yk5sKePfNZe6/F+/U8Pk8bxtd81mtWYhYzsycnEAh0gxBCCCGEEEIMYojTAxBCCCGEEEJItuFBhxBCCCGEEGIcPOgQQgghhBBCjIMHHUIIIYQQQohx8KBDCCGEEEIIMQ4edAghhBBCCCHGwYMOIYQQAmDmzJmYOXOm5dt7vV4UFhZi6dKlglMRQgixCw86hBBC0ubFF1/ETTfdhDPOOAOTJk1CYWEhLr/88gFv39DQgMceewzf+c538LWvfQ0lJSWoqKjAmWeeif/+7/9Gd3ffj3RbunQpCgsLB/1n0aJFksskhBCiMUOdHoAQQoh+rFq1Ch9//DFGjx6N8vJydHR0DHr7xx57DPfffz8mTpyIb33rWygvL0djYyPWr1+Pq666Cn/605/w9NNP9/p3zjrrLEyePLlf35o1a/DFF1/glFNOydaS8NJLL2XNRQghxHlyAoFA379GI4QQQgbhr3/9Kw455BBMnToVmzZtwsKFC/Gv//qvePLJJ/u9/UsvvYTCwkKccMIJyMnJSX29sbER8+fPx+7du/Ff//Vflp6hCQQCOOKIIxCPx/Hpp5+iqKgoa+tKB6/Xi6997Wu44IIL8PDDDzsyAyGEkIHhS9cIIYSkzYknnohDDz2016FlMBYtWoQTTzyxz+3LyspSL3l76623LLnWrFmDvXv3YuHChZYPOW+99Vbq/TR1dXW49NJLceihh2LcuHH48MMPAQz8Hp2Ojg7ceuutOPLII1FSUoJjjz0Wv/rVr/p9uV2ScDiMBx54AN/+9rdxyCGHoLy8HHPnzsWTTz7Z77/36quv4uyzz8bhhx+OCRMmYPr06TjttNPws5/9zNL6CCGE9IUvXSOEEOIow4YNAwAMHWrtP0m//vWvAQCXXnpp2vdVX1+PefPmYfr06ViyZAk6OjowatSoAW8fiURw9tln44MPPsCRRx6JxYsXIxgM4t5778Xbb7/d77/T0dGBqqoqvP/++zj66KNx4YUXAgD+8pe/4Prrr8d7773X6xmgp59+Gtdeey0mTJiA0047DcXFxfD7/di+fTueeuop3HDDDWmvkxBCCA86hBBCHCQWi+H3v/89AGD+/PkHvP3mzZuxbds2HHbYYTjxxBPTvr+///3vuOGGG3D77bdbuv2vfvUrfPDBBzjzzDPxzDPPYMiQ/S+EuO666zB37tx+/51bb70V77//Pu644w5ce+21qa9HIhFcfPHF+N3vfodFixbhjDPOAAA89dRTGD58ODZt2oQJEyb0cvn9/rTXSAghZD986RohhBDHqKmpwbZt23Daaadh3rx5B7x98oIFdp7NAYAJEybg5ptvtnz73/72t8jJycGdd96ZOuQAwOTJk/GDH/ygz+3b2trwu9/9DkcffXSvQw4A5OXlYfny5QD2v/yuJ0OHDk09s9UTp95/RAghJsBndAghhDjCfffdh0ceeQRHHHEEHn300QPevr29HbW1tRg+fHjq5WDpctRRRyEvL8/SbTs6OvD555+jtLQU06ZN6/Pnxx9/fJ+vvf/++4jFYhgyZAjuuuuuPn8ei8UAAP/85z9TX1u8eDFqampw3HHH4ZxzzsGcOXNw3HHHobS01OqyCCGE9AMPOoQQQpRz7733wuPxYMaMGXjxxRdRWFh4wH/n2WefRVdXF84991zbz3R8+aVhgxEMBgEAxcXFll179uwBAGzduhVbt24d0N3Z2Zn631dffTWKi4vx5JNP4vHHH08d+o499lgsX74cJ5xwguWZCSGE/B986RohhBCleDweeDwezJo1C+vXr8f48eMt/XvJixB873vfs33fVq8SBwBjx44FALS0tPT7583NzQP+O1dccQUCgcCA/ySv9JZkyZIleO2111BfX4/nn38e1dXV+Mc//oHFixfjf/7nfyzPTAgh5P/gQYcQQogybr31Vtx777049thj8eKLL2LcuHGW/r0tW7bg448/xmGHHabsGY4xY8Zg6tSp8Pl8/R42+rvq2jHHHIMhQ4bgnXfesXWfY8eOxcknn4xVq1bh6quvRjgcxuuvv27LRQghBzs86BBCCBGnu7sb119/PR566CHMmTMH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 truncated
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAzoAAAKSCAYAAAAaia6DAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvnQurowAAIABJREFUeJzsvXt0lNW9//8OAQIESDD3CMaoKVbEw2kVTvELVbFeaoR4NIuqta3aqFiP9VqLxvu0ilTbntZb8VKPeiwNYgSq0otghVIRXVTUSqOGCeQymYyZXM2EzOT3B7+Zk5ALzzwzn/08e/t+reVabRhf89mv2QnZzswzKcFgsB+EEEIIIYQQYhBjnB6AEEIIIYQQQpINDzqEEEIIIYQQ4+BBhxBCCCGEEGIcPOgQQgghhBBCjIMHHUIIIYQQQohx8KBDCCGEEEIIMQ4edAghhBAAs2fPxuzZsy3f3uv1IjMzE8uWLROcihBCiF140CGEEBI3L7/8Mm6++WacffbZmDFjBjIzM3HZZZeNePv29nb8+Mc/xtlnn41jjz0WeXl5OOaYY3DaaafhkUceQVdXl6X7veaaa5CZmYnMzEz861//StZyCCGEGMhYpwcghBCiHytXrsT777+PyZMno7CwEB0dHaPevrW1Fc888wy+8pWv4IwzzkB2djba29vx17/+Fbfeeiv+53/+Bxs3bkRGRsaIjg0bNuC5557D5MmT0dnZmewlYd26dUl3EkIIcQ4edAghhMTNT3/6Uxx++OE46qijsGXLFpx77rmj3n769Omoq6vDuHHjhvzZFVdcgd///vd44okncOONNw777/t8Pvzwhz/E+eefj6amJmzdujUp6xhIcXFx0p2EEEKcgy9dI4QQEjcLFy7E0UcfjZSUFEu3T01NHfaQAwBLliwBAHzyyScj/vv/9V//hXHjxuFnP/tZ/MMCePPNN2Pvp6mpqcF3v/tdHH300Zg2bRree+89ACO/R6ejowO33norjjvuOOTl5eGkk07Cr3/9a/T39494fz09PfjVr36Fr3/96zj88MNRWFiIU045BU899dSw/96rr76KJUuW4Nhjj0Vubi5mzpyJM888Ew8++KCt9RJCCOEzOoQQQhzmtddeAwAcf/zxw/75U089hT/+8Y9YvXo1pk2bltB91dbWYtGiRZg5cyaWLl2Kjo4OTJo0acTbh0IhLFmyBO+++y6OO+44lJeXo729HT/72c9GfFapo6MDZWVleOedd3DCCSfgoosuAgD85S9/wQ033IC3334bjz76aOz2v/3tb3HdddchNzcXZ555JnJychAIBLB79248/fTTIz7LRQghZHR40CGEEKKMvr4+rFy5EsCB9+1s27YNu3btwsKFC4e9mMHHH3+MyspKfPvb38aZZ56Z8P3//e9/x4033ojbb7/d0u1//etf491338U3v/lNPPfccxgz5sALIa6//nqccsopw/47t956K9555x3cdddduO6662JfD4VCuOSSS/DCCy9g8eLFOPvsswEATz/9NMaPH48tW7YgNzd3kCsQCNhYJSGEEIAvXSOEEKKQvr4+rFixAitWrMBvfvMb7Nq1C9/61rfw/PPPY8KECUNue+WVVyIrKws//elPk3L/ubm5+NGPfmT59s8//zxSUlJw9913xw45AHDEEUfgyiuvHHL71tZWvPDCCzjhhBMGHXIAIC0tDXfccQcAYPXq1YP+bOzYscO+tC8rK8vyrIQQQgbDZ3QIIYQoY8KECQgGg+jv70dTUxM2b96Me+65B6eeeirWrFmDI488MnbbBx54AO+++y6qq6sxderUpNz/8ccfj7S0NEu37ejowKeffor8/HyUlJQM+fOTTz55yNfeeecd9PX1YcyYMbjvvvuG/HlfXx8ADLo0dnl5OSorKzFv3jycd955mD9/PubNm4f8/HyryyKEEDIMPOgQQghRTkpKCgoKCnDhhReipKQEp59+Om688Ua8+OKLAIB//OMfeOihh/D9738fX//615N2vwe/NGw02tvbAQA5OTmWXZ999hkAYOfOndi5c+eI7oGXx77mmmuQk5ODp556Ck888QQef/xxAMBJJ52EO+64AwsWLLA8MyGEkP+DL10jhBDiKCeeeCIyMjKwdevW2BXJ3n//ffT19WHVqlWxDwiN/hO9CMDcuXORmZmJDRs2WL4vq1eJAxB7Fsnv9w/7583NzSP+O1dccQWCweCI/0Sv9BZl6dKl2LhxI2pra7F27VpUVFTgH//4B8rLy/Hxxx9bnpkQQsj/wWd0CCGEOEpHRwc6OjowceLE2EHkmGOOwSWXXDLs7f/4xz/C5/NhyZIlmDp1Ko444giRuaZMmYKjjjoKtbW1+Pjjj3HMMccM+vPh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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 864x720 with 1 Axes>"
|
||||
]
|
||||
@@ -332,7 +332,7 @@
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 864x720 with 1 Axes>"
|
||||
]
|
||||
@@ -342,18 +342,18 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"points = [2, 6, 7]\n",
|
||||
"points = range(2, 8)\n",
|
||||
"show(pct2, mph2, xlabel='Grade (percent)') \n",
|
||||
"plt.plot(points, points, 'gD-', lw=2);"
|
||||
"plt.plot(points, points, 'gD:', lw=2);"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"My guess is confirmed: the green line is where grade = speed, the red and blue lines are best-fit polynomials, and if I interpret \"regularly\" to mean \"about half the time,\" then at 6% grade the best-fit line and 4/8 rides are above the diamond at (6,6), but not so at 7%. \n",
|
||||
"The green dotted line is where grade = speed, the red and blue lines are best-fit polynomials, and if I interpret \"regularly\" to mean \"around half the time,\" then both best-fit lines are above the diamond at 6%, but both are below the diamond for 7%, so my guess is confirmed: my hill-index is 6.\n",
|
||||
"\n",
|
||||
"Note that the original publication h-index is based on cumulative totals, so it can never decrease, but the Hill-index is based on an average so it can (and sadly, often does) decrease as you grow older."
|
||||
"Note that the original h-index for publications is based on cumulative totals, so it can never decrease, but the Hill-index is based on an average so it can (and sadly, often does) decrease as you grow older."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -364,7 +364,7 @@
|
||||
"\n",
|
||||
"# Eddington Number\n",
|
||||
"\n",
|
||||
"The British physicist Sir Arthur Eddington (1882-1944) was a pre-Strava bicyclist who favored this metric:\n",
|
||||
"Speaking of h-numbers, the physicist Sir Arthur Eddington (1882-1944) was a pre-Strava bicyclist who favored this metric:\n",
|
||||
"\n",
|
||||
"> *Your [Eddington Number](https://www.triathlete.com/2011/04/training/measuring-bike-miles-eddington-number_301789) is the maximum integer** e** such that you have bicycled at least** e** miles on at least** e** days.*\n",
|
||||
"\n",
|
||||
@@ -379,7 +379,7 @@
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"54"
|
||||
"56"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
@@ -388,10 +388,11 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def Eddington(distances):\n",
|
||||
" \"Eddington number: max integer e such that there are e or more distances >= e.\"\n",
|
||||
" def at_least(e): return sum(d >= e for d in distances) >= e\n",
|
||||
" return max(filter(at_least, range(len(distances) + 1)))\n",
|
||||
"def Eddington(distances) -> int:\n",
|
||||
" \"Eddington number: the maximum integer e such that you have bicycled at least e on at least e days.\"\n",
|
||||
" return max(e for e in range(300) if days_at_least(e, distances) >= e)\n",
|
||||
"\n",
|
||||
"def days_at_least(e, distances) -> int: return sum(d >= e for d in distances)\n",
|
||||
"\n",
|
||||
"Eddington(mls)"
|
||||
]
|
||||
@@ -400,9 +401,9 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"My **Eddington Number is 54**: I've done at least 54 miles on at least 54 days. This is a bit above [the mean for Strava users](https://swinny.net/Cycling/-4687-Calculate-your-Eddington-Number). Eddington was about my age when he died with a number of **77**, and his available roads, bicycles, and navigation aids were not nearly as nice as mine, so good for him.\n",
|
||||
"My **Eddington Number is 56**: I've done at least 56 miles on at least 56 days. This is a bit above [the mean for Strava users](https://swinny.net/Cycling/-4687-Calculate-your-Eddington-Number). Eddington was 62 years old when he died with a number of **77**, and his available roads, bicycles, and navigation aids were not nearly as nice as mine, so good for him.\n",
|
||||
"\n",
|
||||
"If I wanted a bigger number, I could use kilometers:"
|
||||
"How many more rides would I need to improve my number? Obviously, for anyone to improve from, say, 56 to 60 they need at least 4 more rides. But if some of their past rides were, say, 58 or 59 miles, then those counted for an Eddington number of 55 but they wouldn't count for 60; thus more than 4 rides would be needed. Here is a table of the number of rides I would need—what I'll call the *gap*—for increasingly ambitious Eddington numbers:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -411,28 +412,48 @@
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"77"
|
||||
]
|
||||
},
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Dist Days Gap\n",
|
||||
"---- ---- ---\n",
|
||||
" 56 57 -1\n",
|
||||
" 57 54 3\n",
|
||||
" 58 51 7\n",
|
||||
" 59 46 13\n",
|
||||
" 60 45 15\n",
|
||||
" 61 39 22\n",
|
||||
" 62 37 25\n",
|
||||
" 63 35 28\n",
|
||||
" 64 33 31\n",
|
||||
" 65 30 35\n",
|
||||
" 66 28 38\n",
|
||||
" 67 24 43\n",
|
||||
" 68 22 46\n",
|
||||
" 69 17 52\n",
|
||||
" 70 16 54\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"kms = [m * 5280 * 12 * 2.54 / 100000 for m in mls]\n",
|
||||
"Eddington(kms)"
|
||||
"def Egaps(distances, rows):\n",
|
||||
" \"\"\"Print the number of days doing various distances, and the Eddington gap.\"\"\"\n",
|
||||
" print('Dist Days Gap')\n",
|
||||
" print('---- ---- ---')\n",
|
||||
" for dist in rows:\n",
|
||||
" days = days_at_least(dist, distances)\n",
|
||||
" print(f'{dist:4} {days:4} {dist - days:3}')\n",
|
||||
" \n",
|
||||
"Egaps(mls, range(56, 71))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"My **metric Eddington Number is 77**.\n",
|
||||
"I'd like to get my Eddington number to meet or beat my age. If I really concentrate on long rides and do one 70+ mile ride every 2.5 weeks in 2020 and 2021, I can just barely get to Eddington number 65 by age 65; after that I'll have to do no more than six 70+ mile rides each year up until age 70. That sounds tough, but doable.\n",
|
||||
"\n",
|
||||
"How many more rides would I need to improve my numbers? Obviously, for anyone to improve from 54 to 55 they need at least 1 more ride, but if some of their previous rides are between 54 and 55 miles, they won't count towards an Eddington number of 55, so more than one ride would be needed. Here is a table of the number of rides I would need to improve—what I'll call the *gap*—for increasingly ambitious numbers (measured in both miles and kilometers):"
|
||||
"An easier way to get a bigger Eddington number is to use kilometers rather than miles:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -441,44 +462,19 @@
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Mls Days Gap\n",
|
||||
"--- ---- ---\n",
|
||||
" 54 54 0\n",
|
||||
" 55 52 3\n",
|
||||
" 56 51 5\n",
|
||||
" 57 48 9\n",
|
||||
" 58 45 13\n",
|
||||
" 59 40 19\n",
|
||||
" 60 39 21\n",
|
||||
" 61 33 28\n",
|
||||
" 62 30 32\n",
|
||||
" 63 28 35\n",
|
||||
" 64 26 38\n",
|
||||
" 65 24 41\n",
|
||||
" 66 23 43\n"
|
||||
]
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"80"
|
||||
]
|
||||
},
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def Egaps(distances, rows, unit='Mls'):\n",
|
||||
" \"\"\"Print the number of days doing various distances, and the Eddington gap.\"\"\"\n",
|
||||
" print(f'{unit} Days Gap')\n",
|
||||
" print(f'--- ---- ---')\n",
|
||||
" for dist in rows:\n",
|
||||
" days = sum(d >= dist for d in distances)\n",
|
||||
" print(f'{dist:3} {days:3} {dist - days:2}')\n",
|
||||
" \n",
|
||||
"Egaps(mls, range(54, 67))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"OK, if I do one 65 mile ride every 3 weeks, I could have my Eddington number match my age before I turn 65."
|
||||
"kms = [m * 5280 * 12 * 2.54 / 100 / 1000 for m in mls]\n",
|
||||
"Eddington(kms)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -490,27 +486,41 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Kms Days Gap\n",
|
||||
"--- ---- ---\n",
|
||||
" 77 79 -2\n",
|
||||
" 78 77 1\n",
|
||||
" 79 76 3\n",
|
||||
" 80 75 5\n",
|
||||
" 81 72 9\n",
|
||||
" 82 64 18\n",
|
||||
" 83 63 20\n",
|
||||
" 84 58 26\n",
|
||||
" 85 57 28\n",
|
||||
" 86 56 30\n",
|
||||
" 87 54 33\n",
|
||||
" 88 53 35\n",
|
||||
" 89 52 37\n",
|
||||
" 90 51 39\n"
|
||||
"Dist Days Gap\n",
|
||||
"---- ---- ---\n",
|
||||
" 80 81 -1\n",
|
||||
" 81 78 3\n",
|
||||
" 82 70 12\n",
|
||||
" 83 69 14\n",
|
||||
" 84 64 20\n",
|
||||
" 85 63 22\n",
|
||||
" 86 62 24\n",
|
||||
" 87 60 27\n",
|
||||
" 88 59 29\n",
|
||||
" 89 58 31\n",
|
||||
" 90 57 33\n",
|
||||
" 91 55 36\n",
|
||||
" 92 54 38\n",
|
||||
" 93 51 42\n",
|
||||
" 94 49 45\n",
|
||||
" 95 46 49\n",
|
||||
" 96 45 51\n",
|
||||
" 97 42 55\n",
|
||||
" 98 39 59\n",
|
||||
" 99 37 62\n",
|
||||
" 100 37 63\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"Egaps(kms, range(77, 91), 'Kms')"
|
||||
"Egaps(kms, range(80, 101))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"My **metric Eddington Number is 80**, and I should be able to get to 90 in two years, and 100 in four years, assuming one 100+ km ride every three weeks."
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div style=\"text-align: right\">Peter Norvig<br>January 2020</div>\n",
|
||||
"\n",
|
||||
"# Boggle\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"The word game **Boggle** ([rules here](https://howdoyouplayit.com/boggle-rules-play-boggle/)) is a fun game to play with friends, but not so fun to play against a computer. It is all too simple for a computer to find *all* the valid words in a Boggle board (such as the one shown above) in less than a second. We'll see how to do that as a straightforward programming exercise. Then we'll consider a more challenging problem:\n",
|
||||
"\n",
|
||||
"# Inverse Boggle\n",
|
||||
"\n",
|
||||
"The goal of Inverse Boggle is to find an arrangement of letters on a board that scores the most points. It is called Inverse Boggle because, instead of \"give me a board and I'll find the words\" it is \"give me the word list and I'll find a board with lots of words.\" \n",
|
||||
"\n",
|
||||
"It is not feasible to try all possible boards ($26^{5 \\times 5} \\approx 10^{35}$ possible $5 \\times 5$ boards), so we will use **hill-climbing**, a heuristic search technique that does not guarantee finding an optimal solution.\n",
|
||||
"\n",
|
||||
"# The Boggle Board\n",
|
||||
"\n",
|
||||
"I'll represent a board as a dict where the keys are `(x, y)` coordinates of squares, and the values are individual letters (except that `QU` occupies a single square). A board will also hold:\n",
|
||||
"- `board.squares`: a list of squares in row-major order.\n",
|
||||
"- `board.neighbors`: a dict of `{square: neighbors}`. \n",
|
||||
"- `board.format`: a string format method to print the board nicely using `__repr__`.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import random\n",
|
||||
"import itertools\n",
|
||||
"\n",
|
||||
"class Board(dict):\n",
|
||||
" \"\"\"A board is a dict of {(x, y): L}.\"\"\"\n",
|
||||
" def __init__(self, letters=None):\n",
|
||||
" letters = [('QU' if L == 'Q' else L) for L in letters if 'A' <= L <= 'Z']\n",
|
||||
" n = int(len(letters) ** 0.5)\n",
|
||||
" self.squares = [(x, y) for y in range(n) for x in range(n)]\n",
|
||||
" self.neighbors = {s: neighbors(s, n) for s in self.squares}\n",
|
||||
" self.format = ('\\n'.join(['{:2s}' * n] * n)).format\n",
|
||||
" self.update(zip(self.squares, letters))\n",
|
||||
" \n",
|
||||
" def __repr__(self): \n",
|
||||
" return self.format(*(self[s].capitalize() for s in self.squares))\n",
|
||||
" \n",
|
||||
"def neighbors(square, n) -> list:\n",
|
||||
" \"\"\"All the squares that neighbor a square.\"\"\"\n",
|
||||
" (x, y) = square\n",
|
||||
" return [(x+dx, y+dy) for dx in (-1, 0, 1) for dy in (-1, 0, 1)\n",
|
||||
" if 0 <= x+dx < n and 0 <= y+dy < n and not (dx == dy == 0)]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"P U Z Z L \n",
|
||||
"W O R D E \n",
|
||||
"B O G G L \n",
|
||||
"S E A R C \n",
|
||||
"F I N D H "
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"board = Board('PUZZL WORDE BOGGL SEARC FINDH')\n",
|
||||
"board"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# The word list and scoring\n",
|
||||
"\n",
|
||||
"We'll read in a word list, convert to uppercase, and keep all the words that are three letters or more. The variable `WORDS` holds the set of valid words. The function `total_score` computes the total score of a set of words. It does not take a board as an argument, and so it does not check if the words can actually be made on the board. It does check that the words are in the `WORDS` list of valid words."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" 172820 enable1.txt\r\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"! [ -e enable1.txt ] || curl -O http://norvig.com/ngrams/enable1.txt\n",
|
||||
"! wc -w enable1.txt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"172724"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def total_score(found, scores=[0, 0, 0, 1, 1, 2, 3, 5] + [11] * 99) -> int:\n",
|
||||
" \"\"\"The total score for the words found, according to the rules.\"\"\"\n",
|
||||
" return sum([scores[len(w)] for w in found & WORDS])\n",
|
||||
"\n",
|
||||
"WORDS = {w for w in open('enable1.txt').read().upper().split() if len(w) >= 3}\n",
|
||||
"len(WORDS)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Finding all the words in a board\n",
|
||||
"\n",
|
||||
"The strategy for finding words:\n",
|
||||
"- A word could start at any of the squares on the board, so consider each one.\n",
|
||||
"- At each square, form a **path** consisting of just that square (for example, the top left square forms the path `[(0, 0)]`) and a **prefix string** consisting of the letters visited in the path; for this one-square path on `board` (the one with `PUZZLE` in it), the prefix string would be `'P'`. On a 5 by 5 board we would have 25 initial paths of length one.\n",
|
||||
"- Now continue each path:\n",
|
||||
" - If the prefix string is a word, record it: add it to the set `found`.\n",
|
||||
" - If the prefix string is a prefix of some word in the dictionary, continue the path by visiting each neighboring square that has not already been visited in the path. For example, in `board`, the path `[(0, 0)]` with prefix `'P'` would be continued to three neighbors:\n",
|
||||
" - `[(0, 0), (1, 0)]`, `'PU'`: here `'PU'` is a prefix of a word so continue this path.\n",
|
||||
" - `[(0, 0), (0, 1)]`, `'PW'`: here `'PW'` is not a prefix of any word so do **not** continue this path.\n",
|
||||
" - `[(0, 0), (1, 1)]`, `'PO'`: here `'PO'` is a prefix of a word so continue this path.\n",
|
||||
" - One continuation of `'PO'` is as follows:\n",
|
||||
" - `[(0, 0), (1, 1), (0, 1)]`, `'POW'`: here `'POW'` is both a word and a prefix, so record it and continue.\n",
|
||||
"- We can precompute the set of all `PREFIXES` of all words in the word list."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def word_prefixes(words) -> set:\n",
|
||||
" \"The set of all non-empty, non-full-word prefixes of each word in a word list.\"\n",
|
||||
" return {word[:i] for word in words for i in range(1, len(word))}\n",
|
||||
"\n",
|
||||
"PREFIXES = word_prefixes(WORDS) # Precompute this once.\n",
|
||||
"\n",
|
||||
"def find_words(board, words=WORDS, prefixes=PREFIXES):\n",
|
||||
" \"\"\"Find all words in a Boggle board by recursively continuing paths that are prefixes of words.\"\"\"\n",
|
||||
" found = set() # Accumulate the words found in the set `found`\n",
|
||||
" def continue_path(path, prefix):\n",
|
||||
" if prefix in words:\n",
|
||||
" found.add(prefix)\n",
|
||||
" if prefix in prefixes:\n",
|
||||
" for s in board.neighbors[path[-1]]:\n",
|
||||
" if s not in path:\n",
|
||||
" continue_path(path + [s], prefix + board[s])\n",
|
||||
" # Body of find_words: Start paths from each of the squares on the board\n",
|
||||
" for s in board.squares:\n",
|
||||
" continue_path([s], board[s])\n",
|
||||
" return found"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"How many words can we find? And how long does it take?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"CPU times: user 4.39 ms, sys: 43 µs, total: 4.43 ms\n",
|
||||
"Wall time: 4.5 ms\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"252"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%time len(find_words(board))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Can we see the words and summarize them? "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"252 words found:\n",
|
||||
"AGE AGED AGES AGGRO AGGROS AGO AIN AIS AND ANE ANES ANI ANIS ANISE ARC ARCH ARGLE ARGLED\n",
|
||||
"BEAD BEAGLE BEAN BEAR BEARD BEG BEGAN BEGGAR BEGGED BEGROAN BEN BEND BOA BOAR BOARD BOG\n",
|
||||
"BOGAN BOGGED BOGGLE BOGGLED BOO BOOR BOOS BOP BORDEL BOS BOURG BOW CRAG CRAGGED CRANE\n",
|
||||
"CRANES DAG DAGGLE DAGGLED DAGO DAGOES DAGOS DAIS DARN DEGAGE DEL DRAG DRAGGED DRAGGLE\n",
|
||||
"DRAGGLED DRAIN DROOP DROP EAGLE EAR EARL EARN EDGE EDGES EFS EGAD EGG EGGAR EGGED EGO EGOS\n",
|
||||
"ELD END ENDARCH ENRAGE ENRAGED EOSIN FEAR FEN FENAGLE FENAGLED FEND FIAR FIE FIEND FIN\n",
|
||||
"FINAGLE FINAGLED FIND FINE FINES GAD GAE GAEN GAES GAG GAGE GAGED GAGES GAIN GAN GANE\n",
|
||||
"GANEF GANEFS GAR GARGLE GARGLED GARNI GEAR GED GEL GELD GEN GLED GOA GOAD GOB GOBO GOBOES\n",
|
||||
"GOBOS GOBS GOES GOO GOOP GOOS GOOSE GOR GORGE GORGED GOURD GOURDE GRAD GRAIN GRAN GRAND\n",
|
||||
"GROAN GROG GROUP GROW IFS INARCH LED LEDGE LEDGES LEG LEZ NAE NAG NAGGED NAIF NAIFS NAOS\n",
|
||||
"NARC NARD NEAR NEB NEBS NEIF NEIFS OAR OBE OBES OBOE OBOES OES ORGAN ORGANISE ORZO OSE OUR\n",
|
||||
"POOR POROSE POUR POW PUR PURGE PURGED PURGES PUZZLE PUZZLED RAD RAG RAGE RAGED RAGES\n",
|
||||
"RAGGED RAGGLE RAIN RAISE RAN RAND RANI RANIS ROAD ROAN ROAR ROB ROBE ROBES ROBS ROE ROES\n",
|
||||
"ROOSE ROSE ROSIN ROUP ROW SEA SEAR SEARCH SEG SEGGAR SEGO SEI SEIF SEN SEND SIN SINE SOAR\n",
|
||||
"SOB SOGGED SORD SORGO SOW UPO URD URGE URGED URGES WOAD WOE WOES WOG WOO WOOS WOP WORD WOS\n",
|
||||
"WURZEL ZED ZOO ZOOS\n",
|
||||
"Score of 449 for 252 words on board:\n",
|
||||
"P U Z Z L \n",
|
||||
"W O R D E \n",
|
||||
"B O G G L \n",
|
||||
"S E A R C \n",
|
||||
"F I N D H \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import textwrap\n",
|
||||
"\n",
|
||||
"def report(board, verbose=True):\n",
|
||||
" found = find_words(board, WORDS, PREFIXES)\n",
|
||||
" if verbose:\n",
|
||||
" print(f'{len(found)} words found:') \n",
|
||||
" print(textwrap.fill(' '.join(sorted(found)), width=90))\n",
|
||||
" print(f'Score of {total_score(found)} for {len(found)} words on board:\\n{board}')\n",
|
||||
" \n",
|
||||
"report(board)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's test that the `Q` works, and that a 4x4 board works:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"125 words found:\n",
|
||||
"ANE ANES ANEW ANSWER ANSWERS AWE AWES AWN AWNS AWRY ENOSIS ENS EON EONS ERS ESES ESS ESSES\n",
|
||||
"ION IONS ITS NAE NAW NEIST NENE NEON NEONS NESS NESSES NEST NESTS NEW NEWNESS NEWS NOES\n",
|
||||
"NOESIS NOISE NOISES NOS NOSE NOSES NOSIER OES ONE ONENESS ONERY ONES ONS OSE OSES OSIER\n",
|
||||
"OSIERS QUA QUEAN QUEANS QUEST QUESTION QUESTIONER QUESTIONERS QUESTIONS QUESTS REI REIS\n",
|
||||
"RENEST RENESTS RES RESIST REST RESTS REWAN REWAX SEA SEI SEIS SEISE SEISES SEN SENE SENSE\n",
|
||||
"SENSES SER SERS SESSION SEW SEWAN SEWANS SEWN SEWS SIS SISES SIT SITS SNAW SNAWS SON SONE\n",
|
||||
"SONES SONS SOS STIES SWAN SWANS TIE TIER TIERS TIES TIS WAE WAENESS WAES WAN WANE WANES\n",
|
||||
"WANS WAX WAXY WEN WENS WEST WESTS WREN WRENS WREST WRESTS WRY\n",
|
||||
"Score of 245 for 125 words on board:\n",
|
||||
"QuE S T \n",
|
||||
"A N S I \n",
|
||||
"X W E O \n",
|
||||
"Y R S N \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"report(Board('QEST ANSI XWEO YRSN'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Inverse Boggle\n",
|
||||
"\n",
|
||||
"I'll tackle the Inverse Boggle problem with a **hill-climbing** approach:\n",
|
||||
"- Start with some board.\n",
|
||||
"- Make a random change to some letter on the board.\n",
|
||||
"- Evaluate the score of the new board; if it is the best score so far, keep it.\n",
|
||||
"- If not, revert the change.\n",
|
||||
"- Repeat for a set number of iterations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def boggle_hill_climbing(board, words=WORDS, prefixes=PREFIXES, repeat=500):\n",
|
||||
" \"\"\"Solve Inverse Boggle by hill-climbing: find a high-scoring board by\n",
|
||||
" starting with one and changing it.\"\"\"\n",
|
||||
" board = Board(board[s] for s in board.squares) # Copy board, so we don't mutate original\n",
|
||||
" best_score = total_score(find_words(board, words, prefixes))\n",
|
||||
" for _ in range(repeat):\n",
|
||||
" s, old_letter = mutate_boggle(board)\n",
|
||||
" new_score = total_score(find_words(board, words, prefixes))\n",
|
||||
" if new_score >= best_score:\n",
|
||||
" best_score = new_score\n",
|
||||
" else:\n",
|
||||
" board[s] = old_letter # Change back\n",
|
||||
" return board\n",
|
||||
"\n",
|
||||
"# The distribution of letters in the 16-cube version of the game\n",
|
||||
"LETTERS = 3 * 'AABCDEEEGHIILMNOOPRSTUY' + 'AADEFFIJKKLLNNQRSSTTUVVWWXZ'\n",
|
||||
"\n",
|
||||
"def mutate_boggle(board, letters=LETTERS):\n",
|
||||
" \"\"\"Make a random change to one letter in the board\"\"\"\n",
|
||||
" s = random.choice(board.squares)\n",
|
||||
" old_letter = board[s]\n",
|
||||
" board[s] = random.choice(letters)\n",
|
||||
" return s, old_letter\n",
|
||||
"\n",
|
||||
"def random_board(n=5, letters=LETTERS) -> Board:\n",
|
||||
" \"\"\"Return a random Boggle board.\"\"\"\n",
|
||||
" return Board(random.sample(letters, n * n))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's generate a random board and see if we can improve it:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"177 words found:\n",
|
||||
"ABIOTIC ABO ALE ALOOF ALT ALTO ATT ATTIC AUK BABE BABOO BABOOL BABU BAKE BAL BALE BALK BAT\n",
|
||||
"BATT BAUBEE BEE BEEP BIO BIOTIC BIPOD BOA BOAT BOD BOLA BOLE BOLT BOO BOOT BOP BOT BOTA\n",
|
||||
"BOTT BOTTLE BOY BUB BUBAL BUBALE BUBO BUOY CITOLA CITOLE DIOL DIT DITA DITTO DOPY DOT\n",
|
||||
"DOTTLE EAT EAU ELK FIB FOB FOOL FOOT FOOTBOY FOOTLE FOP HEP HIP HOB HOBO HOE HOOD HOOF\n",
|
||||
"HOOP HOOT HOP HOPE HOY HYP HYPO IODIC IOTA KAB KAE KALE KAT KEA KELOID KLOOF KUE LAB LAKE\n",
|
||||
"LAT LATI LEA LEAK LEK LEKU LOB LOBO LOO LOOF LOOP LOOPY LOOT LOT LOTA LOTI LOTIC LOTTO\n",
|
||||
"LOUP LOUPE OAK OAT OBI OBOE OBOL OBOLE ODIC OFT OLE OLEA OOH OOT OOTID OPE OTIC OTTO OUPH\n",
|
||||
"OUPHE PEE PEH PHI PIBAL POD POH POI POOD POOF POOH POOL POOP POT POTBOY POTTLE POTTO PUB\n",
|
||||
"PUKE TAB TABOO TABU TAE TAEL TAKE TALE TALK TAO TAU TAUPE TIC TIT TITLE TOD TOIT TOLA TOLE\n",
|
||||
"TOO TOOL TOOT TOOTLE TOP TOPI TOT TOTAL TOUPEE UKE UPO YIP YOB YOU\n",
|
||||
"Score of 234 for 177 words on board:\n",
|
||||
"C I T L E \n",
|
||||
"D T O A K \n",
|
||||
"F O B U B \n",
|
||||
"P I O P E \n",
|
||||
"I Y H E U \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"board2 = random_board(5)\n",
|
||||
"report(board2)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Score of 5782 for 1577 words on board:\n",
|
||||
"V I S E N \n",
|
||||
"D E T T S \n",
|
||||
"R A L A R \n",
|
||||
"S I M E C \n",
|
||||
"E D N S H \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"report(boggle_hill_climbing(board2), False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Impressive! We got roughly a ten-fold improvement in score after 500 repetitions.\n",
|
||||
"We can do the same with our original `board`:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"252 words found:\n",
|
||||
"AGE AGED AGES AGGRO AGGROS AGO AIN AIS AND ANE ANES ANI ANIS ANISE ARC ARCH ARGLE ARGLED\n",
|
||||
"BEAD BEAGLE BEAN BEAR BEARD BEG BEGAN BEGGAR BEGGED BEGROAN BEN BEND BOA BOAR BOARD BOG\n",
|
||||
"BOGAN BOGGED BOGGLE BOGGLED BOO BOOR BOOS BOP BORDEL BOS BOURG BOW CRAG CRAGGED CRANE\n",
|
||||
"CRANES DAG DAGGLE DAGGLED DAGO DAGOES DAGOS DAIS DARN DEGAGE DEL DRAG DRAGGED DRAGGLE\n",
|
||||
"DRAGGLED DRAIN DROOP DROP EAGLE EAR EARL EARN EDGE EDGES EFS EGAD EGG EGGAR EGGED EGO EGOS\n",
|
||||
"ELD END ENDARCH ENRAGE ENRAGED EOSIN FEAR FEN FENAGLE FENAGLED FEND FIAR FIE FIEND FIN\n",
|
||||
"FINAGLE FINAGLED FIND FINE FINES GAD GAE GAEN GAES GAG GAGE GAGED GAGES GAIN GAN GANE\n",
|
||||
"GANEF GANEFS GAR GARGLE GARGLED GARNI GEAR GED GEL GELD GEN GLED GOA GOAD GOB GOBO GOBOES\n",
|
||||
"GOBOS GOBS GOES GOO GOOP GOOS GOOSE GOR GORGE GORGED GOURD GOURDE GRAD GRAIN GRAN GRAND\n",
|
||||
"GROAN GROG GROUP GROW IFS INARCH LED LEDGE LEDGES LEG LEZ NAE NAG NAGGED NAIF NAIFS NAOS\n",
|
||||
"NARC NARD NEAR NEB NEBS NEIF NEIFS OAR OBE OBES OBOE OBOES OES ORGAN ORGANISE ORZO OSE OUR\n",
|
||||
"POOR POROSE POUR POW PUR PURGE PURGED PURGES PUZZLE PUZZLED RAD RAG RAGE RAGED RAGES\n",
|
||||
"RAGGED RAGGLE RAIN RAISE RAN RAND RANI RANIS ROAD ROAN ROAR ROB ROBE ROBES ROBS ROE ROES\n",
|
||||
"ROOSE ROSE ROSIN ROUP ROW SEA SEAR SEARCH SEG SEGGAR SEGO SEI SEIF SEN SEND SIN SINE SOAR\n",
|
||||
"SOB SOGGED SORD SORGO SOW UPO URD URGE URGED URGES WOAD WOE WOES WOG WOO WOOS WOP WORD WOS\n",
|
||||
"WURZEL ZED ZOO ZOOS\n",
|
||||
"Score of 449 for 252 words on board:\n",
|
||||
"P U Z Z L \n",
|
||||
"W O R D E \n",
|
||||
"B O G G L \n",
|
||||
"S E A R C \n",
|
||||
"F I N D H \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"report(board)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"metadata": {
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Score of 5690 for 1511 words on board:\n",
|
||||
"L P C A P \n",
|
||||
"N A R O M \n",
|
||||
"D E T I L \n",
|
||||
"S E S E N \n",
|
||||
"R T N R G \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"report(boggle_hill_climbing(board), False) "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Again, roughly a ten-fold improvement. \n",
|
||||
"\n",
|
||||
"Now, let's start from a very high-scoring board, identified by Justin Boyan in [his Ph.D. thesis](https://www.ri.cmu.edu/publications/learning-evaluation-functions-for-global-optimization/), and see if we can improve it:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Score of 10112 for 2290 words on board:\n",
|
||||
"R S T C S \n",
|
||||
"D E I A E \n",
|
||||
"G N L R P \n",
|
||||
"E A T E S \n",
|
||||
"M S S I D \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"boyan = Board('RSTCS DEIAE GNLRP EATES MSSID')\n",
|
||||
"report(boyan, False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Score of 10112 for 2290 words on board:\n",
|
||||
"R S T C S \n",
|
||||
"D E I A E \n",
|
||||
"G N L R P \n",
|
||||
"E A T E S \n",
|
||||
"M S S I D \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"report(boggle_hill_climbing(boyan), False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Sadly, we were not able to make an improvement in 500 repetitions. But that certainly is no guarantee that `boyan` is the best possible board. Here are some things to try to find a better board; maybe you can implement some of them, or try some ideas of your own:\n",
|
||||
"\n",
|
||||
"- **Genetic algorithms**: We used **mutation** of a single board, but we could also consider **crossover** where we keep a pool of boards and take the first half of one board and combine it with the second half of another.\n",
|
||||
"- **Swaps**: We changed one letter at a time. But maybe there is no change of one letter that will improve a board, but there is a change involving two squares, either swapping them or mutating both of them.\n",
|
||||
"- **Incremental score calculation**: We modified just one square and then tried to find all the words from scratch. Would it be faster to keep track of which squares contributed to which words, and only re-do the calculations for the one changed square? This would probably involve infixes of words rather than prefixes. Perhaps by keeping track of what each square contributes, we can make a better choice of which square to mutate.\n",
|
||||
"- **Random restarts**: When is it best to continue searching from the current board, versus starting over from a new board?"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"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.7.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
+348
-321
File diff suppressed because it is too large.
Load diff
+175
-64
@@ -4,25 +4,25 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div align=\"right\"><i>Peter Norvig<br>April 2015<br>Python 3: Feb 2019</i></div>\n",
|
||||
"<div align=\"right\" style=\"text-align: right\"><i>Peter Norvig<br>April 2015<br>Python 3: Feb 2019<br>Steve's bus: Apr 2020<br>Mad Cheryl: May 2020</i></div>\n",
|
||||
"\n",
|
||||
"# When is Cheryl's Birthday?\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"[This puzzle](https://www.google.com/webhp?#q=cheryl%27s%20birthday) has been making the rounds:\n",
|
||||
"**[This puzzle](https://www.google.com/webhp?#q=cheryl%27s%20birthday)** has been making the rounds:\n",
|
||||
"\n",
|
||||
"> 1. Albert and Bernard became friends with Cheryl, and want to know when her birthday is. Cheryl gave them a list of 10 possible dates:\n",
|
||||
" May 15 May 16 May 19\n",
|
||||
" June 17 June 18\n",
|
||||
" July 14 July 16\n",
|
||||
" August 14 August 15 August 17\n",
|
||||
"> 2. Cheryl then tells Albert and Bernard separately the month and the day of the birthday respectively.\n",
|
||||
"> 2. **Cheryl** then privately tells Albert the month and Bernard the day of her birthday.\n",
|
||||
"> 3. **Albert**: \"I don't know when Cheryl's birthday is, and I know that Bernard does not know.\"\n",
|
||||
"> 4. **Bernard**: \"At first I don't know when Cheryl's birthday is, but I know now.\"\n",
|
||||
"> 5. **Albert**: \"Then I also know when Cheryl's birthday is.\"\n",
|
||||
"> 6. So when is Cheryl's birthday?\n",
|
||||
"\n",
|
||||
"Let's work through this puzzle statement by statement.\n",
|
||||
"Let's work through the puzzle line by line.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
@@ -54,9 +54,8 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def Month(date): return date.split()[0]\n",
|
||||
"\n",
|
||||
"def Day(date): return date.split()[1]"
|
||||
"def month(date): return date.split()[0]\n",
|
||||
"def day(date): return date.split()[1]"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -76,7 +75,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"Month('May 15')"
|
||||
"month('May 15')"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -96,7 +95,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"Day('May 15')"
|
||||
"day('May 15')"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -105,9 +104,19 @@
|
||||
"source": [
|
||||
"## 2. Cheryl then tells Albert and Bernard separately the month and the day of the birthday respectively.\n",
|
||||
"\n",
|
||||
"Now we have to think about what we're doing. We'll use a *set of dates* to represent a *belief set*: a person who has the belief set `{'August 15', 'May 15'}` *believes* that Cheryl's birthday is one of those two days. A person *knows* the birthdate when they get down to a belief set with only one possibility. \n",
|
||||
"Now we have to think really carefully about what we're doing. The puzzle is tricky because we're dealing with two types of uncertainty:\n",
|
||||
"\n",
|
||||
"We can define the idea of Cheryl **telling** someone a component of her birthdate, and while we're at it, the idea of **knowing** a birthdate:"
|
||||
"1. Albert and Bernard are uncertain about the birthdate. *(Cheryl knows something they don't know.)*\n",
|
||||
"2. We, the puzzle solvers don't know what Albert and Bernard were told. *(They know something we don't know.)*\n",
|
||||
"\n",
|
||||
"If Cheryl tells Albert \"May\", then he believes the birthdate could be either May 15, May 16, or May 19. We'll call `{'May 15', 'May 16', 'May 19'}` his **belief state** about the birthdate. We will say that a person **knows** the birthdate when they get down to a belief state with exactly one possibility. The type 2 uncertainty is that we don't know that Albert was told \"May\", so we have uncertainty about his belief state. But we do know some statements about his belief state, and our task is to use those statements to solve the puzzle. \n",
|
||||
"\n",
|
||||
"The way we will deal with our uncertainty as puzzle solvers is by considering each of the ten dates one at a time and reasoning as follows: \n",
|
||||
"- If this date were Cheryl's true birthdate, then we would know what Albert and Bernard were told: we would eliminate the type 2 uncertainty, and we could figure out their belief states. \n",
|
||||
"- From that we could figure out if the statements are true (given this date). \n",
|
||||
"- If the puzzle is correct and we don't make mistakes, then there will be only one date that makes all the statements true; that's Cheryl's birthday.\n",
|
||||
"\n",
|
||||
"We can define the idea of Cheryl having **told** someone a component of her birthdate, and the idea of **knowing** a birthdate as follows:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -116,14 +125,14 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"BeliefSet = set\n",
|
||||
"BeliefState = set\n",
|
||||
"\n",
|
||||
"def tell(part, dates=dates) -> BeliefSet:\n",
|
||||
" \"Cheryl tells a part of her birthdate to someone; return a set of possible dates.\"\n",
|
||||
"def told(part: str) -> BeliefState:\n",
|
||||
" \"\"\"Cheryl told a part of her birthdate to someone; return a belief state of possible dates.\"\"\"\n",
|
||||
" return {date for date in dates if part in date}\n",
|
||||
"\n",
|
||||
"def know(beliefs) -> bool:\n",
|
||||
" \"A person `knows` the answer if their belief set has only one possibility.\"\n",
|
||||
"def know(beliefs: BeliefState) -> bool:\n",
|
||||
" \"\"\"A person `knows` the answer if their belief state has only one possibility.\"\"\"\n",
|
||||
" return len(beliefs) == 1"
|
||||
]
|
||||
},
|
||||
@@ -131,7 +140,11 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"For example: If Cheryl tells Albert that her birthday is in May, he would know there is a set of three possible birthdates:"
|
||||
"(Note: one thing I dislike about my code is that `told` uses the global `dates`. Later we will see that `cheryls_birthday` does the same. For that to be an acceptable design choice, we need to think of `dates` as a constant, not a variable.)\n",
|
||||
"\n",
|
||||
"Let's see what happens as we consider the date `'May 15'`:\n",
|
||||
"\n",
|
||||
"Cheryl tells Albert `'May'` and Bernard `'15'`, giving them these belief states: "
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -151,14 +164,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"tell('May')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"And if she tells Bernard that her birthday is on the 15th, he would know there are two possibilities:"
|
||||
"told('May')"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -178,14 +184,14 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"tell('15')"
|
||||
"told('15')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"With two possibilities, Bernard does not know the birthdate:"
|
||||
"We can then check whether Albert and Bernard's statements are true, given this date. The first part of Albert's first statement is \"I don't know when Cheryl's birthday is.\" We can verify that that part of the statement is true (given that Albert was told \"May\"):"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -196,7 +202,7 @@
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"False"
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
@@ -205,20 +211,25 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"know(tell('15'))"
|
||||
"not know(told('May'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Overall Strategy\n",
|
||||
"If the rest of the statements worked out to be true, then `'May 15'` would be a solution to the puzzle. If not, it must be one of the other dates."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Overall Strategy\n",
|
||||
"\n",
|
||||
"If Cheryl tells Albert `'May'` then *he* knows there are three possibilities, but *we* (the puzzle solvers) don't know that, because we don't know what Cheryl said. \n",
|
||||
"Here is the main function, `cheryls_birthday`, which computes the subset of dates in the global variable `dates` that satisfy statements 3 through 5. We will define a **statement** as a boolean function that takes a single date as input and returns true if the statement would be true in the condition that the given date is Cheryl's actual birthday. So a statement only has to consider one date at a time. But the code within a statement may have to consider the belief states of Albert and Bernard, to determine if the statement is true.\n",
|
||||
"\n",
|
||||
"So what can we do? We can consider *all* of the possible dates, one at a time. For example, first consider `'May 15'`. Cheryl tells Albert `'May'` and Bernard `'15'`, giving them the lists of possible birthdates shown above. We can then check whether statements 3 through 5 are true in this scenario. If they are, then `'May 15'` is a solution to the puzzle. Repeat the process for each of the other possible dates. If all goes well, there should be exactly one date for which all the statements are true. \n",
|
||||
"\n",
|
||||
"Here is the main function, `cheryls_birthday`, which takes a set of possible dates, and returns the subset of dates that satisfy statements 3 through 5. The function `satisfy` is similar to the builtin function `filter`: `satisfy` takes a collection of items (here a set of dates) and returns the subset that satisfies all the predicates:"
|
||||
"The function `satisfy` takes a collection of dates and some statement(s) and returns the subset of dates that satisfies all the statements."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -227,16 +238,14 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def cheryls_birthday(dates=dates) -> BeliefSet:\n",
|
||||
" \"Return a subset of the dates for which all three statements are true.\"\n",
|
||||
" return satisfy(dates, statement3, statement4, statement5)\n",
|
||||
"def cheryls_birthday() -> BeliefState:\n",
|
||||
" \"\"\"Return a subset of the global `dates` for which all three statements are true.\"\"\"\n",
|
||||
" return satisfy(dates, albert1, bernard1, albert2)\n",
|
||||
"\n",
|
||||
"def satisfy(items, *predicates):\n",
|
||||
" \"Return the subset of items that satisfy all the predicates.\"\n",
|
||||
" return {item for item in items\n",
|
||||
" if all(pred(item) for pred in predicates)}\n",
|
||||
"\n",
|
||||
"## TO DO: define statement3, statement4, statement5"
|
||||
"def satisfy(some_dates, *statements) -> BeliefState:\n",
|
||||
" \"\"\"Return the subset of dates that satisfy all the statements.\"\"\"\n",
|
||||
" return {date for date in some_dates\n",
|
||||
" if all(statement(date) for statement in statements)}"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -250,9 +259,9 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The function `statement3` corresponds to the third statement in the problem. It takes as input a single possible birthdate (not a set) and returns `True` if Albert's statement is true for that birthdate. How do we go from Albert's English statement to a Python function? Let's paraphrase it in a form that uses the concepts we have defined:\n",
|
||||
"The function `albert1` takes as input a single possible birthdate and returns `True` if Albert's statement is true for that birthdate. How do we go from Albert's English statement to a Python function? Let's paraphrase it in a form that uses the concepts we have defined:\n",
|
||||
"\n",
|
||||
"> **Albert**: After Cheryl told me the month of her birthdate, I didn't know her birthday. But for *any* of the possible dates, if Bernard was told the day of that date, he would not know Cheryl's birthday.\n",
|
||||
"> **Albert**: After Cheryl **told** me the **month** of her birthdate, my **belief state** was such that I didn't **know** her birthday. And I know that Bernard does not know; in other words he could not make the statement that he knows. How do I know that? I can see that for all the possible dates in my **belief state**, if Bernard was **told** the **day** of that date, he would **not know** Cheryl's birthday.\n",
|
||||
"\n",
|
||||
"That I can translate directly into code:"
|
||||
]
|
||||
@@ -263,18 +272,19 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def statement3(date) -> bool:\n",
|
||||
" \"Albert: I don't know when Cheryl's birthday is, but I know that Bernard does not know too.\"\n",
|
||||
" dates = tell(Month(date))\n",
|
||||
" return (not know(dates) \n",
|
||||
" and all(not know(tell(Day(d))) for d in dates))"
|
||||
"def albert1(date) -> bool:\n",
|
||||
" \"Albert: I don't know when Cheryl's birthday is, and I know that Bernard does not know.\"\n",
|
||||
" albert_beliefs = told(month(date))\n",
|
||||
" return not know(albert_beliefs) and not satisfy(albert_beliefs, bernard_knows)\n",
|
||||
"\n",
|
||||
"def bernard_knows(date) -> bool: return know(told(day(date))) "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We haven't solved the puzzle yet, but let's take a peek and see which dates satisfy statement 3:"
|
||||
"We haven't solved the puzzle yet, but let's take a peek and see which dates satisfy Albert's statement:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -294,7 +304,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"satisfy(dates, statement3)"
|
||||
"satisfy(dates, albert1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -305,7 +315,7 @@
|
||||
"\n",
|
||||
"Again, a paraphrase:\n",
|
||||
"\n",
|
||||
"> **Bernard:** At first Cheryl told me the day, and I didn't know. Then, out of the possible dates, I considered just the dates for which Albert's statement 3 is true, and now I know."
|
||||
"> **Bernard:** At first Cheryl **told** me the **day**, and I didn't **know**. After I heard Albert's **statement**, I updated my **belief state**, and now I **know**."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -314,17 +324,18 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def statement4(date):\n",
|
||||
"def bernard1(date) -> bool:\n",
|
||||
" \"Bernard: At first I don't know when Cheryl's birthday is, but I know now.\"\n",
|
||||
" dates = tell(Day(date))\n",
|
||||
" return (not know(dates) and know(satisfy(dates, statement3)))"
|
||||
" at_first_beliefs = told(day(date))\n",
|
||||
" after_beliefs = satisfy(at_first_beliefs, albert1)\n",
|
||||
" return not know(at_first_beliefs) and know(after_beliefs)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's see which dates satisfy both statement 3 and statement 4:"
|
||||
"Let's see which dates satisfy both Albert and Bernard's statements:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -344,18 +355,26 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"satisfy(dates, statement3, statement4)"
|
||||
"satisfy(dates, albert1, bernard1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Wait a minute—I thought that Bernard **knew**?! Why are there three possible dates? Bernard does indeed know; it is just that we, the puzzle solvers, don't know. That's because Bernard knows something we don't know: the day. If Bernard was told `'15'` then he would know `'August 15'`; if he was told `'17'` he would know `'August 17'`, and if he was told `'16'` he would know `'July 16'`. *We* don't know because we don't know which of these is the case.\n",
|
||||
"Wait a minute—I thought that Bernard **knew**?! Why are there three possible dates? \n",
|
||||
"\n",
|
||||
"Bernard does indeed know; it is just that we, the puzzle solvers, don't know. That's because Bernard knows something we don't know: the day. If Bernard was told `'15'` then he would know `'August 15'`; if he was told `'17'` he would know `'August 17'`, and if he was told `'16'` he would know `'July 16'`. *We* don't know because we don't know which of these is the case. We'll need more information (coming in statement `albert2`) before *we* know.\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Albert: Then I also know when Cheryl's birthday is.\n",
|
||||
"\n",
|
||||
"Albert is saying that after hearing the month and Bernard's statement 4, he now knows Cheryl's birthday:"
|
||||
"Albert is saying that after hearing the month and Bernard's statement, he now knows Cheryl's birthday:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -364,9 +383,10 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def statement5(date):\n",
|
||||
" \"Albert: Then I also know when Cheryl's birthday is.\"\n",
|
||||
" return know(satisfy(tell(Month(date)), statement4))"
|
||||
"def albert2(date) -> bool:\n",
|
||||
" \"Albert: Then I also know when Cheryl's birthday is.\" \n",
|
||||
" then = satisfy(told(month(date)), bernard1)\n",
|
||||
" return know(then)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -423,6 +443,97 @@
|
||||
"source": [
|
||||
"know(cheryls_birthday())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"___\n",
|
||||
"\n",
|
||||
"# New Puzzle: Steve's Bus\n",
|
||||
"\n",
|
||||
"Here's [another puzzle](https://www.reddit.com/r/riddles/comments/fw7h42/a_riddle_i_couldnt_solve/) that seems to have a very similar format:\n",
|
||||
"\n",
|
||||
"> 1. Steve tells Alice the hour of his bus departure and he tells Annie at which minute it leaves. He also tells them both that the bus leaves between 06:00 and 10:00.\n",
|
||||
"> 2. Alice and Annie consult the timetable and find the following services between those two time: 06:32 06:43 06:50 07:17 07:46 08:19 08:32 09:17 09:19 09:50.\n",
|
||||
"> 3. Alice then says “I don’t know when Steve’s bus leaves but I am sure that neither does Annie”\n",
|
||||
"> 4. Annie Replies “I didn’t know his bus, but now I do”\n",
|
||||
"> 5. Alice responds “Now I do as well!”\n",
|
||||
"> 6. When is Steve’s bus?\n",
|
||||
"\n",
|
||||
"Upon closer inspection, not only is it a similar format, it is **exactly** the same puzzle, except that months are changed to hours and days to minutes. If we rewrite the times in the same format as `dates`, we can solve the problem without writing a single line of new code:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'08 32'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"times = '06:32 06:43 06:50 07:17 07:46 08:19 08:32 09:17 09:19 09:50'.split()\n",
|
||||
"dates = [time.replace(':', ' ') for time in times]\n",
|
||||
"\n",
|
||||
"cheryls_birthday()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Steve took the 8:32 bus."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Another New Puzzle: Evil Mad Scientist Cheryl\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Again, we can solve this problem just by changing the format of `dates`:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'C 3'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"pads = 'A2 A3 A6 B4 B5 C1 C3 D1 D2 D4'.split()\n",
|
||||
"dates = [L+' '+N for L,N in pads]\n",
|
||||
"\n",
|
||||
"cheryls_birthday()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Mad scientist Cheryl was refering to pad C3. (But may I point out that this Cheryl is not actually a mad scientist, just a [mad engineer](https://www.evilmadscientist.com/2015/evil-mad-engineers/). A true mad scientist would kill 25 people and use the other 25 as a control group.)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
@@ -441,7 +552,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.7.0"
|
||||
"version": "3.7.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"source": [
|
||||
"Pound a bunch of nails into a board, then stretch a rubber band around them and let the rubber band snap taut, like this:\n",
|
||||
"\n",
|
||||
"<img src=\"http://www.personal.kent.edu/~rmuhamma/Compgeometry/MyCG/Gifs-CompGeometry/ch2.gif\">\n",
|
||||
"<img src=\"convexhull.jpg\">\n",
|
||||
"\n",
|
||||
"The rubber band has traced out the *convex hull* of the set of nails. It turns out this is an important problem with applications in computer graphics, robot motion planning, geographical information systems, ethology, and other areas.\n",
|
||||
"More formally, we say that:\n",
|
||||
|
||||
+1066
-97
File diff suppressed because it is too large.
Load diff
+686
-164
@@ -4,47 +4,33 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div align=\"right\"><a href=\"http://norvig.com\">Peter Norvig</a><br><a href=\"https://github.com/norvig/pytudes\">pytudes</a><br>March 2019</div>\n",
|
||||
"<div align=\"right\" style=\"text-align:right\"><a href=\"http://norvig.com\">Peter Norvig</a><br>March 2019</div>\n",
|
||||
"\n",
|
||||
"# Dice Baseball\n",
|
||||
"# Baseball Simulation\n",
|
||||
"\n",
|
||||
"The [538 Riddler for March 22, 2019](https://fivethirtyeight.com/features/can-you-turn-americas-pastime-into-a-game-of-yahtzee/) asks us to simulate baseball using probabilities from a 19th century dice game called *Our National Ball Game*:\n",
|
||||
"\n",
|
||||
" 1,1: double 2,2: strike 3,3: out at 1st 4,4: fly out\n",
|
||||
" 1,2: single 2,3: strike 3,4: out at 1st 4,5: fly out\n",
|
||||
" 1,3: single 2,4: strike 3,5: out at 1st 4,6: fly out\n",
|
||||
" 1,4: single 2,5: strike 3,6: out at 1st 5,5: double play\n",
|
||||
" 1,5: base on error 2,6: foul out 5,6: triple\n",
|
||||
" 1,6: base on balls 6,6: home run\n",
|
||||
"The [538 Riddler for March 22, 2019](https://fivethirtyeight.com/features/can-you-turn-americas-pastime-into-a-game-of-yahtzee/) asks us to simulate baseball using probabilities from a 19th century dice game called *Our National Ball Game*. The Riddler description of the rules said *you can assume some standard baseball things* but left many things unspecified, so I [looked up](http://baseballgames.dreamhosters.com/BbDiceHome.htm) the original rules of *Our National Ball Game*, which are shown below and which, it turns out, contradict some of the *standard baseball things* assumed by 538. I'll go with the rules as stated below.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"The rules left some things unspecified; the following are my current choices (in an early version I made different choices that resulted in slightly more runs):\n",
|
||||
"|RULES FOR PLAYING \"OUR NATIONAL BALL GAME\"|DICE ROLL OUTCOMES|\n",
|
||||
"|-----|-----|\n",
|
||||
"|  |  |\n",
|
||||
"\n",
|
||||
"* On a* b*-base hit, runners advance* b* bases, except that a runner on second scores on a 1-base hit.\n",
|
||||
"* On an \"out at first\", all runners advance one base.\n",
|
||||
"* A double play only applies if there is a runner on first; in that case other runners advance.\n",
|
||||
"* On a fly out, a runner on third scores; other runners do not advance.\n",
|
||||
"* On an error all runners advance one base. \n",
|
||||
"* On a base on balls, only forced runners advance.\n",
|
||||
"# Design Choices\n",
|
||||
"\n",
|
||||
"I also made some choices about the implementation:\n",
|
||||
"\n",
|
||||
"- Exactly one outcome happens to each batter. We call that an *event*.\n",
|
||||
"- I'll represent events with the following one letter codes:\n",
|
||||
" - `K`, `O`, `o`, `f`, `D`: strikeout, foul out, out at first, fly out, double play\n",
|
||||
" - `1`, `2`, `3`, `4`: single, double, triple, home run\n",
|
||||
" - `E`, `B`: error, base on balls\n",
|
||||
"- Note the \"strike\" dice roll is not an event; it is only part of an event. From the probability of a \"strike\" dice roll, I compute the probability of three strikes in a row, and call that a strikeout event. Sice there are 7 dice rolls giving \"strike\", the probability of a strike is 7/36, and the probability of a strikeout is (7/36)**3.\n",
|
||||
"- Note that a die roll such as `1,1` is a 1/36 event, whereas `1,2` is a 2/36 event, because it also represents (2, 1).\n",
|
||||
"- I'll keep track of runners with a list of occupied bases; `runners = [1, 2]` means runners on first and second.\n",
|
||||
"- A runner who advances to base 4 or higher has scored a run (unless there are already 3 outs).\n",
|
||||
"- The function `inning` simulates a half inning and returns the number of runs scored.\n",
|
||||
"- I want to be able to test `inning` by feeding it specific events, and I also want to generate random innings. So I'll make the interface be that I pass in an *iterable* of events. The function `event_stream` generates an endless stream of randomly sampled events.\n",
|
||||
"- Note that it is consider good Pythonic style to automatically convert Booleans to integers, so for a runner on second (`r = 2`) when the event is a single (`e = '1'`), the expression `r + int(e) + (r == 2)` evaluates to `2 + 1 + 1` or `4`, meaning the runner on second scores.\n",
|
||||
"- I'll play 1 million innings and store the resulting scores in `innings`.\n",
|
||||
"- To simulate a game I just sample 9 elements of `innings` and sum them.\n",
|
||||
"- Exactly one thing happens to each batter. I'll call that an **event**.\n",
|
||||
"- To clarify: the dice roll `1,1` has probability 1/36, whereas `1,2` has probability 2/36, because it also represents `2,1`.\n",
|
||||
"- The \"One Strike\" dice roll is not an event; it is only *part* of an event. From the probability of a \"One Strike\" dice roll, 7/36, I compute the probability of three strikes in a row, `(7/36)**3 == 0.00735`, and call that a strikeout event. \n",
|
||||
"- I'll represent events with the following 11 one letter **event codes**:\n",
|
||||
" - `1`, `2`, `3`, `4`: one-, two-, three-, and four-base (home run) hits. Runners advance same number of bases.\n",
|
||||
" - `B`: base on balls. Runners advance only if forced.\n",
|
||||
" - `D`: double play. Batter and runner nearest home are out; others advance one base.\n",
|
||||
" - `E`: error. Batter reaches first and all runners advance one base.\n",
|
||||
" - `F`, `K`, `O`: fly out, strikeout, foul out. Batter is out, runners do not advance.\n",
|
||||
" - `S`: called \"out at first\" in rules, but actually a sacrifice. Batter is out, runners advance one base.\n",
|
||||
"\n",
|
||||
"# The Code"
|
||||
"\n",
|
||||
"# Implementation"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -53,9 +39,11 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%matplotlib inline\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import random"
|
||||
"import random\n",
|
||||
"from statistics import mean, stdev\n",
|
||||
"from collections import Counter\n",
|
||||
"from itertools import islice"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -64,85 +52,82 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def event_stream(events='2111111EEBBOOooooooofffffD334', strike=7/36):\n",
|
||||
" \"An iterator of random events. Defaults from `Our National Ball Game`.\"\n",
|
||||
" while True:\n",
|
||||
" yield 'K' if (random.random() < strike ** 3) else random.choice(events)\n",
|
||||
" \n",
|
||||
"def inning(events=event_stream(), verbose=False) -> int:\n",
|
||||
" \"Simulate a half inning based on events, and return number of runs scored.\"\n",
|
||||
" outs = runs = 0 # Inning starts with no outs and no runs,\n",
|
||||
" runners = [] # ... and with nobody on base\n",
|
||||
" for e in events:\n",
|
||||
" if verbose: print(f'{outs} outs, {runs} runs, event: {e}, runners: {runners}')\n",
|
||||
" # What happens to the batter?\n",
|
||||
" if e in 'KOofD': outs += 1 # Batter is out\n",
|
||||
" elif e in '1234EB': runners.append(0) # Batter becomes a runner\n",
|
||||
" # What happens to the runners?\n",
|
||||
" if e == 'D' and 1 in runners: # double play: runner on 1st out, others advance\n",
|
||||
" outs += 1\n",
|
||||
" runners = [r + 1 for r in runners if r != 1]\n",
|
||||
" elif e in 'oE': # out at first or error: runners advance\n",
|
||||
" runners = [r + 1 for r in runners]\n",
|
||||
" elif e == 'f' and 3 in runners and outs < 3: # fly out: runner on 3rd scores\n",
|
||||
" runners.remove(3)\n",
|
||||
" runs += 1\n",
|
||||
" elif e in '1234': # single, double, triple, homer\n",
|
||||
" runners = [r + int(e) + (r == 2) for r in runners]\n",
|
||||
" elif e == 'B': # base on balls: forced runners advance \n",
|
||||
" runners = [r + forced(runners, r) for r in runners]\n",
|
||||
" # See if inning is over, and if not, whether anyone scored\n",
|
||||
" if outs >= 3:\n",
|
||||
" return runs\n",
|
||||
" runs += sum(r >= 4 for r in runners)\n",
|
||||
" runners = [r for r in runners if r < 4]\n",
|
||||
" \n",
|
||||
"def forced(runners, r) -> bool: return all(b in runners for b in range(r))"
|
||||
"event_codes = {\n",
|
||||
" '1': 'single', '2': 'double', '3': 'triple', '4': 'home run',\n",
|
||||
" 'B': 'base on balls', 'D': 'double play', 'E': 'error',\n",
|
||||
" 'F': 'fly out', 'K': 'strikeout', 'O': 'foul out', 'S': 'out at first'}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Testing\n",
|
||||
"I'll define the function `inning` to simulate a half inning and return the number of runs scored. Design choices for `inning`:\n",
|
||||
"\n",
|
||||
"Let's peek at some random innings:"
|
||||
"- I'll keep track of runners with a set of occupied bases; `runners = {1, 3}` means runners on first and third.\n",
|
||||
"- I'll keep track of the number of `runs` and `outs` in an inning, and return the number of `runs` when there are three `outs`.\n",
|
||||
"- Each event follows four steps. If `runners = {1, 3}` and the event is `'2'` (a double), then the steps are:\n",
|
||||
" - The batter steps up to the plate. The plate is represented as base `0`, so now `runners = {0, 1, 3}`.\n",
|
||||
" - Check if the event causes runner(s) to be out, and if the inning is over. In this case, no.\n",
|
||||
" - Advance each runner according to `advance(r, e)`. In this case, `runners = {2, 3, 5}`.\n",
|
||||
" - Remove the runners who have `scored` and increment `runs` accordingly. In this case, runner `5` has scored, so we increment `runs` by 1 and end up with `runners = {2, 3}`.\n",
|
||||
"- I want `inning` to be easily **testable**: I want to say `assert 2 = inning('1KO4F')`.\n",
|
||||
"- I also want `inning` to be capable of simulating many independent random innings. So the interface is to accept an *iterable* of event codes. That could be string, or a generator, as provided by `event_stream()`.\n",
|
||||
"- I want `inning` to be **loggable**: calling `inning(events, verbose=True)` should produce printed output for each event.\n",
|
||||
"- `advance(r, e)` says that a runner advances `e` bases on an `e` base hit; one base on an error, sacrifice, or double play; and one base on a base on balls only if forced.\n",
|
||||
"- A runner on base `r` is `forced` if all the lower-numbered bases have runners.\n",
|
||||
"- `ONBG` is defined as a generator of random events with the probabilities from \"Our National Ball Game\"."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs, 0 runs, event: E, runners: []\n",
|
||||
"0 outs, 0 runs, event: 4, runners: [1]\n",
|
||||
"0 outs, 2 runs, event: E, runners: []\n",
|
||||
"0 outs, 2 runs, event: 1, runners: [1]\n",
|
||||
"0 outs, 2 runs, event: f, runners: [2, 1]\n",
|
||||
"1 outs, 2 runs, event: B, runners: [2, 1]\n",
|
||||
"1 outs, 2 runs, event: 1, runners: [3, 2, 1]\n",
|
||||
"1 outs, 4 runs, event: E, runners: [2, 1]\n",
|
||||
"1 outs, 4 runs, event: o, runners: [3, 2, 1]\n",
|
||||
"2 outs, 5 runs, event: o, runners: [3, 2]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"5"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"inning(verbose=True)"
|
||||
"def inning(events, verbose=True) -> int:\n",
|
||||
" \"\"\"Simulate a half inning based on events, and return number of runs scored.\"\"\"\n",
|
||||
" outs = runs = 0 # Inning starts with no outs and no runs,\n",
|
||||
" runners = set() # and with nobody on base\n",
|
||||
" def out(r) -> int: runners.remove(r); return 1\n",
|
||||
" def forced(r) -> bool: return all(b in runners for b in range(r))\n",
|
||||
" def advance(r, e) -> int: \n",
|
||||
" return int(e if e in '1234' else (e in 'ESD' or (e == 'B' and forced(r))))\n",
|
||||
" for e in events:\n",
|
||||
" if verbose: show(outs, runs, runners, e)\n",
|
||||
" runners.add(batter) # Batter steps up to the plate\n",
|
||||
" if e == 'D' and len(runners) > 1: # Double play: batter and lead runner out\n",
|
||||
" outs += out(batter) + out(max(runners))\n",
|
||||
" elif e in 'DSKOF': # Batter is out\n",
|
||||
" outs += out(batter) \n",
|
||||
" if outs >= 3: # If inning is over: return runs scored\n",
|
||||
" return runs \n",
|
||||
" runners = {r + advance(r, e) for r in runners} # Runners advance\n",
|
||||
" runs += len(runners & scored) # Tally runs\n",
|
||||
" runners = runners - scored # Remove runners who scored\n",
|
||||
" \n",
|
||||
"def event_stream(events, strikes=0):\n",
|
||||
" \"\"\"A generator of random baseball events.\"\"\"\n",
|
||||
" while True:\n",
|
||||
" yield 'K' if (random.random() < strikes ** 3) else random.choice(events)\n",
|
||||
"\n",
|
||||
"def show(outs, runs, runners, event):\n",
|
||||
" \"\"\"Print a representation of the current state of play.\"\"\"\n",
|
||||
" bases = ''.join(b if int(b) in runners else '-' for b in '321')\n",
|
||||
" print(f'{outs} outs {runs} runs {bases} {event} ({event_codes[event]})')\n",
|
||||
" \n",
|
||||
"ONBG = event_stream('2111111EEBBOOSSSSSSSFFFFFD334', 7/36) # Our National Ball Game\n",
|
||||
"batter = 0 # The batter is not yet at first base\n",
|
||||
"scored = {4, 5, 6, 7} # Runners in these positions have scored"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Examples and Tests\n",
|
||||
"\n",
|
||||
"Let's peek at some random innings:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -154,20 +139,16 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs, 0 runs, event: 1, runners: []\n",
|
||||
"0 outs, 0 runs, event: B, runners: [1]\n",
|
||||
"0 outs, 0 runs, event: O, runners: [2, 1]\n",
|
||||
"1 outs, 0 runs, event: 1, runners: [2, 1]\n",
|
||||
"1 outs, 1 runs, event: 3, runners: [2, 1]\n",
|
||||
"1 outs, 3 runs, event: 1, runners: [3]\n",
|
||||
"1 outs, 4 runs, event: f, runners: [1]\n",
|
||||
"2 outs, 4 runs, event: o, runners: [1]\n"
|
||||
"0 outs 0 runs --- 3 (triple)\n",
|
||||
"0 outs 0 runs 3-- F (fly out)\n",
|
||||
"1 outs 0 runs 3-- S (out at first)\n",
|
||||
"2 outs 1 runs --- F (fly out)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"4"
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
@@ -176,14 +157,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"inning(verbose=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"And we can feed in any events we want to test the code:"
|
||||
"inning(ONBG)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -195,22 +169,15 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs, 0 runs, event: 2, runners: []\n",
|
||||
"0 outs, 0 runs, event: E, runners: [2]\n",
|
||||
"0 outs, 0 runs, event: B, runners: [3, 1]\n",
|
||||
"0 outs, 0 runs, event: B, runners: [3, 2, 1]\n",
|
||||
"0 outs, 1 runs, event: 1, runners: [3, 2, 1]\n",
|
||||
"0 outs, 3 runs, event: D, runners: [2, 1]\n",
|
||||
"2 outs, 3 runs, event: B, runners: [3]\n",
|
||||
"2 outs, 3 runs, event: 1, runners: [3, 1]\n",
|
||||
"2 outs, 4 runs, event: 2, runners: [2, 1]\n",
|
||||
"2 outs, 5 runs, event: f, runners: [3, 2]\n"
|
||||
"0 outs 0 runs --- F (fly out)\n",
|
||||
"1 outs 0 runs --- S (out at first)\n",
|
||||
"2 outs 0 runs --- S (out at first)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"5"
|
||||
"0"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
@@ -219,36 +186,40 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"inning('2EBB1DB12f', verbose=True)"
|
||||
"inning(ONBG)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"That looks good.\n",
|
||||
"\n",
|
||||
"# Simulating\n",
|
||||
"\n",
|
||||
"Now, simulate a million innings, and then sample from them to simulate a million nine-inning games (for one team):"
|
||||
"Let's also test some historic innings. I'll take some of the Red Sox innings from their 2004 playoff series against the Yankees."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs 0 runs --- K (strikeout)\n",
|
||||
"1 outs 0 runs --- 2 (double)\n",
|
||||
"1 outs 0 runs -2- O (foul out)\n",
|
||||
"2 outs 0 runs -2- 1 (single)\n",
|
||||
"2 outs 0 runs 3-1 2 (double)\n",
|
||||
"2 outs 1 runs 32- 1 (single)\n",
|
||||
"2 outs 2 runs 3-1 4 (home run)\n",
|
||||
"2 outs 5 runs --- K (strikeout)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"N = 1000000\n",
|
||||
"innings = [inning() for _ in range(N)]\n",
|
||||
"games = [sum(random.sample(innings, 9)) for _ in range(N)]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's see histograms:"
|
||||
"# 7th inning in game 1: 5 runs (Homer by Varitek)\n",
|
||||
"# (But not a perfect reproduction, because our simulation doesn't have passed balls.)\n",
|
||||
"assert 5 == inning('K2O1214K')"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -257,35 +228,90 @@
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs 0 runs --- S (out at first)\n",
|
||||
"1 outs 0 runs --- S (out at first)\n",
|
||||
"2 outs 0 runs --- 2 (double)\n",
|
||||
"2 outs 0 runs -2- 1 (single)\n",
|
||||
"2 outs 0 runs 3-1 1 (single)\n",
|
||||
"2 outs 1 runs -21 4 (home run)\n",
|
||||
"2 outs 4 runs --- F (fly out)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def hist(nums, title): \n",
|
||||
" \"Plot a histogram.\"\n",
|
||||
" plt.hist(nums, ec='black', bins=max(nums)-min(nums)+1, align='left')\n",
|
||||
" plt.title(f'{title} Mean: {sum(nums)/len(nums):.3f}, Min: {min(nums)}, Max: {max(nums)}')\n",
|
||||
" \n",
|
||||
"hist(innings, 'Runs per inning:')"
|
||||
"# 4th inning in game 6: 4 runs (Homer by Bellhorn)\n",
|
||||
"assert 4 == inning('SS2114F')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs 0 runs --- S (out at first)\n",
|
||||
"1 outs 0 runs --- 1 (single)\n",
|
||||
"1 outs 0 runs --1 B (base on balls)\n",
|
||||
"1 outs 0 runs -21 B (base on balls)\n",
|
||||
"1 outs 0 runs 321 4 (home run)\n",
|
||||
"1 outs 4 runs --- B (base on balls)\n",
|
||||
"1 outs 4 runs --1 F (fly out)\n",
|
||||
"2 outs 4 runs --1 S (out at first)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# 2nd inning in game 7: 4 runs (Grand Slam by Damon)\n",
|
||||
"assert 4 == inning('S1BB4BFS')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"That looks good to me.\n",
|
||||
"\n",
|
||||
"# Simulation\n",
|
||||
"\n",
|
||||
"Now, simulate a hundred thousand innings, and then sample from them to simulate a hundred thousand nine-inning games (for one team), and show histograms of the results, labelled with statistics:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def simulate(N=100000, inning=inning, events=ONBG) -> None:\n",
|
||||
" innings = [inning(events=events, verbose=False) for _ in range(N)]\n",
|
||||
" games = [sum(random.sample(innings, 9)) for _ in range(N)]\n",
|
||||
" hist(innings, 'Runs/inning (for one team)')\n",
|
||||
" hist(games, 'Runs/game (for one team)')\n",
|
||||
" \n",
|
||||
"def hist(nums, title): \n",
|
||||
" \"\"\"Plot a histogram and show some statistics.\"\"\"\n",
|
||||
" plt.hist(nums, ec='black', bins=max(nums)-min(nums), align='left')\n",
|
||||
" plt.xlabel(title)\n",
|
||||
" plt.title(f'μ: {mean(nums):.2f}, σ: {stdev(nums):.2f}, max: {max(nums)}')\n",
|
||||
" plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
@@ -294,10 +320,506 @@
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAX0AAAEWCAYAAACKSkfIAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAfuElEQVR4nO3dfZxcVZ3n8c/XBAjKQxJosE0nBjWIOCuBiQHFByRuCBnX4I6s4AMNk504Iz6w6mJw1SjIDMgowiIoSiQoiqijBIYFYyT4yEMgEAjBSQQhTZ4aEgIIIoTf/nFPmZtOVVd1p7v64Xzfr1e96t5zz7333Jv0r06de+ocRQRmZpaHFw10AczMrHkc9M3MMuKgb2aWEQd9M7OMOOibmWXEQd/MLCMO+mZmGXHQt50mqVXSQklrJYWkiV22f0nSGklPSHpI0v/ZiWPtJml+OtZ6SR/vj2uqUq6JqTxPlV6f7Sb/GyXdJulJScslvam07ShJL3Q5VnszrqM/SHprujdfLKV9vcv1PSvpyYEspxUc9K0vvADcAPx9je2XAQdFxF7AG4H3SvrvvTzW54FJwMuBtwGnS5rRy3L3xuiI2CO9zqqWQdJYYCFwHjAa+BJwraQxpWxrS8fZIyIW9H/R+56kXYALgFvL6RHxT+XrA74P/HAgymjbc9A3AFJN7VWl9cvLNbfuRMSGiLgYuL3G9t9HxJ9KSS8Ar6qRt9tjAScBZ0XE5ohYCXwTOLmRckoaL+l6SZvS9VZepzWyfw+8EdgQET+MiK0R8V2gE6j1Qdew9C2hQ9LpkjZKWifpOEkzJf1nurZPl/JPlfQ7SY+nvBdJ2jVte6OkRyWNT+uHpHwH9aBInwB+BtzfTZlfQvEhPiQ/2IYbB31rSAoGb6qfs+b+cyU9BXQALwG+14tjjAFeBtxdSr4beG2Dh7gYeAaYkF73A/8GXJmOv1zSe+sc46EUdL8tad9aRU2vrml/U1rfT9IGSQ9KOj8Fxka9FBgFjAM+R/HB937gb4E3A5+T9IqUdyvwv4B9gTcA04APAUTEb4FvAAsk7Q58B/hMRNwPIOliSRfXKoSklwP/AJxZp7x/T/Gh98seXKP1Ewd9a0hEjI6IX+/E/ucAewKHUQSXLb04zB7pvbzvlnTcbkkaCRxLEdSeiog1wPnA1IjoTGV8XUTU+jB6FHg9RbPS36ZzXlkj72+Bl0k6UdIuqb3+lcCL0/b7gclAK3B0Ot5X6l1DyXPA2RHxHHAVRUC/ICKejIgVwArgdema7oiIWyLi+Yj4I0WQf2vpWJ8H9gZuA9YCX6tsiIgPRcSHuinHhcBnI+KpOuVtB64ID/Q1KDjoW9NEYRlFbfsLvThEJbjsVUrbC2jkAeE+wAjg4VLaQxTfHOpKHxRLU/DcAHwYmC5pryp5HwNmAR8HNgAzgJ9TfMshItZHxH0R8UJEPAicDry7kXIkj0XE1rT8THrfUNr+DOkDUtKBkq5LD72fAP6F4kOiUtbngMspvoV8udHALOm/AXtGxA/q5BtP8SFzRSPHtf7noG9lo0rLo/vxPCMpar49EhGbgXXAIaXkQyhqtvV0As8CB5TSDiAF4l6oBMeuzTjFxoibI+L1ETEW+ADwaoradK1jVT1OH7iE4pvFpPQg/dPlc0kaB8wDvg18WdJuDR53GjAlfZisB94DnCbpmi75TgJ+GxEP7OR1WB9x0LeyUySNkDSZ4o96z9Q7oy5Jo4BKwNgtrSPpRZI+KGmMClOBU4HFPT1WcgXwmXS8g4B/pKipVvYNSUd1PWZEvEDxHOFsSXuq6Ar6cWo30XQt0+GSXp2uZx+Kpo0lEVG1mUrSoalpZy+K5wYdEXFj2naUpAnpfowHzgGuKe17uaTLqx23F/YEngCeSvfrn0vnEcW9uwyYTfGBWrVHUhWfBQ6kaKaaTNFb6ZvAKV3ynUTp38cGnoO+lb2Y4g//mxQPCNsp2pxJfa3f3M2+z7Ct+eV+tjU7ALwL+ANFM8x3gf+bXtQ4dnfHmpeO9RBwM3BeRNyQjtOW9runRhlPAx4HHqB4qPhdYH6pHCskva/Gvq+g6Er6JHAvxbeGE0v7fl3S10v5T6d4DrCGou3+XaVthwG/A/5E0f5/L/DR0vbxwG9qlKOnPgm8N5X7m0C5OeajwP4U7fJBEbBPqfxbVLmmv0rPD9ZXXhT/Rn+KiE2VPJLeALThrpqDivxsxaCoIVM0Aawe6LL0lqT3A6+NiDMGuiy9lbpT3g28LrW3m/UpB30DhkfQN7P63LxjZpYR1/TNzDLimr6ZWUZGDnQBurPvvvvGxIkTB7oYZmZDyh133PFoRLRU2zaog/7EiRNZunTpQBfDzGxIkfRQrW1u3jEzy4iDvplZRhz0zcwyUjfop/FG7iq9npB0mqSxkhZJWpXex6T8knShpNVpfPLDSsdqT/lXaQhPD2dmNlTVDfpp1qPJETGZYtzvp4GfAHOBxRExiWLwrLlpl2MpprObBMyhGOWvMoXcPOBwYCowT9tPH2dmZv2sp80704A/RMRDFOOFV6Y/WwAcl5ZnkSZMiIhbgNGSWoFjgEURsSkNkbuIYpxxMzNrkp4G/RMoJjgG2D8i1gGk9/1S+jiKkQUrOlJarfTtSJojaamkpZ2dnT0snpmZdafhoJ9G/3sn9YdJrTYZRK1JInYYAyIiLo2IKRExpaWl6m8LzMysl3pS0z8WuDNNFQewITXbkN43pvQOivHAK9oo5t6slW5mZk3Sk6B/ItuadqCYKafSA6edbTP/LAROSr14jgC2pOafGynmFB2THuBOT2k2AFrbJiBph1dr24SBLpqZ9aOGhmGQ9GLgvwIfLCWfA1wtaTbFZNPHp/TrgZnAaoqePqcARMQmSWcBt6d8Z5Zn2bHmWv/IGl7+qet2SH/o3HcMQGnMrFkaCvoR8TSwT5e0xyh683TNGxRzoFY7znxK09OZmVlz+Re5tr0Ru1Rt9nHTj9nwMKhH2bSd09o2gfWPrKmfsWzrc1WbfcBNP2bDgYP+MFar3R4cwM1y5eYdM7OMOOibmWXEQd/MLCMO+mZmGXHQNzPLiIO+Na5GH3733zcbOtxl0xpXow+/u3+aDR2u6ZuZZcRB38wsIw76ZmYZcdA3M8uIg/4wUGtCFDOzrtx7ZxjwhChm1ijX9M3MMuKgbzvPE6+YDRlu3rGd54lXzIYM1/TNzDLioG9mlpGGgr6k0ZJ+JOl+SSslvUHSWEmLJK1K72NSXkm6UNJqScslHVY6TnvKv0pSe39dlJmZVddoTf8C4IaIOAg4BFgJzAUWR8QkYHFaBzgWmJRec4BLACSNBeYBhwNTgXmVDwozM2uOukFf0l7AW4DLACLiLxHxODALWJCyLQCOS8uzgCuicAswWlIrcAywKCI2RcRmYBEwo0+vxszMutVITf8VQCfwbUnLJH1L0kuA/SNiHUB63y/lHwesKe3fkdJqpW9H0hxJSyUt7ezs7PEFmZlZbY0E/ZHAYcAlEXEo8Ce2NeVUU+33/9FN+vYJEZdGxJSImNLS0tJA8czMrFGNBP0OoCMibk3rP6L4ENiQmm1I7xtL+ceX9m8D1naTbmZmTVI36EfEemCNpFenpGnAfcBCoNIDpx24Ji0vBE5KvXiOALak5p8bgemSxqQHuNNTmpmZNUmjv8j9CHClpF2BB4BTKD4wrpY0G3gYOD7lvR6YCawGnk55iYhNks4Cbk/5zoyITX1yFWZm1pCGgn5E3AVMqbJpWpW8AZxa4zjzgfk9KaCZmfUd/yLXzCwjDvpDRK2JUjxZipn1hEfZHCJqTZQCHsnSzBrnmr71rxpj7XucfbOB4Zq+9a8aY+3724nZwHBN38wsIw76ZmYZcdA3M8uIg76ZWUYc9M3MMuKgb2aWEQd9M7OMOOibmWXEQd/MLCMO+mZmGXHQNzPLiIO+mVlGHPTNzDLioG9mlhEHfRsYNcbZ91j7Zv3L4+nbwKgxzj54rH2z/tRQTV/SHyXdI+kuSUtT2lhJiyStSu9jUrokXShptaTlkg4rHac95V8lqb1/LmloqzUXrplZX+hJTf9tEfFoaX0usDgizpE0N61/CjgWmJRehwOXAIdLGgvMA6YAAdwhaWFEbO6D6xg2as2F69qvmfWFnWnTnwUsSMsLgONK6VdE4RZgtKRW4BhgUURsSoF+ETBjJ85vZmY91GjQD+Bnku6QNCel7R8R6wDS+34pfRywprRvR0qrlb4dSXMkLZW0tLOzs/ErMTOzuhpt3jkyItZK2g9YJOn+bvJWa4CObtK3T4i4FLgUYMqUKTtsNzOz3muoph8Ra9P7RuAnwFRgQ2q2Ib1vTNk7gPGl3duAtd2km5lZk9QN+pJeImnPyjIwHbgXWAhUeuC0A9ek5YXASakXzxHAltT8cyMwXdKY1NNnekozM7MmaaR5Z3/gJ6nb4EjgexFxg6TbgaslzQYeBo5P+a8HZgKrgaeBUwAiYpOks4DbU74zI2JTn12JmZnVVTfoR8QDwCFV0h8DplVJD+DUGseaD8zveTHNzKwveBgGM7OMOOibmWXEQd/MLCMO+mZmGXHQt8GnxrDLHnLZbOd5aGUbfGoMu+xB58x2nmv6ZmYZcdA3M8uIg76ZWUYc9M3MMuKgb2aWEQd9M7OMOOibmWXEQd/MLCMO+mZmGXHQNzPLiIO+mVlGHPTNzDLioG9mlhEHfTOzjDjom5llpOGgL2mEpGWSrkvrB0i6VdIqST+QtGtK3y2tr07bJ5aOcUZK/72kY/r6YoaK1rYJVScJkTTQRTOzYa4nk6h8DFgJ7JXWzwXOj4irJH0dmA1ckt43R8SrJJ2Q8r1H0sHACcBrgZcBP5d0YERs7aNrGTLWP7Km6iQh4IlCupVm1KrmpePGs67j4SYXyGzoaSjoS2oD/g44G/i4ir+8o4H3piwLgM9TBP1ZaRngR8BFKf8s4KqIeBZ4UNJqYCrwuz65EhvLine truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"CPU times: user 2.52 s, sys: 16.6 ms, total: 2.54 s\n",
|
||||
"Wall time: 2.58 s\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"hist(games, 'Runs per game:')"
|
||||
"%time simulate()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"So, about 13 runs per game (per team). This shows that the dice game is not very realistic with respect to current-day baseball. It is true that games were higher-scoring 130 years ago, and perhaps a dice game is more fun when there is a lot of action.\n",
|
||||
"\n",
|
||||
"# Real Major League Baseball Stats\n",
|
||||
"\n",
|
||||
"Could I make the game reflect baseball as it is played today? To do so I would need:\n",
|
||||
"1. A source of major league baseball (MLB) statistics.\n",
|
||||
"2. A way to convert those statistics into the format expected by the function `inning`.\n",
|
||||
"3. Possibly some modifications to `inning`, depending on how the conversion goes.\n",
|
||||
"\n",
|
||||
"[Baseball-reference.com](https://www.baseball-reference.com) has lots of stats, in particular \n",
|
||||
"[MLB annual batting stats](https://www.baseball-reference.com/leagues/MLB/bat.shtml) and\n",
|
||||
"[fielding stats](https://www.baseball-reference.com/leagues/MLB/field.shtml); I'll use the stats for the complete 2019 season. The batting stats have most of what we need, and the fielding stats give us double plays and errors.\n",
|
||||
"\n",
|
||||
"I start by defining two utility functions that can be useful for any tabular data: `cell_value`, which converts a table cell entry into an `int`, `float`, or `str` as appropriate; and `header_row_dict`, which creates a dict of `{column_name: value}` entries. The function `mlb_convert` then converts this format (a dict keyed by `H/2B/3B/HR` etc.) into the event code format (a string of `'1234...'`). As part of the conversion I'll add hit-by-pitch (`HBP`) into the \"base on balls\" category, and I'll record all otherwise unaccounted-for outs under the \"fly out\" (`F`) category (runners do not advance). With this understood, we won't need to change the function `inning` at all. (It is true that `mlb_convert` returns a very long string, equal in length to the number of plate appearances over the whole MLB season. But that takes up less space than storing one photo, so I'm not going to worry about it.)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def cell_value(entry, types=(int, float, str)):\n",
|
||||
" \"\"\"Convert a cell entry into the first type that doesn't raise an error.\"\"\"\n",
|
||||
" for typ in types:\n",
|
||||
" try:\n",
|
||||
" return typ(entry)\n",
|
||||
" except ValueError:\n",
|
||||
" pass\n",
|
||||
" \n",
|
||||
"def header_row_dict(header, row, sep=None, value=cell_value) -> dict:\n",
|
||||
" \"\"\"Parse a header and table row into a dict of `{column_name: value(cell)}`.\"\"\"\n",
|
||||
" return dict(zip(header.split(sep), map(value, row.split(sep))))\n",
|
||||
"\n",
|
||||
"def mlb_convert(stats: dict) -> str:\n",
|
||||
" \"\"\"Given baseball stats return a string '11...FFF'.\"\"\"\n",
|
||||
" events = Counter({\n",
|
||||
" '1': stats['H'] - stats['2B'] - stats['3B'] - stats['HR'],\n",
|
||||
" '2': stats['2B'], '3': stats['3B'], '4': stats['HR'],\n",
|
||||
" 'E': stats['E'], 'B': stats['BB'] + stats['HBP'],\n",
|
||||
" 'K': stats['SO'], 'D': stats['DP'], 'S': stats['SH'] + stats['SF']})\n",
|
||||
" events['F'] = stats['PA'] - sum(events.values()) # All unaccounted-for outs\n",
|
||||
" return ''.join(events.elements()) # A str of events"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Below I copy-and-paste the data I need from baseball-reference.com to create the dict `mlb_stats`; convert it to the string `mlb_string`; and use that to create the event generator `mlb_stream`:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"mlb_stats = header_row_dict(\n",
|
||||
" \"Year Tms #Bat BatAge R/G G PA AB R H 2B 3B HR RBI SB CS BB SO BA OBP SLG OPS TB GDP HBP SH SF IBB E DP\",\n",
|
||||
" \"\"\"2019 30 1284 27.9 4.84 4828 185377 165622 23346 41794 8485 783 6735 22358 2261 827 15806 42546 \n",
|
||||
" .252 .323 .435 .758 72050 3441 1968 774 1146 752 2882 3981\"\"\")\n",
|
||||
"mlb_string = mlb_convert(mlb_stats)\n",
|
||||
"mlb_stream = event_stream(mlb_string)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can take a look:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'Year': 2019,\n",
|
||||
" 'Tms': 30,\n",
|
||||
" '#Bat': 1284,\n",
|
||||
" 'BatAge': 27.9,\n",
|
||||
" 'R/G': 4.84,\n",
|
||||
" 'G': 4828,\n",
|
||||
" 'PA': 185377,\n",
|
||||
" 'AB': 165622,\n",
|
||||
" 'R': 23346,\n",
|
||||
" 'H': 41794,\n",
|
||||
" '2B': 8485,\n",
|
||||
" '3B': 783,\n",
|
||||
" 'HR': 6735,\n",
|
||||
" 'RBI': 22358,\n",
|
||||
" 'SB': 2261,\n",
|
||||
" 'CS': 827,\n",
|
||||
" 'BB': 15806,\n",
|
||||
" 'SO': 42546,\n",
|
||||
" 'BA': 0.252,\n",
|
||||
" 'OBP': 0.323,\n",
|
||||
" 'SLG': 0.435,\n",
|
||||
" 'OPS': 0.758,\n",
|
||||
" 'TB': 72050,\n",
|
||||
" 'GDP': 3441,\n",
|
||||
" 'HBP': 1968,\n",
|
||||
" 'SH': 774,\n",
|
||||
" 'SF': 1146,\n",
|
||||
" 'IBB': 752,\n",
|
||||
" 'E': 2882,\n",
|
||||
" 'DP': 3981}"
|
||||
]
|
||||
},
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"mlb_stats"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'111111111111111111111111112222222223444444EEEBBBBBBBBBBBBBBBBBBKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKDDDDSSFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'"
|
||||
]
|
||||
},
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"mlb_string[::1000] # Just look at every 1000th character"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs 0 runs --- 2 (double)\n",
|
||||
"0 outs 0 runs -2- F (fly out)\n",
|
||||
"1 outs 0 runs -2- F (fly out)\n",
|
||||
"2 outs 0 runs -2- F (fly out)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"0"
|
||||
]
|
||||
},
|
||||
"execution_count": 15,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"inning(mlb_stream)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"I can simulate:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"CPU times: user 2.39 s, sys: 21.8 ms, total: 2.41 s\n",
|
||||
"Wall time: 2.46 s\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%time simulate(events=mlb_stream)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"That looks a *lot* more like real baseball. But MLB averaged 4.84 runs per team per game in 2019, and this is significantly lower. I think we can make some minor changes to the function `inning`—some \"standard baseball things\"—to make the simulation more realistic. I'm thinking of two changes:\n",
|
||||
"- The most common double play eliminates the batter and the runner on first, not the runner closest to home. \n",
|
||||
"- On a single, a runner on second often scores. \n",
|
||||
"\n",
|
||||
"I'll make those two things the case for all double plays and singles."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def inning2(events, verbose=False) -> int:\n",
|
||||
" \"\"\"Simulate a half inning based on events, and return number of runs scored.\"\"\"\n",
|
||||
" outs = runs = 0 # Inning starts with no outs and no runs,\n",
|
||||
" runners = set() # and with nobody on base\n",
|
||||
" def out(r) -> int: runners.remove(r); return 1\n",
|
||||
" def forced(r) -> bool: return all(b in runners for b in range(r))\n",
|
||||
" def advance(r, e) -> int: \n",
|
||||
" return ((2 if r == 2 else int(e)) if e in '1234' else \n",
|
||||
" (e in 'ESD' or (e == 'B' and forced(r)))) \n",
|
||||
" for e in events:\n",
|
||||
" if verbose: show(outs, runs, runners, e)\n",
|
||||
" runners.add(batter) # Batter steps up to the plate\n",
|
||||
" if e == 'D' and 1 in runners: # Double play: batter and runner on first out\n",
|
||||
" outs += out(batter) + out(1)\n",
|
||||
" elif e in 'DSKOF': # Batter is out\n",
|
||||
" outs += out(batter) \n",
|
||||
" if outs >= 3: # If inning is over: return runs scored\n",
|
||||
" return runs \n",
|
||||
" runners = {r + advance(r, e) for r in runners} # Runners advance\n",
|
||||
" runs += len(runners & scored) # Tally runs\n",
|
||||
" runners = runners - scored # Remove runners who scored"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We show the difference with two examples. First, a triple/walk/double-play sequence scores a run under `inning2` but not `inning`:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs 0 runs --- 3 (triple)\n",
|
||||
"0 outs 0 runs 3-- B (base on balls)\n",
|
||||
"0 outs 0 runs 3-1 D (double play)\n",
|
||||
"2 outs 1 runs --- K (strikeout)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"inning2('3BDK', True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 19,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs 0 runs --- 3 (triple)\n",
|
||||
"0 outs 0 runs 3-- B (base on balls)\n",
|
||||
"0 outs 0 runs 3-1 D (double play)\n",
|
||||
"2 outs 0 runs -2- K (strikeout)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"0"
|
||||
]
|
||||
},
|
||||
"execution_count": 19,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"inning('3BDK', True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Second, a double/single sequence scores a run under `inning2` but not `inning`:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 20,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs 0 runs --- 2 (double)\n",
|
||||
"0 outs 0 runs -2- 1 (single)\n",
|
||||
"0 outs 1 runs --1 F (fly out)\n",
|
||||
"1 outs 1 runs --1 F (fly out)\n",
|
||||
"2 outs 1 runs --1 F (fly out)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 20,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"inning2('21FFF', True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 21,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 outs 0 runs --- 2 (double)\n",
|
||||
"0 outs 0 runs -2- 1 (single)\n",
|
||||
"0 outs 0 runs 3-1 F (fly out)\n",
|
||||
"1 outs 0 runs 3-1 F (fly out)\n",
|
||||
"2 outs 0 runs 3-1 F (fly out)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"0"
|
||||
]
|
||||
},
|
||||
"execution_count": 21,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"inning('21FFF', True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can simulate again and note any differences:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"metadata": {
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAYMAAAEWCAYAAACEz/viAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAf+UlEQVR4nO3de5hcVZnv8e/PhKtckkDDxHRCIjQ4wFHEHkARRaIhYdDgOTIn6JEG44lHg5dRj4Azz0RBHCOOUR6FIUIkeBxixEtCBGIEIspAINwSQmDShkuaBNKYEFC5Jbznj7VKNp3qS1V1d3U6v8/z7Kf3fvdae6/aXU+9tfZllSICMzPbub2u3g0wM7P6czIwMzMnAzMzczIwMzOcDMzMDCcDMzPDycDMzHAysDqR9ENJIemQTtYfKmmBpHZJmyQtlnRYYb0kfU3SE5K2SFoq6Yh+avu3JK2R9JykhySd2UXZL0v6U2F6XtIrkvbP61d1WL9V0nX98Tp6i6TjJC3J/6d2ST+VNLKw/nOS1kp6VtJ6SbMkDa1nm217TgbW7yS9Ezi4m2LDgIXAYcCBwJ3AgsL604GPAScAI4DbgR/1emPL+zPwfmBfoAX4rqR3lCsYEV+PiL1KEzATWBoRT+f1RxTW7Q08Dvy0X15F7xkOzAbGAgcBzwE/LKy/Djg6IvYBjgTeAnymn9to3YkIT556PAEBHFJYvgr4WgX1hwL3Am/uuK1u6o3I5ffLy+cC8wvrjwBeqKAdnwT+ALyUtxvAM1Uek4XAF3pQTnmfLZ2sfzfwJ+D1PdzvV0iJ4/+RPoBXAocC5wMbgXXAhEL5s4HVuexa4BOFdecCdwBDC8dnFbB7FcfjaOC5TtbtB/wGuLTe72VPr53cM7BeJemZ/M2/M/8I3BoRKyrc9LuAJyPij3l5HnBIPp20C+kb+o09bOPbgG8DU4E9gc8Dm4H35PUfltSj9knaA/g70gdnd04g9XJ+1sn6FuDaiPhzT/advZ/UIxpOSrKLST3+UcAFwOWFshuBU4F9SIlhlqSj87qLSYnxnyU1AV8H/ldEvAAgaYWkD/ewTe+iw/HIx/RZ4GlSz+DychWtjuqdjTztWBM19AyA0UArsG+5bXVRrxF4AjijENsV+G7exlbgEWBcD9vxVeAnhWXl7b+niuMxl5SE1IOyVwJXdbJuT+BZ4MQK9v0VYElh+f2knsWQvLx3Pj7DOqn/S+CzheWxwCZS7+H8Kt8fb87bOKGT9U3AhcDf1Pu97Om1k3sG1p++A1wQEVt6WkFSA/Br0mmFawqrZpC+kY8Gdid9wN8sac8ebPYA0rl5ACJ9Sj0OvKGn7cptu5h0Dvwf8ja6KrsH6TrH3E6K/HfSh+hvK2kD8FRh/nng6YjYVlgG2Cu3YZKkO/KF3meAU4D9S5Uj4lHgFlJS+H6F7SDfDHADKcH8rlyZiFhD6jVcWun2rW85GVg1di/MD6ug3njgYklPSnoyx27v7PSDpOGkRLAwIi7qsPotpG/3bRGxNSKuIp0qObwH7WgDxhX28zrShc+2nr4QSV8FJpHOyT/bgyqlD/ulnaxvAa7uLqlUS9JupNNT3wIOjIhhwPWkXlGpzCnA24GbSKeNKtn+QaRrARdGRHcX8ofS/Q0E1s+cDKwaZ0saIuko0gf83vm8fXcOJX2IH5UnSKc2ftGxoKR9SOe/b4uI88ps6y7gdEkHSnqdpI8Cu5BOQyHpK5KWdtKOecBESe/N7f4i8ALpjqRuSTof+DDwvnj1GkZ3Ov2wl9RIul6xXa9B0qOSzurhPrqyK7Ab0A5slTQJmFDYz/6k01gfz219f04O3ZI0CrgZ+H5E/HuZ9R+XdECeP5x0gfum2l6O9TYnA6vGnsAG4AfAv5A+PE4CyPfKn1CuUkRsjIgnS1MOPx0Rz+e6N0j6co5/kHQa6OwO9+GPyetnAvcD9wHPkC5M/4+IeCavHw3c1kk7/gBMIZ22ehr4e+D9EfFSbsdHJHV1QfjrwBhgTaFdpXZvdwzyh+VJwNWdbO+jwO25XX8laVfS3Td3dNGWHomI50i3c84nXSz/MOkuqJLZwIKIuD4nuKnAFZL2y21ZJekjnWz+48AbgRnF/1Vh/fHASkl/JvVGrge+XGY7Vkfqo16pDVKSAmiKiNZ6t6Urku4DxlfwzX3AyXdlTY+IM+rdFhv8nAysIjtKMjCzynR7mkjSHEkbJT3QIf5pSQ/n7uM3C/HzJbXmdScX4hNzrFXSeYX4OEnLlB7v/0nuGpuZWT/qyTWDq4CJxYCk9wCTgTdHxBGkOxRKF4emkJ4GnQhcmi80DiHdqjaJdLfHGbkspHO/syKiiXQuc2qtL8r6TkTIvQKzwafbZBARt5JuiSv6JPCNiHgxl9mY45OBeRHxYkQ8Qrqz45g8tUbE2nyRbh4wWZJIF9auzfXnAqfV+JrMzKxC1Y4ceChwgqSLSLfkfTEi7iI9Al+886EtxyCNk1KMH0u6U+KZiNhapnyX9t9//xg7dmyVzTcz2zndfffdT0dEQ8d4tclgKOkBn+NIt//Nl/RGCg+wFATleyDRRfmyJE0DpgGMGTOG5cuXV9hsM7Odm6THysWrfc6gDfh5JHcCr5Aea28j3d9d0gis7yL+NDCsMLZ5KV5WRMyOiOaIaG5o2C6xmZlZlapNBr/k1YeMDiU93fg06SGWKZJ2kzSONCjVnaSnRZvynUO7ki4yL8xPY94CfChvt4XXjllvZmb9oNvTRJKuAU4E9pfURhogbA4wJ99u+hJpfPYAVkmaDzxIGklyemnQLEnnkIYXGALMiYjSE57nAvMkfY00BO+Vvfj6zMysB3bYh86am5vD1wzMzCoj6e6IaO4Y99hEZmbmZGBmZk4GZmaGk4GZmeFkYGZmOBkMSCMbxyCpomlk45juN2xm1olqh6OwPvTkE+s46NxFFdV5bOapfdQaM9sZuGdgZmZOBmZm5mRgZmY4GZiZGU4GZmaGk4GZmeFkMHgM2cXPJphZ1fycwWCx7WU/m2BmVXPPwMzMnAzMzMzJwMzMcDIwMzN6kAwkzZG0UdIDZdZ9UVJI2j8vS9IlklolrZB0dKFsi6Q1eWopxN8maWWuc4kk9daLMzOznulJz+AqYGLHoKTRwPuAxwvhSUBTnqYBl+WyI4AZwLHAMcAMScNzncty2VK97fZlZmZ9q9tkEBG3ApvKrJoFfAmIQmwycHUkdwDDJI0ETgaWRMSmiNgMLAEm5nX7RMTtERHA1cBptb0kMzOrVFXXDCR9AHgiIu7vsGoUsK6w3JZjXcXbysQ72+80ScslLW9vb6+m6WZmVkbFyUDSnsA/Af9SbnWZWFQRLysiZkdEc0Q0NzQ09KS5dVXNL5b5komZ1UM1TyAfDIwD7s8fXI3APZKOIX2zH10o2wisz/ETO8SX5nhjmfKDQjW/WAZ+MtjM+l/FPYOIWBkRB0TE2IgYS/pAPzoingQWAmfmu4qOA7ZExAZgMTBB0vB84XgCsDive07ScfkuojOBBb302szMrId6cmvpNcDtwGGS2iRN7aL49cBaoBX4AfApgIjYBFwI3JWnC3IM4JPAFbnOH4AbqnspZmZWrW5PE0XEGd2sH1uYD2B6J+XmAHPKxJcDR3bXDjMz6zt+AtnMzJwMzMzMycDMzHAyMDMznAzMzAwnAzMzw8nAzMxwMjAzM5wMzMwMJwMzM8PJwMzMcDIwMzOcDMzMDCcDMzPDycDMzHAyMDMznAzMzAwnAzMzo2e/gTxH0kZJDxRiF0t6SNIKSb+QNKyw7nxJrZIelnRyIT4xx1olnVeIj5O0TNIaST+RtGtvvkAzM+teT3oGVwETO8SWAEdGxJuB/wLOB5B0ODAFOCLXuVTSEElDgO8Dk4DDgTNyWYCZwKyIaAI2A1NrekXWc0N2QVJF08jGMfVutZn1gaHdFYiIWyWN7RD7dWHxDuBDeX4yMC8iXgQekdQKHJPXtUbEWgBJ84DJklYDJwEfzmXmAl8BLqvmxViFtr3MQecuqqjKYzNP7aPGmFk99cY1g48BN+T5UcC6wrq2HOssvh/wTERs7RAvS9I0ScslLW9vb++FppuZGdSYDCT9E7AV+HEpVKZYVBEvKyJmR0RzRDQ3NDRU2lwzM+tEt6eJOiOpBTgVGB8RpQ/wNmB0oVgjsD7Pl4s/DQyTNDT3Dorlzcysn1TVM5A0ETgX+EBE/KWwaiEwRdJuksYBTcCdwF1AU75zaFfSReaFOYncwqvXHFqABdW9FDMzq1ZPbi29BrgdOExSm6SpwPeAvYElku6T9O8AEbEKmA88CNwITI+Ibflb/znAYmA1MD+XhZRUPp8vNu8HXNmrr9DMzLrVk7uJzigT7vQDOyIuAi4qE78euL5MfC2v3nFkZmZ14CeQzczMycDMzJwMzMwMJwMzM8PJwMzMcDIwMzOcDMzMDCcDMzPDycDMzHAyMDMznAzMzAwnAzMzw8nAzMxwMjAzM5wMzMwMJwMzM8PJwMzMcDIwMzN69hvIcyRtlPRAITZC0hJJa/Lf4TkuSZdIapW0QtLRhTotufwaSS2F+Nskrcx1LpGk3n6RZmbWtZ70DK4CJnaInQfcFBFNwE15GWAS0JSnacBlkJIHMAM4lvR7xzNKCSSXmVao13FfZmbWx7pNBhFxK7CpQ3gyMDfPzwVOK8SvjuQOYJikkcDJwJKI2BQRm4ElwMS8bp+IuD0iAri6sC0zM+sn1V4zODAiNgDkvwfk+ChgXaFcW451FW8rEy9L0jRJyyUtb29vr7LpZmbWUW9fQC53vj+qiJcVEbMjojkimhsaGqpsYnVGNo5BUkWTmdmOYmiV9Z6SNDIiNuRTPRtzvA0YXSjXCKzP8RM7xJfmeGOZ8gPOk0+s46BzF1VU57GZp/ZRa8zMele1PYOFQOmOoBZgQSF+Zr6r6DhgSz6NtBiYIGl4vnA8AVic1z0n6bh8F9GZhW2ZmVkLine truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"CPU times: user 2.37 s, sys: 17.1 ms, total: 2.39 s\n",
|
||||
"Wall time: 2.41 s\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%time simulate(events=mlb_stream, inning=inning2)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"There is a slight increase in the number of runs.\n",
|
||||
"\n",
|
||||
"# Opportunities for Improvement\n",
|
||||
"\n",
|
||||
"There are many problems with the code as it is. For example:\n",
|
||||
"\n",
|
||||
"- It assumes all teams are equal. They're not.\n",
|
||||
"- It assumes all pitchers and defense are equal. They're not.\n",
|
||||
"- It assumes all batters are equal. They're not. (I think this is the main place where we get a shortfall in runs: real lineups cluster their best hitters together, and they are more apt to produce runs than a lineup of all median players.)\n",
|
||||
"- It assumes all hits are the same (runners always advance the same number of bases). They're not.\n",
|
||||
"- There's only one type of double play (batter and runner on first out) and no triple play.\n",
|
||||
"- It ignores stolen bases, pickoffs, passed balls, wild pitches, runners taking extra bases, and runners being out on attempted steals, extra bases, or sacrifices.\n",
|
||||
"- There is no strategy (offense and defense behave the same, regardless of the situation).\n",
|
||||
"- It assumes both teams bat for 9 innings. But if the home team is ahead at the bottom of the 9th, they do not bat, and if the score is tied: extra innings.\n",
|
||||
"- With two outs, or with no runners on base, there can be no sacrifice or double play; those types of events would just be regular outs. The stats say that a double play should occur in 3981 out of 185377 at bats, or about 2% of the time. In our simulation the `D` event code would come up that often, but perhaps only half the time there would be a runner and less than two outs, so we would only actually get a double play maybe 1% of the time.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"What can you do to make the simulation better?"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -317,7 +839,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.7.2"
|
||||
"version": "3.7.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
+167
-120
@@ -4,11 +4,11 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div align=\"right\"><i>Peter Norvig<br>16 December 2019</i></div>\n",
|
||||
"<div align=\"right\"><i>Peter Norvig<br>Dec 2019</i></div>\n",
|
||||
"\n",
|
||||
"# Predicting Presidential Electoral Votes from Approval Polls\n",
|
||||
"\n",
|
||||
"Various sites aggregate opinion polls on presidential job performance. The polls are broken out state-by-state, month-by-month at [Mourning Consult](https://morningconsult.com/tracking-trump/). Trump's national **net approval** (percent approving minus percent disapproving) is currently -10% on [RealClearPolitics](https://www.realclearpolitics.com/epolls/other/president_trump_job_approval-6179.html), -13% on [538](https://projects.fivethirtyeight.com/trump-approval-ratings/), and -16% on [Gallup](https://news.gallup.com/poll/203198/presidential-approval-ratings-donald-trump.aspx). There are four big caveats in jumping from these numbers to conclusions about the election:\n",
|
||||
"Various sites aggregate opinion polls on presidential job performance. The polls are broken out state-by-state, month-by-month at [Mourning Consult](https://morningconsult.com/tracking-trump/). Trump's national **net approval** (percent approving minus percent disapproving) is currently -7% on [RealClearPolitics](https://www.realclearpolitics.com/epolls/other/president_trump_job_approval-6179.html), -8% on [538](https://projects.fivethirtyeight.com/trump-approval-ratings/), and -9% on [Gallup](https://news.gallup.com/poll/203198/presidential-approval-ratings-donald-trump.aspx). There are four big caveats in jumping from these numbers to conclusions about the election:\n",
|
||||
"\n",
|
||||
"1. Today is not election day 2020. \n",
|
||||
"\n",
|
||||
@@ -22,9 +22,14 @@
|
||||
"predict electoral votes? ***Yes we can!***\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# TL;DR: Trump is at 185 Electoral Votes\n",
|
||||
"# TL;DR: In this model, Trump gets 104 to 235 Electoral Votes\n",
|
||||
"\n",
|
||||
"As of November, 2019, Trump would get **185** electoral votes under the assumption that he wins the electoral votes of states where he currently has positive net approval and loses the states where he has negative net approval. To win, he'd have to get to **270**, which would entail flipping 11 states (if you go in net approval order), the toughest of which would be Pennsylvania, where he is currently at **-9%** net approval."
|
||||
"There is a lot of uncertainty, so I won't try to give an exact single number. Rather, I will say that, as of December 2019, under the assumption that Trump wins the electoral votes of states where he currently has positive net approval and loses the states where he has negative net approval:\n",
|
||||
"- His mean expected number of electoral votes is **149.5**.\n",
|
||||
"- He is tied (net approval zero) in Texas and North Carolina, so depending on how they go, the total would be **123 to 176**.\n",
|
||||
"- If you allow for a swing of up to 3.5% across the board, then the totals range anywere from **104 to 235**.\n",
|
||||
"- Recall that you need **270** to win.\n",
|
||||
"- To reach 270, he'd need to take all the states where he is ahead or tied, plus six states where he is behind by up to **5%**.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -44,7 +49,7 @@
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"185"
|
||||
"149.5"
|
||||
]
|
||||
},
|
||||
"execution_count": 1,
|
||||
@@ -55,7 +60,50 @@
|
||||
"source": [
|
||||
"%run ElectoralVotesCode.ipynb\n",
|
||||
"\n",
|
||||
"EV(states)"
|
||||
"EV(states) # Number of electoral vote Trump can expect according to this model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The `.5` indicates a tie in one or more states. \n",
|
||||
"\n",
|
||||
"Imagine a swing in voter approval of just 0.5% in either direction, for every state across the board. That would break ties, and we'd get a range of electoral votes: fewer in one direction, more in the other. If we allowed larger swings in approval, we'd get an even larger range. Here's how it goes:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Swing EV Range\n",
|
||||
"±0.5% 123 to 176\n",
|
||||
"±1.5% 118 to 205\n",
|
||||
"±2.5% 118 to 224\n",
|
||||
"±3.5% 104 to 235\n",
|
||||
"±4.5% 104 to 253\n",
|
||||
"±5.5% 89 to 286\n",
|
||||
"±6.5% 79 to 290\n",
|
||||
"±7.5% 67 to 290\n",
|
||||
"±8.5% 60 to 290\n",
|
||||
"±9.5% 52 to 306\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"show_swings()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"That is, the `4.5%` row says that if Trump's approval slipped 4.5% in every state, he would suffer a blowout loss, getting only 104 electoral votes. If he gained 4.5% in every state, it would be a close loss with 253 electoral votes (to 285 for the opponent)."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -69,12 +117,12 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 720x504 with 1 Axes>"
|
||||
]
|
||||
@@ -101,9 +149,9 @@
|
||||
"\n",
|
||||
"- **Movement**: I define the **maximum expected movement** of approval as 1/5 of the undecided voters (i.e., assume the undecided voters broke 60/40 one way or the other) plus 2 standard deviations in the monthly net approval. \n",
|
||||
"\n",
|
||||
"- **Swing state**: I define a swing state as one whose maximum expected movement is greater than the absolute value of the net approval. There are **13** such states now, all with net approval between **+3% and -5%**. The swing states are shown below in **BOLD CAPS**.\n",
|
||||
"- **Swing state**: I define a swing state as one whose maximum expected movement is greater than the absolute value of the net approval. There are **12** such states now. The swing states are shown below in **BOLD CAPS**.\n",
|
||||
"\n",
|
||||
"- **Margin**: If you list states in order of net approval, the key turning-point state is Pennsylvania; Trump would need to win that and every state in which he is more popular to reach 270. He currently is **7% behind in Pennsylvania**, so we say that his **margin is 7%**.\n",
|
||||
"- **Margin**: If you list states in order of net approval, the key turning-point state is Pennsylvania; Trump would need to win that and every state in which he is more popular to reach 270. He currently is **5% behind in Pennsylvania**, so we say that his margin is just over 5%.\n",
|
||||
"\n",
|
||||
"# State-by-state summary table\n",
|
||||
"\n",
|
||||
@@ -120,15 +168,16 @@
|
||||
"- **𝝈**: Standard deviation in net approval over the past 12 months in state.\n",
|
||||
"\n",
|
||||
"The table shows that:\n",
|
||||
"- If Trump wins the states he is leading in, he gets **185** electoral votes (the **ΣEV** in the Florida row).\n",
|
||||
"- If he also wins all the swing states, he gets **253** electoral votes (**ΣEV** in the Ohio row). \n",
|
||||
"- To get to **270**, going in order of states with least bad net approval, he would also need to win Ohio, Nevada, and Pennsylvania. Or substitute Virginia for one of these; but he would need to win at least one state where he is currently down by **7%**, so that's the margin.\n",
|
||||
"- Some traditional swing states currently seem out of reach for Trump: he's at **-14%** in Michigan and Wisconsin.\n"
|
||||
"- If Trump wins the states he is leading or tied in, he gets **176** electoral votes (the **ΣEV** in the TEXAS row).\n",
|
||||
"- If he also wins all the swing states, he gets **235** electoral votes (**ΣEV** in the ARIZONA row). \n",
|
||||
"- To get to **270**, he would also need to win Ohio and Pennsylvania. \n",
|
||||
"- He is currently down by **5%** in Pennsylvania, so that's the margin.\n",
|
||||
"- Some traditional swing states currently seem out of reach for Trump: he's at **-15%** in Michigan and **-9%** in Wisconsin.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -136,57 +185,57 @@
|
||||
"text/markdown": [
|
||||
"|State|Net|Move|EV|ΣEV|+|−|?|𝝈|\n",
|
||||
"|-|-|-|-|-|-|-|-|-|\n",
|
||||
"|Wyoming|+35%|±11%|3|3|66%|31%|3%|±5.3%\n",
|
||||
"|Alabama|+22%|±6%|9|12|59%|37%|4%|±2.5%\n",
|
||||
"|Mississippi|+19%|±6%|6|18|57%|38%|5%|±2.7%\n",
|
||||
"|West Virginia|+19%|±6%|5|23|58%|39%|3%|±2.7%\n",
|
||||
"|Idaho|+18%|±6%|4|27|57%|39%|4%|±2.8%\n",
|
||||
"|Tennessee|+16%|±5%|11|38|56%|40%|4%|±2.1%\n",
|
||||
"|Kentucky|+14%|±4%|8|46|55%|41%|4%|±1.5%\n",
|
||||
"|Alaska|+10%|±7%|3|49|53%|43%|4%|±3.1%\n",
|
||||
"|Louisiana|+10%|±7%|8|57|53%|43%|4%|±2.9%\n",
|
||||
"|Oklahoma|+9%|±6%|7|64|53%|44%|3%|±2.5%\n",
|
||||
"|Arkansas|+8%|±5%|6|70|52%|44%|4%|±2.0%\n",
|
||||
"|South Carolina|+8%|±4%|9|79|52%|44%|4%|±1.7%\n",
|
||||
"|**KANSAS**|**+3%**|**±5%**|6|85|50%|47%|3%|±2.2%\n",
|
||||
"|**MISSOURI**|**+3%**|**±6%**|10|95|50%|47%|3%|±2.7%\n",
|
||||
"|**SOUTH DAKOTA**|**+3%**|**±9%**|3|98|50%|47%|3%|±4.1%\n",
|
||||
"|**TEXAS**|**+3%**|**±4%**|38|136|49%|46%|5%|±1.5%\n",
|
||||
"|**INDIANA**|**+2%**|**±4%**|11|147|49%|47%|4%|±1.8%\n",
|
||||
"|**NORTH DAKOTA**|**+2%**|**±6%**|3|150|49%|47%|4%|±2.7%\n",
|
||||
"|**UTAH**|**+2%**|**±7%**|6|156|49%|47%|4%|±3.1%\n",
|
||||
"|**FLORIDA**|**+1%**|**±3%**|29|185|49%|48%|3%|±1.4%\n",
|
||||
"|**MONTANA**|**-1%**|**±7%**|3|188|48%|49%|3%|±3.1%\n",
|
||||
"|**NORTH CAROLINA**|**-2%**|**±4%**|15|203|47%|49%|4%|±1.5%\n",
|
||||
"|**GEORGIA**|**-3%**|**±6%**|16|219|46%|49%|5%|±2.6%\n",
|
||||
"|**ARIZONA**|**-4%**|**±4%**|11|230|46%|50%|4%|±1.7%\n",
|
||||
"|**NEBRASKA**|**-5%**|**±9%**|5|235|46%|51%|3%|±4.1%\n",
|
||||
"|Ohio|-5%|±3%|18|253|46%|51%|3%|±1.0%\n",
|
||||
"|Nevada|-7%|±5%|6|259|45%|52%|3%|±2.3%\n",
|
||||
"|Pennsylvania|-7%|±3%|20|279|45%|52%|3%|±1.2%\n",
|
||||
"|Virginia|-7%|±4%|13|292|45%|52%|3%|±1.9%\n",
|
||||
"|New Mexico|-9%|±7%|5|297|44%|53%|3%|±3.4%\n",
|
||||
"|Iowa|-13%|±4%|6|303|42%|55%|3%|±1.8%\n",
|
||||
"|Maine|-13%|±5%|4|307|42%|55%|3%|±2.4%\n",
|
||||
"|Minnesota|-13%|±5%|10|317|42%|55%|3%|±2.1%\n",
|
||||
"|Michigan|-14%|±4%|16|333|41%|55%|4%|±1.8%\n",
|
||||
"|Wisconsin|-14%|±4%|10|343|41%|55%|4%|±1.7%\n",
|
||||
"|Colorado|-15%|±5%|9|352|41%|56%|3%|±2.3%\n",
|
||||
"|New Hampshire|-15%|±9%|4|356|41%|56%|3%|±4.1%\n",
|
||||
"|New Jersey|-15%|±4%|14|370|41%|56%|3%|±1.5%\n",
|
||||
"|Delaware|-18%|±8%|3|373|39%|57%|4%|±3.5%\n",
|
||||
"|Illinois|-20%|±4%|20|393|38%|58%|4%|±1.6%\n",
|
||||
"|Connecticut|-22%|±3%|7|400|37%|59%|4%|±1.3%\n",
|
||||
"|Rhode Island|-22%|±4%|4|404|37%|59%|4%|±1.8%\n",
|
||||
"|Oregon|-24%|±5%|7|411|36%|60%|4%|±2.1%\n",
|
||||
"|New York|-25%|±3%|29|440|36%|61%|3%|±1.2%\n",
|
||||
"|Maryland|-27%|±4%|10|450|35%|62%|3%|±1.8%\n",
|
||||
"|California|-28%|±3%|55|505|34%|62%|4%|±1.2%\n",
|
||||
"|Massachusetts|-29%|±4%|11|516|34%|63%|3%|±1.8%\n",
|
||||
"|Washington|-29%|±5%|12|528|34%|63%|3%|±2.0%\n",
|
||||
"|Hawaii|-35%|±9%|4|532|31%|66%|3%|±4.2%\n",
|
||||
"|Vermont|-42%|±8%|3|535|28%|70%|2%|±3.6%\n",
|
||||
"|District of Columbia|-62%|±6%|3|538|17%|79%|4%|±2.5%"
|
||||
"|Wyoming|+28%|±11%|3|3|62%|34%|4%|±5.0%\n",
|
||||
"|Alabama|+22%|±6%|9|12|59%|37%|4%|±2.4%\n",
|
||||
"|West Virginia|+21%|±5%|5|17|59%|38%|3%|±2.1%\n",
|
||||
"|Idaho|+16%|±5%|4|21|56%|40%|4%|±2.2%\n",
|
||||
"|Tennessee|+15%|±5%|11|32|56%|41%|3%|±2.2%\n",
|
||||
"|Kentucky|+14%|±4%|8|40|55%|41%|4%|±1.5%\n",
|
||||
"|Mississippi|+14%|±7%|6|46|55%|41%|4%|±3.0%\n",
|
||||
"|Arkansas|+10%|±5%|6|52|53%|43%|4%|±1.9%\n",
|
||||
"|Louisiana|+9%|±7%|8|60|53%|44%|3%|±3.3%\n",
|
||||
"|Oklahoma|+8%|±6%|7|67|52%|44%|4%|±2.5%\n",
|
||||
"|Kansas|+7%|±5%|6|73|52%|45%|3%|±2.3%\n",
|
||||
"|**UTAH**|**+7%**|**±8%**|6|79|52%|45%|3%|±3.6%\n",
|
||||
"|**MISSOURI**|**+6%**|**±6%**|10|89|51%|45%|4%|±2.7%\n",
|
||||
"|**NORTH DAKOTA**|**+5%**|**±5%**|3|92|51%|46%|3%|±2.4%\n",
|
||||
"|South Carolina|+5%|±5%|9|101|51%|46%|3%|±2.1%\n",
|
||||
"|**SOUTH DAKOTA**|**+5%**|**±7%**|3|104|51%|46%|3%|±3.2%\n",
|
||||
"|Indiana|+3%|±3%|11|115|50%|47%|3%|±1.2%\n",
|
||||
"|**MONTANA**|**+3%**|**±7%**|3|118|50%|47%|3%|±3.0%\n",
|
||||
"|**NEBRASKA**|**+1%**|**±7%**|5|123|49%|48%|3%|±3.4%\n",
|
||||
"|**NORTH CAROLINA**|**+0%**|**±4%**|15|138|48%|48%|4%|±1.6%\n",
|
||||
"|**TEXAS**|**+0%**|**±4%**|38|176|48%|48%|4%|±1.7%\n",
|
||||
"|**FLORIDA**|**-1%**|**±3%**|29|205|48%|49%|3%|±1.4%\n",
|
||||
"|**ALASKA**|**-2%**|**±7%**|3|208|47%|49%|4%|±3.2%\n",
|
||||
"|**GEORGIA**|**-2%**|**±6%**|16|224|47%|49%|4%|±2.5%\n",
|
||||
"|**ARIZONA**|**-3%**|**±4%**|11|235|47%|50%|3%|±1.8%\n",
|
||||
"|Ohio|-4%|±2%|18|253|46%|50%|4%|±0.8%\n",
|
||||
"|Pennsylvania|-5%|±3%|20|273|46%|51%|3%|±1.4%\n",
|
||||
"|Virginia|-5%|±4%|13|286|46%|51%|3%|±1.8%\n",
|
||||
"|Maine|-6%|±5%|4|290|46%|52%|2%|±2.4%\n",
|
||||
"|Iowa|-9%|±4%|6|296|44%|53%|3%|±1.9%\n",
|
||||
"|Wisconsin|-9%|±5%|10|306|44%|53%|3%|±2.1%\n",
|
||||
"|New Mexico|-10%|±7%|5|311|43%|53%|4%|±3.2%\n",
|
||||
"|Minnesota|-11%|±5%|10|321|43%|54%|3%|±2.3%\n",
|
||||
"|Nevada|-12%|±5%|6|327|42%|54%|4%|±2.3%\n",
|
||||
"|Delaware|-15%|±8%|3|330|41%|56%|3%|±3.5%\n",
|
||||
"|Michigan|-15%|±5%|16|346|40%|55%|5%|±1.9%\n",
|
||||
"|New Jersey|-15%|±4%|14|360|41%|56%|3%|±1.5%\n",
|
||||
"|Colorado|-18%|±6%|9|369|39%|57%|4%|±2.5%\n",
|
||||
"|New Hampshire|-19%|±6%|4|373|39%|58%|3%|±2.7%\n",
|
||||
"|Oregon|-20%|±5%|7|380|38%|58%|4%|±2.2%\n",
|
||||
"|Connecticut|-21%|±3%|7|387|38%|59%|3%|±1.1%\n",
|
||||
"|Illinois|-22%|±4%|20|407|37%|59%|4%|±1.6%\n",
|
||||
"|Rhode Island|-23%|±4%|4|411|37%|60%|3%|±1.8%\n",
|
||||
"|Maryland|-24%|±5%|10|421|36%|60%|4%|±2.1%\n",
|
||||
"|New York|-25%|±3%|29|450|36%|61%|3%|±1.3%\n",
|
||||
"|Washington|-25%|±4%|12|462|36%|61%|3%|±1.6%\n",
|
||||
"|California|-28%|±3%|55|517|34%|62%|4%|±1.3%\n",
|
||||
"|Hawaii|-28%|±9%|4|521|34%|62%|4%|±4.2%\n",
|
||||
"|Massachusetts|-31%|±4%|11|532|33%|64%|3%|±1.8%\n",
|
||||
"|Vermont|-35%|±8%|3|535|31%|66%|3%|±3.6%\n",
|
||||
"|District of Columbia|-58%|±6%|3|538|19%|77%|4%|±2.6%"
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.Markdown object>"
|
||||
@@ -206,17 +255,17 @@
|
||||
"source": [
|
||||
"# Margin and country-wide net approval by month\n",
|
||||
"\n",
|
||||
"The next plot gives the swing margin needed to reach 270 for each month, along with the country-wide net approval. Trump has been in negative territory on all metrics since his fourth month in office. He's been net -10% or worse country-wide every month since his third in office. His margin has been 7% or worse for a year now. We see that the state-by-state margin roughly correlates with the country-wide net approval, but not exactly."
|
||||
"The next plot gives the swing margin needed to reach 270 for each month, along with the country-wide net approval. Trump has been in negative territory on all metrics since his fourth month in office. He's been net -10% or worse country-wide every month since his third in office. We see that the state-by-state margin roughly correlates with the country-wide net approval, but not exactly."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 720x504 with 1 Axes>"
|
||||
]
|
||||
@@ -235,16 +284,14 @@
|
||||
"source": [
|
||||
"# Popularity Above Replacement President (PARP) \n",
|
||||
"\n",
|
||||
"Fivethirtyeight is a combination sports/politics site, and it has a lot of statistics about sports players and how much better they are than the average replacement player. Given that, they [decided](https://fivethirtyeight.com/features/the-states-where-trump-is-more-and-less-popular-than-he-should-be/) to rate the president's approval versus each state's overall approval of the president's party (in recent elections), which is a way of rating the president's performance versus an average replacement candidate from the same party. I'll duplicate that work and keep it up to date. We define the PARP for a state as the president's net approval minus the average party-member's net approval in that state (known as the \"partisan lean\").\n",
|
||||
"Fivethirtyeight is a combination sports/politics site, and it has a lot of statistics about sports players and how much better they are than the average replacement player. Given that, they [decided](https://fivethirtyeight.com/features/the-states-where-trump-is-more-and-less-popular-than-he-should-be/) to rate the president's approval versus each state's overall approval of the president's party (in recent elections), which is a way of rating the president's performance versus an average replacement candidate from the same party. I'll duplicate that work and keep it up to date. We define the PARP for a state as the president's net approval minus the average party-member's net approval in that state (known as the \"partisan lean\"). \n",
|
||||
"\n",
|
||||
"Trump is much less popular than the average Republican. There are only three states where Trump is exceeding a replacement Republican (i.e., has a positive PARP): one deep-red southern state, Mississippi, and two deep-blue coastal states, Hawaii and Rhode Island (where he is deeply unpopular, but not as unpopular as other Republicans). Again, the swing states are **BOLD CAPITALIZED**. \n",
|
||||
"\n",
|
||||
"In the table below, states are ordered by PARP. In each row we have the state name followed by the partisan lean of the state (positive numbers lean towards the president's party), the electoral votes, and then a pair of statistics (PARP, and net approval (in parentheses)) for four time periods (today, followed by January of 2019, 2018, and 2017). Trump was more popular than an average Republican at his inauguration in January 2017, with positive PARP in 42 states. But by 2018 he had positive PARP in only 6 states, and today only 3."
|
||||
"In the table below, states are ordered by PARP. In each row we have the state name followed by the partisan lean of the state (positive numbers lean towards the president's party), the electoral votes, and then a pair of statistics (PARP, and net approval (in parentheses)) for four time periods (today, followed by January of 2019, 2018, and 2017). Trump was more popular than an average Republican at his inauguration in January 2017, with positive PARP in 42 states. But by 2018 he had positive PARP in only 6 states, and today only 2, Hawaii and Rhode Island, where he is deeply unpopular, but not quite as unpopular as other Republicans."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -252,57 +299,57 @@
|
||||
"text/markdown": [
|
||||
"|State|Lean|EV|PARP|(Net)|PARP'19|(Net)|PARP'18|(Net)|PARP'17|(Net)|\n",
|
||||
"|-|-|-|-|-|-|-|-|-|-|-|\n",
|
||||
"|Mississippi|+15|6|+4|(+19)|-2|(+13)|+2|(+17)|+19|(+34)|\n",
|
||||
"|Rhode Island|-26|4|+4|(-22)|+7|(-19)|+4|(-22)|+22|(-4)|\n",
|
||||
"|Hawaii|-36|4|+1|(-35)|+7|(-29)|+0|(-36)|+23|(-13)|\n",
|
||||
"|Massachusetts|-29|11|+0|(-29)|-2|(-31)|-3|(-32)|+25|(-4)|\n",
|
||||
"|Hawaii|-36|4|+8|(-28)|+7|(-29)|+0|(-36)|+23|(-13)|\n",
|
||||
"|Rhode Island|-26|4|+3|(-23)|+7|(-19)|+4|(-22)|+22|(-4)|\n",
|
||||
"|Delaware|-14|3|-1|(-15)|-1|(-15)|+0|(-14)|+22|(+8)|\n",
|
||||
"|Maine|-5|4|-1|(-6)|-6|(-11)|-11|(-16)|+13|(+8)|\n",
|
||||
"|Maryland|-23|10|-1|(-24)|-7|(-30)|+0|(-23)|+10|(-13)|\n",
|
||||
"|Mississippi|+15|6|-1|(+14)|-2|(+13)|+2|(+17)|+19|(+34)|\n",
|
||||
"|Massachusetts|-29|11|-2|(-31)|-2|(-31)|-3|(-32)|+25|(-4)|\n",
|
||||
"|New Jersey|-13|14|-2|(-15)|-6|(-19)|-3|(-16)|+15|(+2)|\n",
|
||||
"|New Mexico|-7|5|-2|(-9)|-11|(-18)|-13|(-20)|+24|(+17)|\n",
|
||||
"|New Mexico|-7|5|-3|(-10)|-11|(-18)|-13|(-20)|+24|(+17)|\n",
|
||||
"|New York|-22|29|-3|(-25)|-2|(-24)|+4|(-18)|+30|(+8)|\n",
|
||||
"|California|-24|55|-4|(-28)|-6|(-30)|+1|(-23)|+18|(-6)|\n",
|
||||
"|Delaware|-14|3|-4|(-18)|-1|(-15)|+0|(-14)|+22|(+8)|\n",
|
||||
"|**FLORIDA**|+5|29|-4|(+1)|-9|(-4)|+0|(+5)|+17|(+22)|\n",
|
||||
"|Maryland|-23|10|-4|(-27)|-7|(-30)|+0|(-23)|+10|(-13)|\n",
|
||||
"|Alabama|+27|9|-5|(+22)|-7|(+20)|+3|(+30)|+9|(+36)|\n",
|
||||
"|Alaska|+15|3|-5|(+10)|-14|(+1)|-14|(+1)|+9|(+24)|\n",
|
||||
"|Illinois|-13|20|-7|(-20)|-10|(-23)|-8|(-21)|+22|(+9)|\n",
|
||||
"|Louisiana|+17|8|-7|(+10)|-2|(+15)|+2|(+19)|+14|(+31)|\n",
|
||||
"|**NORTH CAROLINA**|+5|15|-7|(-2)|-9|(-4)|-6|(-1)|+13|(+18)|\n",
|
||||
"|Virginia|+0|13|-7|(-7)|-10|(-10)|-4|(-4)|+8|(+8)|\n",
|
||||
"|Maine|-5|4|-8|(-13)|-6|(-11)|-11|(-16)|+13|(+8)|\n",
|
||||
"|Nevada|+1|6|-8|(-7)|-14|(-13)|-2|(-1)|+9|(+10)|\n",
|
||||
"|Pennsylvania|+1|20|-8|(-7)|-11|(-10)|-4|(-3)|+9|(+10)|\n",
|
||||
"|**NORTH CAROLINA**|+5|15|-5|(+0)|-9|(-4)|-6|(-1)|+13|(+18)|\n",
|
||||
"|Virginia|+0|13|-5|(-5)|-10|(-10)|-4|(-4)|+8|(+8)|\n",
|
||||
"|**FLORIDA**|+5|29|-6|(-1)|-9|(-4)|+0|(+5)|+17|(+22)|\n",
|
||||
"|Pennsylvania|+1|20|-6|(-5)|-11|(-10)|-4|(-3)|+9|(+10)|\n",
|
||||
"|Louisiana|+17|8|-8|(+9)|-2|(+15)|+2|(+19)|+14|(+31)|\n",
|
||||
"|Illinois|-13|20|-9|(-22)|-10|(-23)|-8|(-21)|+22|(+9)|\n",
|
||||
"|Kentucky|+23|8|-9|(+14)|-9|(+14)|-8|(+15)|+11|(+34)|\n",
|
||||
"|South Carolina|+17|9|-9|(+8)|-9|(+8)|-10|(+7)|+8|(+25)|\n",
|
||||
"|Connecticut|-11|7|-11|(-22)|-13|(-24)|-8|(-19)|+16|(+5)|\n",
|
||||
"|Minnesota|-2|10|-11|(-13)|-16|(-18)|-12|(-14)|+5|(+3)|\n",
|
||||
"|West Virginia|+30|5|-11|(+19)|-6|(+24)|-8|(+22)|+7|(+37)|\n",
|
||||
"|Ohio|+7|18|-12|(-5)|-13|(-6)|-11|(-4)|+7|(+14)|\n",
|
||||
"|Tennessee|+28|11|-12|(+16)|-16|(+12)|-11|(+17)|+5|(+33)|\n",
|
||||
"|Wyoming|+47|3|-12|(+35)|-17|(+30)|-22|(+25)|-7|(+40)|\n",
|
||||
"|**ARIZONA**|+9|11|-13|(-4)|-17|(-8)|-12|(-3)|+11|(+20)|\n",
|
||||
"|Michigan|-1|16|-13|(-14)|-14|(-15)|-9|(-10)|+9|(+8)|\n",
|
||||
"|Colorado|-1|9|-14|(-15)|-17|(-18)|-13|(-14)|+2|(+1)|\n",
|
||||
"|**TEXAS**|+17|38|-14|(+3)|-17|(+0)|-10|(+7)|+3|(+20)|\n",
|
||||
"|**GEORGIA**|+12|16|-15|(-3)|-14|(-2)|-5|(+7)|+6|(+18)|\n",
|
||||
"|Oregon|-9|7|-15|(-24)|-13|(-22)|-11|(-20)|+11|(+2)|\n",
|
||||
"|Wisconsin|+1|10|-15|(-14)|-17|(-16)|-13|(-12)|+5|(+6)|\n",
|
||||
"|Arkansas|+24|6|-16|(+8)|-14|(+10)|-11|(+13)|+6|(+30)|\n",
|
||||
"|**INDIANA**|+18|11|-16|(+2)|-14|(+4)|-17|(+1)|+4|(+22)|\n",
|
||||
"|**MISSOURI**|+19|10|-16|(+3)|-21|(-2)|-17|(+2)|+0|(+19)|\n",
|
||||
"|Idaho|+35|4|-17|(+18)|-20|(+15)|-24|(+11)|-6|(+29)|\n",
|
||||
"|New Hampshire|+2|4|-17|(-15)|-21|(-19)|-12|(-10)|-1|(+1)|\n",
|
||||
"|Washington|-12|12|-17|(-29)|-14|(-26)|-11|(-23)|+13|(+1)|\n",
|
||||
"|Vermont|-24|3|-18|(-42)|-11|(-35)|-12|(-36)|+22|(-2)|\n",
|
||||
"|District of Columbia|-43|3|-19|(-62)|-22|(-65)|-21|(-64)|+12|(-31)|\n",
|
||||
"|Iowa|+6|6|-19|(-13)|-20|(-14)|-16|(-10)|+3|(+9)|\n",
|
||||
"|**MONTANA**|+18|3|-19|(-1)|-17|(+1)|-18|(+0)|+6|(+24)|\n",
|
||||
"|**KANSAS**|+23|6|-20|(+3)|-22|(+1)|-18|(+5)|+1|(+24)|\n",
|
||||
"|Oklahoma|+34|7|-25|(+9)|-24|(+10)|-19|(+15)|+0|(+34)|\n",
|
||||
"|**SOUTH DAKOTA**|+31|3|-28|(+3)|-25|(+6)|-21|(+10)|-10|(+21)|\n",
|
||||
"|**NEBRASKA**|+24|5|-29|(-5)|-24|(+0)|-18|(+6)|-1|(+23)|\n",
|
||||
"|**UTAH**|+31|6|-29|(+2)|-37|(-6)|-34|(-3)|-4|(+27)|\n",
|
||||
"|**NORTH DAKOTA**|+33|3|-31|(+2)|-29|(+4)|-22|(+11)|-10|(+23)|"
|
||||
"|Minnesota|-2|10|-9|(-11)|-16|(-18)|-12|(-14)|+5|(+3)|\n",
|
||||
"|West Virginia|+30|5|-9|(+21)|-6|(+24)|-8|(+22)|+7|(+37)|\n",
|
||||
"|Connecticut|-11|7|-10|(-21)|-13|(-24)|-8|(-19)|+16|(+5)|\n",
|
||||
"|Wisconsin|+1|10|-10|(-9)|-17|(-16)|-13|(-12)|+5|(+6)|\n",
|
||||
"|Ohio|+7|18|-11|(-4)|-13|(-6)|-11|(-4)|+7|(+14)|\n",
|
||||
"|Oregon|-9|7|-11|(-20)|-13|(-22)|-11|(-20)|+11|(+2)|\n",
|
||||
"|Vermont|-24|3|-11|(-35)|-11|(-35)|-12|(-36)|+22|(-2)|\n",
|
||||
"|**ARIZONA**|+9|11|-12|(-3)|-17|(-8)|-12|(-3)|+11|(+20)|\n",
|
||||
"|South Carolina|+17|9|-12|(+5)|-9|(+8)|-10|(+7)|+8|(+25)|\n",
|
||||
"|**MISSOURI**|+19|10|-13|(+6)|-21|(-2)|-17|(+2)|+0|(+19)|\n",
|
||||
"|Nevada|+1|6|-13|(-12)|-14|(-13)|-2|(-1)|+9|(+10)|\n",
|
||||
"|Tennessee|+28|11|-13|(+15)|-16|(+12)|-11|(+17)|+5|(+33)|\n",
|
||||
"|Washington|-12|12|-13|(-25)|-14|(-26)|-11|(-23)|+13|(+1)|\n",
|
||||
"|Arkansas|+24|6|-14|(+10)|-14|(+10)|-11|(+13)|+6|(+30)|\n",
|
||||
"|**GEORGIA**|+12|16|-14|(-2)|-14|(-2)|-5|(+7)|+6|(+18)|\n",
|
||||
"|Michigan|-1|16|-14|(-15)|-14|(-15)|-9|(-10)|+9|(+8)|\n",
|
||||
"|District of Columbia|-43|3|-15|(-58)|-22|(-65)|-21|(-64)|+12|(-31)|\n",
|
||||
"|Indiana|+18|11|-15|(+3)|-14|(+4)|-17|(+1)|+4|(+22)|\n",
|
||||
"|Iowa|+6|6|-15|(-9)|-20|(-14)|-16|(-10)|+3|(+9)|\n",
|
||||
"|**MONTANA**|+18|3|-15|(+3)|-17|(+1)|-18|(+0)|+6|(+24)|\n",
|
||||
"|Kansas|+23|6|-16|(+7)|-22|(+1)|-18|(+5)|+1|(+24)|\n",
|
||||
"|**ALASKA**|+15|3|-17|(-2)|-14|(+1)|-14|(+1)|+9|(+24)|\n",
|
||||
"|Colorado|-1|9|-17|(-18)|-17|(-18)|-13|(-14)|+2|(+1)|\n",
|
||||
"|**TEXAS**|+17|38|-17|(+0)|-17|(+0)|-10|(+7)|+3|(+20)|\n",
|
||||
"|Idaho|+35|4|-19|(+16)|-20|(+15)|-24|(+11)|-6|(+29)|\n",
|
||||
"|Wyoming|+47|3|-19|(+28)|-17|(+30)|-22|(+25)|-7|(+40)|\n",
|
||||
"|New Hampshire|+2|4|-21|(-19)|-21|(-19)|-12|(-10)|-1|(+1)|\n",
|
||||
"|**NEBRASKA**|+24|5|-23|(+1)|-24|(+0)|-18|(+6)|-1|(+23)|\n",
|
||||
"|**UTAH**|+31|6|-24|(+7)|-37|(-6)|-34|(-3)|-4|(+27)|\n",
|
||||
"|Oklahoma|+34|7|-26|(+8)|-24|(+10)|-19|(+15)|+0|(+34)|\n",
|
||||
"|**SOUTH DAKOTA**|+31|3|-26|(+5)|-25|(+6)|-21|(+10)|-10|(+21)|\n",
|
||||
"|**NORTH DAKOTA**|+33|3|-28|(+5)|-29|(+4)|-22|(+11)|-10|(+23)|"
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.Markdown object>"
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
" gives state-by-state, month-by-month presidential approval poll data. Within the web page there is some Javascript from which\n",
|
||||
" we can extract the data we need. It looks like this:\n",
|
||||
"\n",
|
||||
" var mc_state_trend = [[\"Demographic\",\"January 1, 2017\",\"February 1, 2017\", ...]\n",
|
||||
" var mc_state_trend = [[\"Demographic\",\"Jan-17\",\"\",\"Feb-17\",\"\", ...]\n",
|
||||
" [\"Alabama\",\"62\",\"26\",\"65\",\"29\", ...], \n",
|
||||
" ... ]\n",
|
||||
" \n",
|
||||
"The first row is a header (each date is a day at which polls were aggregated).\n",
|
||||
"The subsequent rows each start with the state name, followed by the approval and disapproval percentages for each date. That is, if there are 34 dates, there will by 68 numbers. The row shown above is saying that on January 1, 2017, 62% of Alabamans approved and 26% disapproved; then on February 1, 2017, 65% approved and 29% disapproved, and so on. Our job is to extract this data and find ways to visualize and understand it.\n",
|
||||
"The first row is a header (each date is a month at which polls were aggregated).\n",
|
||||
"The subsequent rows each start with the state name, followed by the approval and disapproval percentages for each date. That is, if there are 34 dates, there will by 68 numbers. The row shown above is saying that in January, 2017, 62% of Alabamans approved and 26% disapproved; then in February, 2017, 65% approved and 29% disapproved, and so on. Our job is to extract this data and find ways to visualize and understand it.\n",
|
||||
"\n",
|
||||
"First fetch the page and save it locally:"
|
||||
]
|
||||
@@ -57,7 +57,9 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Additional data: the variable `state_data` contains the [electoral votes by state](https://www.britannica.com/topic/United-States-Electoral-College-Votes-by-State-1787124) and the [partisan lean by state](https://github.com/fivethirtyeight/data/tree/master/partisan-lean) (how much more Republican (plus) or Democratic (minus) leaning the state is compared to the country as a whole, across recent elections). The variable `net_usa` has the [country-wide net presidential approval](https://projects.fivethirtyeight.com/trump-approval-ratings/) by month."
|
||||
"Additional data: the variable `state_data` contains the [electoral votes by state](https://www.britannica.com/topic/United-States-Electoral-College-Votes-by-State-1787124) and the [partisan lean by state](https://github.com/fivethirtyeight/data/tree/master/partisan-lean) (how much more Republican (plus) or Democratic (minus) leaning the state is compared to the country as a whole, across recent elections). \n",
|
||||
"\n",
|
||||
"The variable `net_usa` has the [country-wide net presidential approval](https://projects.fivethirtyeight.com/trump-approval-ratings/) by month."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -67,7 +69,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# From https://github.com/fivethirtyeight/data/tree/master/partisan+lean\n",
|
||||
"# a dict of {\"state name\": (electoral_votes, partisan_lean)}\n",
|
||||
"# A dict of {\"state name\": (electoral_votes, partisan_lean)}\n",
|
||||
"state_data = { \n",
|
||||
" \"Alabama\": (9, +27), \"Alaska\": (3, +15), \"Arizona\": (11, +9), \n",
|
||||
" \"Arkansas\": (6, +24), \"California\": (55, -24), \"Colorado\": (9, -1), \n",
|
||||
@@ -88,20 +90,20 @@
|
||||
" \"West Virginia\": (5, +30), \"Wisconsin\": (10, +1), \"Wyoming\": (3, +47)}\n",
|
||||
"\n",
|
||||
"# From https://projects.fivethirtyeight.com/trump-approval-ratings/\n",
|
||||
"# A dict of {'date': country-wide-net-approval}\n",
|
||||
"# A dict of {'date': country-wide-net-approval}, taken from 1st of month.\n",
|
||||
"net_usa = {\n",
|
||||
" 'January 2017': 10, 'January 2018': -18, 'January 2019': -12, \n",
|
||||
" 'February 2017': 0, 'February 2018': -15, 'February 2019': -16, \n",
|
||||
" 'March 2017': -6, 'March 2018': -14, 'March 2019': -11, \n",
|
||||
" 'April 2017': -13, 'April 2018': -13, 'April 2019': -11, \n",
|
||||
" 'May 2017': -11, 'May 2018': -12, 'May 2019': -12, \n",
|
||||
" 'June 2017': -16, 'June 2018': -11, 'June 2019': -12, \n",
|
||||
" 'July 2017': -15, 'July 2018': -10, 'July 2019': -11, \n",
|
||||
" 'August 2017': -19, 'August 2018': -12, 'August 2019': -10, \n",
|
||||
" 'September 2017': -20, 'September 2018': -14, 'September 2019': -13, \n",
|
||||
" 'October 2017': -17, 'October 2018': -11, 'October 2019': -13, \n",
|
||||
" 'November 2017': -19, 'November 2018': -11, 'November 2019': -13,\n",
|
||||
" 'December 2017': -18, 'December 2018': -10, 'December 2019': -12,\n",
|
||||
" 'Jan-17': 10, 'Jan-18': -18, 'Jan-19': -12, 'Jan-20': -11,\n",
|
||||
" 'Feb-17': 0, 'Feb-18': -15, 'Feb-19': -16, 'Feb-20': -10,\n",
|
||||
" 'Mar-17': -6, 'Mar-18': -14, 'Mar-19': -11, \n",
|
||||
" 'Apr-17': -13, 'Apr-18': -13, 'Apr-19': -11, \n",
|
||||
" 'May-17': -11, 'May-18': -12, 'May-19': -12, \n",
|
||||
" 'Jun-17': -16, 'Jun-18': -11, 'Jun-19': -12, \n",
|
||||
" 'Jul-17': -15, 'Jul-18': -10, 'Jul-19': -11, \n",
|
||||
" 'Aug-17': -19, 'Aug-18': -12, 'Aug-19': -10, \n",
|
||||
" 'Sep-17': -20, 'Sep-18': -14, 'Sep-19': -13, \n",
|
||||
" 'Oct-17': -17, 'Oct-18': -11, 'Oct-19': -13, \n",
|
||||
" 'Nov-17': -19, 'Nov-18': -11, 'Nov-19': -13,\n",
|
||||
" 'Dec-17': -18, 'Dec-18': -10, 'Dec-19': -12,\n",
|
||||
" }"
|
||||
]
|
||||
},
|
||||
@@ -146,7 +148,7 @@
|
||||
"\n",
|
||||
"def margin(states, date=now) -> int:\n",
|
||||
" \"What's the least swing that would lead to a majority?\"\n",
|
||||
" return min(swing for swing in range(-50, 50) if EV(states, date, swing) >= 270)\n",
|
||||
" return min(swing for swing in range(-50, 50) if EV(states, date, swing+0.1) >= 270)\n",
|
||||
"\n",
|
||||
"def net(state, date=now) -> int: return state.approvals[date] - state.disapprovals[date]\n",
|
||||
"def undecided(state, date=now) -> int: return 100 - state.approvals[date] - state.disapprovals[date]\n",
|
||||
@@ -164,24 +166,25 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def labels(xlab, ylab): plt.xlabel(xlab); plt.ylabel(ylab); plt.legend()\n",
|
||||
"\n",
|
||||
"def grid(): plt.minorticks_on(); plt.grid(which='minor', ls=':', alpha=0.7)\n",
|
||||
" \n",
|
||||
"def header(head) -> str: return head + '\\n' + '-'.join('|' * head.count('|'))\n",
|
||||
"\n",
|
||||
"def markdown(fn) -> callable: return lambda *args: display(Markdown('\\n'.join(fn(*args))))\n",
|
||||
"\n",
|
||||
"def parp(state, date=now) -> int: return net(state, date) - state.lean "
|
||||
"def parp(state, date=now) -> int: return net(state, date) - state.lean\n",
|
||||
"\n",
|
||||
"def grid(dates, xlab, ylab): \n",
|
||||
" plt.minorticks_on(); plt.grid(which='minor', axis='y', ls=':', alpha=0.7)\n",
|
||||
" plt.xticks(range(len(dates)), dates, rotation=90)\n",
|
||||
" plt.xlabel(xlab); plt.ylabel(ylab); plt.legend()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -192,16 +195,16 @@
|
||||
" N = len(dates)\n",
|
||||
" err = [[EV(states, date) - EV(states, date, -swing) for date in dates],\n",
|
||||
" [EV(states, date, +swing) - EV(states, date) for date in dates]]\n",
|
||||
" grid()\n",
|
||||
" plt.plot(range(N), [270] * N, color='darkorange', label=\"270 EVs\", lw=2)\n",
|
||||
" plt.errorbar(range(N), [EV(states, date) for date in dates], fmt='D-',\n",
|
||||
" yerr=err, ecolor='grey', capsize=7, label='Trump EVs ±3% swing', lw=2)\n",
|
||||
" labels('Months into term', 'Electoral Votes')"
|
||||
" grid(dates, 'Date', 'Electoral Vptes')\n",
|
||||
" #labels('Date', 'Electoral Votes')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -210,11 +213,23 @@
|
||||
" plt.rcParams[\"figure.figsize\"] = [10, 7]\n",
|
||||
" plt.style.use('fivethirtyeight')\n",
|
||||
" N = len(dates)\n",
|
||||
" grid()\n",
|
||||
" plt.plot(range(N), [0] * N, label='Net zero', color='darkorange')\n",
|
||||
" plt.plot(range(N), [-margin(states, date) for date in dates], 'D-', label='Margin to 270')\n",
|
||||
" plt.plot(range(N), [net_usa[date] for date in dates], 'go-', label='Country-wide Net')\n",
|
||||
" labels('Months into term', 'Net popularity')"
|
||||
" grid(dates, 'Date', 'Net popularity')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def show_swings(swings=range(10)):\n",
|
||||
" print('Swing EV Range')\n",
|
||||
" for swing in swings:\n",
|
||||
" s = swing + 0.5\n",
|
||||
" print(f'±{s:3.1f}% {EV(states, swing=-s):3} to {EV(states, swing=s):3}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -226,7 +241,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"@markdown\n",
|
||||
"def show_states(states=states, d=now, ref='January 2017'):\n",
|
||||
"def show_states(states=states, d=now, ref='Jan-17'):\n",
|
||||
" \"A table of states, sorted by net approval, with electoral votes.\"\n",
|
||||
" total = 0\n",
|
||||
" yield header(f'|State|Net|Move|EV|ΣEV|+|−|?|𝝈|')\n",
|
||||
@@ -246,7 +261,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"@markdown\n",
|
||||
"def show_parp(states=states, dates=(now, 'January 2019', 'January 2018', 'January 2017')):\n",
|
||||
"def show_parp(states=states, dates=(now, 'Jan-19', 'Jan-18', 'Jan-17')):\n",
|
||||
" \"A table of states, sorted by Popularity Above Replacement President.\"\n",
|
||||
" def year(date): return '' if date == now else \"'\" + date[-2:]\n",
|
||||
" fields = [f\"PARP{year(date)}|(Net)\" for date in dates]\n",
|
||||
@@ -260,7 +275,7 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"**Tests** (I really should have more)."
|
||||
"**Tests** (I really should have more):"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div style=\"text-align: right\"><i>Peter Norvig<br>March 2020</i></div>\n",
|
||||
"\n",
|
||||
"# Elemental Spelling\n",
|
||||
"\n",
|
||||
"Here's a problem: \n",
|
||||
"\n",
|
||||
"> Given a word, decide if it can be spelled using only the symbols in the **[periodic table](https://en.wikipedia.org/wiki/Periodic_table)** of elements. For example, the word \"bananas\" can be spelled with \"BaNaNaS\" (Barium-Sodium-Sodium-Sulfur). Note that there can be multiple possible spellings for a word—\"coin\" could be \"CoIn\" (Cobalt-Indium) or \"COIN\" (Carbon-Oxygen-Iodine-Nitrogen). \n",
|
||||
"\n",
|
||||
"Here is a sketch of a recursive algorithm to solve the problem. A word is **spellable** if any of the following are true:\n",
|
||||
"- The word is the empty word.\n",
|
||||
"- The first 2 letters of the word (capitalized) form an element symbol, and the rest of the word is spellable.\n",
|
||||
"- The first 1 letter of the word (capitalized) forms an element symbol, and the rest of the word is spellable.\n",
|
||||
"\n",
|
||||
"The input to `spellable` should be a string and the output is a boolean. Here is the code:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def spellable(word: str) -> bool:\n",
|
||||
" \"\"\"Can we spell `word` using the `symbols` of the elements?\"\"\"\n",
|
||||
" return (word == ''\n",
|
||||
" or word[:2].capitalize() in symbols and spellable(word[2:])\n",
|
||||
" or word[:1].capitalize() in symbols and spellable(word[1:]))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"I felt a bit bad about repeating a line of code above—violating [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)—but using a subfunction or `any/for` would add complexity. Here are the 118 currently defined `symbols`. (Note that the symbols are all capitalized, so I capitalize `[word[:2]` and `word[:1]` in `spellable` to make sure they match.)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 23,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"symbols = set( # Elements in the periodic table\n",
|
||||
" 'Ac Al Am Sb Ar As At Ba Bk Be Bi Bh B Br Cd Ca Cf C Ce Cs Cl Cr Co Cn Cu Cm Ds Db '\n",
|
||||
" 'Dy Es Er Eu Fm Fl F Fr Gd Ga Ge Au Hf Hs He Ho H In I Ir Fe Kr La Lr Pb Li Lv Lu '\n",
|
||||
" 'Mg Mn Mt Md Hg Mo Mc Nd Ne Np Ni Nh Nb N No Og Os O Pd P Pt Pu Po K Pr Pm Pa Ra Rn '\n",
|
||||
" 'Re Rh Rg Rb Ru Rf Sm Sc Sg Se Si Ag Na Sr S Ta Tc Te Ts Tb Tl Th Tm Sn Ti W U V Xe '\n",
|
||||
" 'Yb Y Zn Zr'.split())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can now| test the function (on `'Bananas'` and `'hello'`):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 24,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 24,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"spellable('Bananas')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 25,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"False"
|
||||
]
|
||||
},
|
||||
"execution_count": 25,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"spellable('hello')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"That was easy. \n",
|
||||
"\n",
|
||||
"But maybe you'd like to see the actual spelling:`'BaNaNaS'`. The function `spelling` does that. The general idea is the same, except:\n",
|
||||
" - We use the subfunction `first_rest_spelling` rather than repeating code.\n",
|
||||
" - Both `spelling` and `first_rest_spelling` return either a string (the spelling) or `None` if no spelling is possible.\n",
|
||||
" - There might be multiple possible spellings; only one is returned.\n",
|
||||
" - We use `lru_cache` to avoid repeated computation and thereby speed up the function."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 26,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from functools import lru_cache\n",
|
||||
"\n",
|
||||
"@lru_cache()\n",
|
||||
"def spelling(word):\n",
|
||||
" \"The spelling for `word` using `symbols` of the elements; or None if fail.\"\n",
|
||||
" return '' if word == '' else first_rest_spelling(word, 2) or first_rest_spelling(word, 1)\n",
|
||||
"\n",
|
||||
"def first_rest_spelling(word, k):\n",
|
||||
" \"Resulting spelling from taking off first k characters of word; or None if fail.\"\n",
|
||||
" first, rest = word[:k].capitalize(), word[k:]\n",
|
||||
" if first in symbols and spelling(rest) is not None:\n",
|
||||
" return first + spelling(rest)\n",
|
||||
" else:\n",
|
||||
" return None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 27,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'BaNaNaS'"
|
||||
]
|
||||
},
|
||||
"execution_count": 27,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"spelling('bananas')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Testing\n",
|
||||
"\n",
|
||||
"Here I define `bad`, a list of words that are **not** spellable, and `good`, a list of words that **are**, and make some assertions:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"bad = 'hello world failure not an alternative'.split() # Unspellable words\n",
|
||||
"\n",
|
||||
"good = '''howdy sphere falure is notan option bananas \n",
|
||||
" carbon iron silver silicon copper arsenic tin xenon bismuth\n",
|
||||
" attention copernicus inconspicuous hyperbolic orbits functions\n",
|
||||
" wonky nutso officious psychic unprofessional bilateralism \n",
|
||||
" whippersnappers vichyssois bobbysocks alterabilities capabilities\n",
|
||||
" biostatistical physics floccinaucinihilipilification'''.split() # Spellable words\n",
|
||||
"\n",
|
||||
"assert len(symbols) == 118\n",
|
||||
"assert not any(spellable(w) or spelling(w) for w in bad) \n",
|
||||
"assert all(spellable(w) and spelling(w) for w in good)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"And here are the actual spellings for the good words:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'AlTeRaBiLiTiEs',\n",
|
||||
" 'ArSeNiC',\n",
|
||||
" 'AtTeNTiON',\n",
|
||||
" 'BOBBYSOCKS',\n",
|
||||
" 'BaNaNaS',\n",
|
||||
" 'BiLaTeRaLiSm',\n",
|
||||
" 'BiOsTaTiSTiCAl',\n",
|
||||
" 'BiSmUTh',\n",
|
||||
" 'CaPaBiLiTiEs',\n",
|
||||
" 'CaRbON',\n",
|
||||
" 'CoPErNiCuS',\n",
|
||||
" 'CoPPEr',\n",
|
||||
" 'FAlURe',\n",
|
||||
" 'FUNCTiONS',\n",
|
||||
" 'FlOCCInAuCInIHILiPILiFICaTiON',\n",
|
||||
" 'HYPErBOLiC',\n",
|
||||
" 'HoWDy',\n",
|
||||
" 'IS',\n",
|
||||
" 'InCoNSPICuOUS',\n",
|
||||
" 'IrON',\n",
|
||||
" 'NUTsO',\n",
|
||||
" 'NoTaN',\n",
|
||||
" 'OFFICIOUS',\n",
|
||||
" 'OPtION',\n",
|
||||
" 'ORbITs',\n",
|
||||
" 'PHYSiCs',\n",
|
||||
" 'PSYCHIC',\n",
|
||||
" 'SPHeRe',\n",
|
||||
" 'SiLiCoN',\n",
|
||||
" 'SiLvEr',\n",
|
||||
" 'TiN',\n",
|
||||
" 'UNPrOFeSSiONAl',\n",
|
||||
" 'VICHYSSOIS',\n",
|
||||
" 'WHIPPErSNaPPErS',\n",
|
||||
" 'WONKY',\n",
|
||||
" 'XeNoN'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"{spelling(w) for w in good}"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"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.7.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
+3167
-1172
File diff suppressed because it is too large.
Load diff
+319
-195
@@ -4,21 +4,21 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div style=\"text-align: right\">Peter Norvig<br>13 March 2018</div> \n",
|
||||
"<div align=\"right\" style=\"text-align: right\"><i>Peter Norvig<br>March 2018</i></div> \n",
|
||||
"\n",
|
||||
"# Maze Generation\n",
|
||||
"# Making and Solving Mazes\n",
|
||||
"\n",
|
||||
"Let's make some mazes! I'm thinking of mazes like this one, which is a rectangular grid of squares, with walls on some of the sides of squares, and openings on other sides:\n",
|
||||
"Let's make some mazes! I'm thinking of mazes like this one, which is a rectangular grid of squares, with walls on some of the sides of squares, and openings on other sides. The goal is to get from the red arrow to the green arrow.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"The main constraint is that there should be a path from entrance to exit, and it should be ***fun*** to solve the maze with pencil, paper, and brain power—not too easy, but also not impossible. \n",
|
||||
"The two main constraints are that there should be a path from entrance to exit, and it should be ***fun*** to solve the maze with pencil, paper, and brain power—not too easy, but also not impossible. \n",
|
||||
"\n",
|
||||
"As I think about how to model a maze on the computer, it seems like a **graph** is the right model: the nodes of\n",
|
||||
"the graph are the squares of the grid, and the edges of the graph are the openings between adjacent squares. So what properties of a graph make a good maze?\n",
|
||||
"- There must be a path from entrance to exit.\n",
|
||||
"- There must not be too such many paths; maybe it is best if there is only one. \n",
|
||||
"- Probably the graph should be *singly connected*—there shouldn't be islands of squares that are unreachable from the start. And maybe we want exactly one path between any two squares.\n",
|
||||
"- There must not be too many such paths; maybe it is best if there is only one. \n",
|
||||
"- Probably the graph should be *singly connected*—there shouldn't be islands of squares that are unreachable from the start. In fact, maybe we want exactly one path between any two squares.\n",
|
||||
"- The path should have many twists; it would be too easy if it was mostly straight.\n",
|
||||
"\n",
|
||||
"I know that a **tree** has all these properties except the last one. So my goal has become: *Superimpose a tree over the grid, covering every square, and make sure the paths are twisty.* Here's how I'll do it:\n",
|
||||
@@ -60,24 +60,28 @@
|
||||
" | G H I J| | | | | | |\n",
|
||||
" o--o--o--o--o--o--o--o--o--o o\n",
|
||||
" \n",
|
||||
"Continue like this until every square in the grid has been added to the tree. \n",
|
||||
"Continue like this until every square in the grid has been added to the tree. At that point there will be a path from start to goal. Some walls will remain; some will be knocked down.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"## Implementing Random Trees\n",
|
||||
"# Making Random Trees\n",
|
||||
"\n",
|
||||
"I'll make the following implementation choices:\n",
|
||||
"\n",
|
||||
"- A tree will be represented as a list of edges.\n",
|
||||
"- An `Edge` is a tuple of two nodes. I'll keep them sorted so that `Edge(A, B)` is the same as `Edge(B, A)`.\n",
|
||||
"- A node in a tree can be anything: a number, a letter, a square, ...\n",
|
||||
"- The algorithm for `random_tree(nodes, neighbors, pop)` is as follows:\n",
|
||||
" * We will keep track of three collections:\n",
|
||||
" - `tree`: a list of edges that constitutes the tree.\n",
|
||||
"- A tree will be represented as a set of edges.\n",
|
||||
"- An `Edge` is a tuple of two nodes. Edges are bidirectional, so to avoid confusion we will always us the tuple that is in sorted order: always `(A, B)`, never `(B, A)`. The constructor `edge` enforces that.\n",
|
||||
"- A node in a tree can be anything: a number, a letter, ... In this notebook we will make trees where the nodes are squares in a grid, but the function `random_tree` accepts nodes of any type.\n",
|
||||
"- The algorithm for `random_tree(nodes, neighbors, pop)` works as follows:\n",
|
||||
" * The arguments are:\n",
|
||||
" - `nodes`: a collection of nodes.\n",
|
||||
" - `neighbors`: a function such that `neighbors(node)` returns a set of nodes.\n",
|
||||
" - `pop`: a function such that `pop(frontier)` removes and returns an element from `frontier`.\n",
|
||||
" * The function keeps track of three collections:\n",
|
||||
" - `tree`: a set of edges that constitutes the tree.\n",
|
||||
" - `nodes`: the set of nodes that have not yet been added to the tree, but will be.\n",
|
||||
" - `frontier`: a queue of nodes in the tree that are eligible to have an edge added.\n",
|
||||
" * On each iteration:\n",
|
||||
" - Use `pop` to pick a `node` from the frontier, and find the neighbors that are not already in the tree.\n",
|
||||
" - If there are any neighbors, randomly pick one (`nbr`), add `Edge(node, nbr)` to `tree`, remove the\n",
|
||||
" - If there are any neighbors, randomly pick one (`nbr`), add `edge(node, nbr)` to `tree`, remove the\n",
|
||||
" neighbor from `nodes`, and keep both the node and the neighbor on the frontier. If there are no neighbors,\n",
|
||||
" drop the node from the frontier.\n",
|
||||
" * When no `nodes` remain, return `tree`."
|
||||
@@ -86,19 +90,21 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import random\n",
|
||||
"from collections import deque, namedtuple\n",
|
||||
"\n",
|
||||
"def Edge(node1, node2): return tuple(sorted([node1, node2]))\n",
|
||||
"Edge = tuple\n",
|
||||
"Tree = set\n",
|
||||
"\n",
|
||||
"def random_tree(nodes: set, neighbors: callable, pop: callable) -> [Edge]:\n",
|
||||
" \"Repeat: pop a node and add Edge(node, nbr) until all nodes have been added to tree.\"\n",
|
||||
" tree = []\n",
|
||||
"def edge(A, B) -> Edge: return Edge(sorted([A, B]))\n",
|
||||
"\n",
|
||||
"def random_tree(nodes, neighbors, pop=deque.pop) -> Tree:\n",
|
||||
" \"\"\"Repeat: pop a node and add edge(node, nbr) until all nodes have been added to tree.\"\"\"\n",
|
||||
" tree = Tree()\n",
|
||||
" nodes = set(nodes)\n",
|
||||
" root = nodes.pop()\n",
|
||||
" frontier = deque([root])\n",
|
||||
" while nodes:\n",
|
||||
@@ -106,7 +112,7 @@
|
||||
" nbrs = neighbors(node) & nodes\n",
|
||||
" if nbrs:\n",
|
||||
" nbr = random.choice(list(nbrs))\n",
|
||||
" tree.append(Edge(node, nbr))\n",
|
||||
" tree.add(edge(node, nbr))\n",
|
||||
" nodes.remove(nbr)\n",
|
||||
" frontier.extend([node, nbr])\n",
|
||||
" return tree"
|
||||
@@ -116,52 +122,56 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Implementing Random Mazes\n",
|
||||
"# Making Random Mazes\n",
|
||||
"\n",
|
||||
"Now let's use `random_tree` to implement `random_maze`. Some more choices:\n",
|
||||
"Now let's use `random_tree` to implement `random_maze`. Basically, we just create a collection of `(x, y)` squares, pass these to `random_tree`, and let it do the work. Note that:\n",
|
||||
"\n",
|
||||
"* A `Maze` is a named tuple with three fields: the `width` and `height` of the grid, and a list of `edges` between squares. \n",
|
||||
"* A `Maze` is a named tuple with three fields: the `width` and `height` of the grid, and a set of `edges` between squares. \n",
|
||||
"* A square is denoted by an `(x, y)` tuple of integer coordinates.\n",
|
||||
"* The function `neighbors4` gives the four surrounding squares."
|
||||
"* The function `neighbors4(square)` gives the four surrounding squares."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"collapsed": true
|
||||
},
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"Maze = namedtuple('Maze', 'width, height, edges')\n",
|
||||
"\n",
|
||||
"def neighbors4(square):\n",
|
||||
" \"The 4 neighbors of an (x, y) square.\"\n",
|
||||
"Square = tuple\n",
|
||||
"\n",
|
||||
"def neighbors4(square) -> {Square}:\n",
|
||||
" \"\"\"The 4 neighbors of an (x, y) square.\"\"\"\n",
|
||||
" (x, y) = square\n",
|
||||
" return {(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)}\n",
|
||||
"\n",
|
||||
"def squares(width, height): \n",
|
||||
" \"All squares in a grid of these dimensions.\"\n",
|
||||
"def grid(width, height) -> {Square}: \n",
|
||||
" \"\"\"All squares in a grid of these dimensions.\"\"\"\n",
|
||||
" return {(x, y) for x in range(width) for y in range(height)}\n",
|
||||
"\n",
|
||||
"def random_maze(width, height, pop=deque.pop):\n",
|
||||
" \"Use random_tree to generate a random maze.\"\n",
|
||||
" nodes = squares(width, height)\n",
|
||||
" tree = random_tree(nodes, neighbors4, pop)\n",
|
||||
"def random_maze(width, height, pop=deque.pop) -> Maze:\n",
|
||||
" \"\"\"Generate a random maze, using random_tree.\"\"\"\n",
|
||||
" tree = random_tree(grid(width, height), neighbors4, pop)\n",
|
||||
" return Maze(width, height, tree)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"I'll make a 10x5 maze, as in the diagram at the top of this notebook:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Maze(width=10, height=5, edges=[((6, 3), (7, 3)), ((6, 3), (6, 4)), ((6, 4), (7, 4)), ((7, 4), (8, 4)), ((8, 3), (8, 4)), ((8, 2), (8, 3)), ((7, 2), (8, 2)), ((7, 1), (7, 2)), ((7, 0), (7, 1)), ((7, 0), (8, 0)), ((8, 0), (8, 1)), ((8, 1), (9, 1)), ((9, 0), (9, 1)), ((9, 1), (9, 2)), ((9, 2), (9, 3)), ((9, 3), (9, 4)), ((6, 0), (7, 0)), ((5, 0), (6, 0)), ((5, 0), (5, 1)), ((5, 1), (6, 1)), ((6, 1), (6, 2)), ((5, 2), (6, 2)), ((4, 2), (5, 2)), ((3, 2), (4, 2)), ((3, 2), (3, 3)), ((2, 3), (3, 3)), ((2, 2), (2, 3)), ((2, 1), (2, 2)), ((2, 0), (2, 1)), ((1, 0), (2, 0)), ((0, 0), (1, 0)), ((0, 0), (0, 1)), ((0, 1), (1, 1)), ((1, 1), (1, 2)), ((0, 2), (1, 2)), ((0, 2), (0, 3)), ((0, 3), (1, 3)), ((1, 3), (1, 4)), ((0, 4), (1, 4)), ((1, 4), (2, 4)), ((2, 4), (3, 4)), ((3, 4), (4, 4)), ((4, 3), (4, 4)), ((4, 3), (5, 3)), ((5, 3), (5, 4)), ((2, 0), (3, 0)), ((3, 0), (4, 0)), ((4, 0), (4, 1)), ((3, 1), (4, 1))])"
|
||||
"Maze(width=10, height=5, edges={((2, 1), (3, 1)), ((5, 0), (5, 1)), ((8, 3), (8, 4)), ((6, 2), (6, 3)), ((6, 4), (7, 4)), ((8, 0), (8, 1)), ((5, 3), (6, 3)), ((5, 4), (6, 4)), ((0, 2), (1, 2)), ((1, 0), (2, 0)), ((4, 3), (4, 4)), ((0, 2), (0, 3)), ((2, 0), (2, 1)), ((2, 1), (2, 2)), ((7, 0), (7, 1)), ((7, 4), (8, 4)), ((6, 0), (7, 0)), ((8, 0), (9, 0)), ((6, 1), (6, 2)), ((0, 3), (1, 3)), ((2, 3), (2, 4)), ((7, 1), (7, 2)), ((0, 4), (1, 4)), ((3, 2), (3, 3)), ((8, 1), (9, 1)), ((9, 1), (9, 2)), ((4, 2), (5, 2)), ((7, 3), (8, 3)), ((4, 0), (4, 1)), ((2, 4), (3, 4)), ((1, 0), (1, 1)), ((3, 1), (4, 1)), ((1, 1), (1, 2)), ((3, 3), (3, 4)), ((0, 0), (1, 0)), ((5, 1), (5, 2)), ((5, 0), (6, 0)), ((3, 4), (4, 4)), ((0, 0), (0, 1)), ((0, 3), (0, 4)), ((9, 2), (9, 3)), ((1, 3), (2, 3)), ((9, 3), (9, 4)), ((3, 0), (4, 0)), ((5, 3), (5, 4)), ((7, 2), (8, 2)), ((3, 2), (4, 2)), ((6, 0), (6, 1)), ((8, 2), (9, 2))})"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
@@ -170,7 +180,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"random_maze(10,5)"
|
||||
"random_maze(10, 5)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -179,89 +189,40 @@
|
||||
"source": [
|
||||
"That's not very pretty to look at. I'm going to need a way to visualize a maze.\n",
|
||||
"\n",
|
||||
"# Printing a maze"
|
||||
"# Plotting a maze\n",
|
||||
"\n",
|
||||
"I will use `matplotlib` to plot the walls of a maze. I'm going to look ahead to when we have a *solution path,* and allow that to be plotted as well."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"o o--o--o--o--o--o--o--o--o--o\n",
|
||||
"| | | |\n",
|
||||
"o--o--o--o o--o o o o--o o\n",
|
||||
"| | | | |\n",
|
||||
"o o o--o--o o--o--o--o--o o\n",
|
||||
"| | | | | | |\n",
|
||||
"o o--o o o--o o o--o o--o\n",
|
||||
"| | | | | | |\n",
|
||||
"o o o--o--o--o--o o o--o o\n",
|
||||
"| | |\n",
|
||||
"o--o--o--o--o--o--o--o--o--o o\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def print_maze(maze, dot='o', lin='--', bar='|', sp=' '):\n",
|
||||
" \"Print maze in ASCII.\"\n",
|
||||
" exit = Edge((maze.width-1, maze.height-1), (maze.width-1, maze.height))\n",
|
||||
" edges = set(maze.edges) | {exit}\n",
|
||||
" print(dot + sp + lin.join(dot * maze.width)) # Top line, including entrance\n",
|
||||
" def vert_wall(x, y): return (' ' if Edge((x, y), (x+1, y)) in edges else bar)\n",
|
||||
" def horz_wall(x, y): return (sp if Edge((x, y), (x, y+1)) in edges else lin)\n",
|
||||
" for y in range(maze.height):\n",
|
||||
" print(bar + cat(sp + vert_wall(x, y) for x in range(maze.width)))\n",
|
||||
" print(dot + cat(horz_wall(x, y) + dot for x in range(maze.width)))\n",
|
||||
" \n",
|
||||
"cat = ''.join\n",
|
||||
" \n",
|
||||
"print_maze(random_maze(10, 5))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"*Much better!* But can I do better still?\n",
|
||||
"\n",
|
||||
"# Plotting a maze\n",
|
||||
"\n",
|
||||
"I'll use `matplotlib` to plot lines where the edges aren't:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%matplotlib inline\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"\n",
|
||||
"def plot_maze(maze):\n",
|
||||
" \"Plot a maze by drawing lines between adjacent squares, except for pairs in maze.edges\"\n",
|
||||
" plt.figure(figsize=(8, 4))\n",
|
||||
"def plot_maze(maze, figsize=None, path=None):\n",
|
||||
" \"\"\"Plot a maze by drawing lines between adjacent squares, except for pairs in maze.edges\"\"\"\n",
|
||||
" w, h = maze.width, maze.height\n",
|
||||
" plt.figure(figsize=figsize or (w/5, h/5))\n",
|
||||
" plt.axis('off')\n",
|
||||
" plt.gca().invert_yaxis()\n",
|
||||
" w, h = maze.width, maze.height\n",
|
||||
" exits = {Edge((0, 0), (0, -1)), Edge((w-1, h-1), (w-1, h))}\n",
|
||||
" edges = set(maze.edges) | exits\n",
|
||||
" for sq in squares(w, h):\n",
|
||||
" exits = {edge((0, 0), (0, -1)), edge((w-1, h-1), (w-1, h))}\n",
|
||||
" edges = maze.edges | exits\n",
|
||||
" for sq in grid(w, h):\n",
|
||||
" for nbr in neighbors4(sq):\n",
|
||||
" if Edge(sq, nbr) not in edges:\n",
|
||||
" if edge(sq, nbr) not in edges:\n",
|
||||
" plot_wall(sq, nbr)\n",
|
||||
" plt.show()\n",
|
||||
" if path: # Plot the solution (or any path) as a red line through the maze\n",
|
||||
" X, Y = transpose((x + 0.5, y + 0.5) for (x, y) in path)\n",
|
||||
" plt.plot(X, Y, 'r-', linewidth=2)\n",
|
||||
" \n",
|
||||
"def transpose(matrix): return list(zip(*matrix))\n",
|
||||
"\n",
|
||||
"def plot_wall(s1, s2):\n",
|
||||
" \"Plot a thick black line between squares s1 and s2.\"\n",
|
||||
" \"\"\"Plot a wall: a black line between squares s1 and s2.\"\"\"\n",
|
||||
" (x1, y1), (x2, y2) = s1, s2\n",
|
||||
" if x1 == x2: # horizontal wall\n",
|
||||
" y = max(y1, y2)\n",
|
||||
@@ -269,164 +230,327 @@
|
||||
" else: # vertical wall\n",
|
||||
" x = max(x1, x2)\n",
|
||||
" X, Y = [x, x], [y1, y1+1]\n",
|
||||
" plt.plot(X, Y, 'k-', linewidth=4.0)"
|
||||
" plt.plot(X, Y, 'k-', linewidth=2)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's compare the two visualization functions:"
|
||||
"Let's see what it looks like:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAASUAAACWCAYAAACYTp4YAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAACuklEQVR4nO3dQU7cQBRFUTvK/rdcGSFlQCSi2MWtn3MWALS7uTxG/15rXQAVP777BwD4nSgBKaIEpIgSkCJKQIooASmiBKSIEpAiSkCKKAEpogSk/Hzyi933va7rutZa95Nf97PvAbQ89XtvKQEpjy6lHd5cYRPtWK+7THotkzz934ulBKSIEpAiSkCKKAEpogSkiBKQIkpAiigBKaIEpIgSkCJKQIooASmiBKSIEpAiSkCKKAEpogSkiBKQIkpAiigBKaIEpIgSkHLciaWJxygnnAzaef5o0mdgx+HW0z5flhKQctxS+nBa/fl3k97zSWvvaZYSkCJKQIooASmiBKSIEpAiSkCKKAEpogSkiBKQIkpAiigBKaIEpIgSkCJKQIooASmiBKSIEpAiSkCKKAEpogSkiBKQcuw1E9cgmqa8L5Mup5zGUgJSjl1K/pJ9ze7l4n35Gs/pzywlIEWUgBRRAlJECUgRJSBFlIAUUQJSRAlIESUgRZSAFFECUkQJSBElIEWUgBRRAlJECUgRJSBFlIAUUQJSRAlIESUgRZSAlGNPLL1t52miSed2phyj5PtYSkDKcUtp96qYtGLe5Dn9nY9F+eZzO3W1WkpAiigBKaIEpIgSkCJKQIooASmiBKSIEpAiSkCKKAEpogSkiBKQIkpAiigBKaIEpIgSkCJKQIooASmiBKSIEpAiSkCKKAEpx51YOvVszGd2vpZJz43ZLCUg5biltOvo4Y5l4YAjbzr182UpASmiBKSIEpAiSkCKKAEpogSkiBKQIkpAiigBKaIEpIgSkCJKQIooASmiBKSIEpAiSkCKKAEpogSkiBKQIkpAiigBKaIEpLxyYmnS4cNJr4X/02mnliwlIOVeyxAAOiwlIEWUgBRRAlJECUgRJSBFlIAUUQJSRAlIESUgRZSAFFECUn4BAo5mOYT9KpYAAAAASUVORK5CYII=\n",
|
||||
"text/plain": [
|
||||
"<Figure size 360x180 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"M = random_maze(10, 5)\n",
|
||||
"plot_maze(M, figsize=(5, 2.5)) "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Solving Mazes\n",
|
||||
"\n",
|
||||
"Now it is time to show how to solve a maze. I'll use breadth-first search, which guarantees that the solution will be the shortest possible (although for mazes with only one solution, the guarantee doesn't matter). The function `breadth_first_search` maintains a `frontier` of unexplored squares, and on each iteration removes a square from the frontier that is at the shallowest path depth, and adds to the frontier all the neighbors of that square that are not blocked by walls and have not been seen previously. The dictionary of `{square: [square,...]}` called `paths` has two purposes: it prevents us from creating loops in a path, and at the end it tells us the path from start to goal, e.g. `[(0, 0), (0, 1), (1, 1), (2, 1), ...]`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAd0AAAD8CAYAAAAyun5JAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAABNxJREFUeJzt2sFuIjEUAMF4xf//svcaJSsIAdr2UnXj9uSZUct6jDnn\nBwDwen9WDwAA70J0ASAiugAQEV0AiIguAEREFwAiogsAEdEFgIjoAkBEdAEgIroAEBFdAIiILgBE\nRBcAIqILABHRBYCI6AJA5LJ6gGvGGPPz7znnWDXLv3ydD4Cz1F1x0wWAiOgCQER0ASCy9U53d7vt\nmHlfu///AXax+r84broAEBFdAIiILgBERBcAIqILABHRBYCI6AJARHQBICK6ABARXQCIiC4AREQX\nACKiCwAR0QWAiOgCQER0ASAiugAQEV0AiIguAEREFwAiogsAEdEFgIjoAkBEdAEgIroAEBFdAIiI\nLgBERBcAIqILABHRBYCI6AJARHQBICK6ABARXQCIiC4ARC6rBzjZGGOunuFec86xeoavTjvHHc9w\nd6c9Y57Dt/Kdmy4AREQXACKiCwARO90n2nF/ccIubcdz++yEMzzN7s+c3/Gt3OamCwAR0QWAiOgC\nQER0ASAiugAQEV0AiIguAEREFwAiogsAEdEFgIjoAkBEdAEgIroAEBFdAIiILgBERBcAIqILABHR\nBYCI6AJARHQBICK6ABARXQCIiC4AREQXACKiCwAR0QWAiOgCQER0ASAiugAQEV0AiIguAEREFwAi\nogsAEdEFgMhl9QC0xhhz9Qy3zDnH6hl4Le/h75xwblznpgsAEdEFgIjoAkDETvfN7Lin4v14D5/D\nOZ7HTRcAIqILABHRBYCI6AJARHQBICK6ABARXQCIiC4AREQXACKiCwAR0QWAiOgCQER0ASAiugAQ\nEV0AiIguAEREFwAiogsAEdEFgIjoAkBEdAEgIroAEBFdAIiILgBERBcAIqILABHRBYCI6AJARHQB\nICK6ABARXQCIiC4AREQXACKiCwAR0QWAyGX1ADDGmKtnuMdp8/IznuvjnOFtbroAEBFdAIiILgBE\n7HQfMOccq2f4HzjHx9mlPZ/38nE7nuHqb8VNFwAiogsAEdEFgIjoAkBEdAEgIroAEBFdAIiILgBE\nRBcAIqILABHRBYCI6AJARHQBICK6ABARXQCIiC4AREQXACKiCwAR0QWAiOgCQER0ASAiugAQEV0A\niIguAEREFwAiogsAEdEFgIjoAkBEdAEgIroAEBFdAIiILgBERBcAIqILAJHL6gHuMcaYq2e4Zs45\nVs9wy+5neIITnjOP863wCm66ABARXQCIiC4ARI7a6dql3c+ZPe6E3d7uz3n3+T4+zpiR87npAkBE\ndAEgIroAEBFdAIiILgBERBcAIqILABHRBYCI6AJARHQBICK6ABARXQCIiC4AREQXACKiCwAR0QWA\niOgCQER0ASAiugAQEV0AiIguAEREFwAiogsAEdEFgIjoAkBEdAEgIroAEBFdAIiILgBERBcAIqIL\nABHRBYCI6AJARHQBICK6ABC5rB7gHmOMuXoG8B7Cz8w5x+oZduOmCwAR0QWAiOgCQGTMaT0FAAU3\nXQCIiC4AREQXACKiCwAR0QWAiOgCQER0ASAiugAQEV0AiIguAEREFwAiogsAEdEFgIjoAkBEdAEg\nIroAEBFdAIiILgBERBcAIqILABHRBYCI6AJARHQBICK6ABARXQCIiC4AREQXACKiCwAR0QWAiOgC\nQER0ASDyFyUScgM3r2rXAAAAAElFTkSuQmCC\n",
|
||||
"text/plain": [
|
||||
"<matplotlib.figure.Figure at 0x10dd4dd68>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"o o--o--o--o--o--o--o--o--o--o\n",
|
||||
"| | |\n",
|
||||
"o o--o o--o o o--o--o--o o\n",
|
||||
"| | | | | | | |\n",
|
||||
"o o o--o o o o--o o--o o\n",
|
||||
"| | | | | | | |\n",
|
||||
"o o o o--o--o o o--o o--o\n",
|
||||
"| | | | | | |\n",
|
||||
"o--o o--o--o o o o--o--o o\n",
|
||||
"| | |\n",
|
||||
"o--o--o--o--o--o--o--o--o--o o\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"M = random_maze(10, 5)\n",
|
||||
"plot_maze(M) \n",
|
||||
"print_maze(M)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# `pop` strategies\n",
|
||||
"\n",
|
||||
"Now I want to compare how the maze varies based on theree different choices for the `pop` parameter. \n",
|
||||
"\n",
|
||||
"(1) The default is `deque.pop`\n",
|
||||
"which means that the tree is created **depth-first**; we always select the `node` at the end of the `frontier`, so the tree follows a single branch along a randomly-twisted path until the path doubles back on itself and there are no more neighbors; at that point we select the most recent square for which there are neighbors:"
|
||||
"def breadth_first_search(maze):\n",
|
||||
" \"\"\"Find a shortest sequence of states from start to the goal.\"\"\"\n",
|
||||
" start = (0, 0)\n",
|
||||
" goal = (maze.width - 1, maze.height - 1)\n",
|
||||
" frontier = deque([start]) # A queue of states to consider\n",
|
||||
" paths = {start: [start]} # start has a one-square path\n",
|
||||
" while frontier:\n",
|
||||
" s = frontier.popleft()\n",
|
||||
" if s == goal:\n",
|
||||
" return paths[s]\n",
|
||||
" for s2 in neighbors4(s):\n",
|
||||
" if s2 not in paths and edge(s, s2) in maze.edges:\n",
|
||||
" frontier.append(s2)\n",
|
||||
" paths[s2] = paths.get(s, []) + [s2]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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\n",
|
||||
"text/plain": [
|
||||
"<matplotlib.figure.Figure at 0x1176e5e80>"
|
||||
"[(0, 0),\n",
|
||||
" (0, 1),\n",
|
||||
" (0, 2),\n",
|
||||
" (0, 3),\n",
|
||||
" (1, 3),\n",
|
||||
" (1, 2),\n",
|
||||
" (2, 2),\n",
|
||||
" (3, 2),\n",
|
||||
" (3, 3),\n",
|
||||
" (2, 3),\n",
|
||||
" (2, 4),\n",
|
||||
" (3, 4),\n",
|
||||
" (4, 4),\n",
|
||||
" (5, 4),\n",
|
||||
" (6, 4),\n",
|
||||
" (7, 4),\n",
|
||||
" (8, 4),\n",
|
||||
" (9, 4)]"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"plot_maze(random_maze(40, 20, deque.pop))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The maze with `deque.pop` looks pretty good. Reminds me of those [cyber brain](https://www.vectorstock.com/royalty-free-vector/cyber-brain-vector-3071965) images.\n",
|
||||
"\n",
|
||||
"(2) An alternative is `queue.popleft`, which creates the maze roughly **breadth-first**—we start at some root square , add an edge to it, and from then on we always select first a parent edge before we select a child edge. The net result is a design that appears to radiate out in concentric layers from the root (which is chosen by `random_tree` and is not necessarily the top-left square; below it looks like the root is in the upper-left quadrant). "
|
||||
"solution = breadth_first_search(M)\n",
|
||||
"solution"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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\n",
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAASUAAACWCAYAAACYTp4YAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAADQ0lEQVR4nO3dwW7TQBRAURvxdfC99PfMhlRdtFJCnPGd6Tl7mthOLw+U0duP49gAKn5c/QYAPhIlIEWUgBRRAlJECUgRJSBFlIAUUQJSRAlIESUgRZSAlJ9n/rB9349t27bjOPYzf+5nrwG0nPV7b1ICUk6dlEZ45RS2ohHT6ygrXctKzv7Xi0kJSBElIEWUgBRRAlJECUgRJSBFlIAUUQJSRAlIuT5K+35szrMB/1wfJYAPRAlIESUgRZSAFFECUkQJSBElIEWUgBRRAlJECUgRJSBFlICU6VYsrbiMcoWVQSPXH630GRixuHW2z5dJCUiZblK6ma3+PG+lZ77StHc2kxKQIkpAiigBKaIEpIgSkCJKQIooASmiBKSIEpAiSkCKKAEpogSkiBKQIkpAiigBKaIEpIgSkCJKQIooASmiBKSIEpAy7TYT2yCaVnkuK21OmY1JCUiZdlLyN9l9Rk8unst93KevmZSAFFECUkQJSBElIEWUgBRRAlJECUgRJSBFlIAUUQJSpj1m8pB9/7Nt26+r38aJ3rbj+H31m4BX2I/jvKNRt3NWD53ruZ3NuvPPPPUaK3nl/foPo16H+8367L/HpHSzwi/MioGFD/yfEpAiSkCKKAEpogSkiBKQIkpAiigBKaIEpIgSkPK9vtH9gJGriVY6mrHKMkquY1ICUqablEZPFStNMa/kPj1mxGHZWadWkxKQIkpAiigBKaIEpIgSkCJKQIooASmiBKSIEpAiSkBK55jJpF+Jf7fewku4RGFSelvkdUYGadQ9g+Gu35Ab9fC1PLjpd5SVnslKRh7InW1DbmFSAngnSkCKKAEpogSkiBKQIkpAiigBKaIEpIgSkNI5+3anWdfGfGbktax031ibSQlImW5SGnWGa8Rk4TwarzTr58ukBKSIEpAiSkCKKAEpogSkiBKQIkpAiigBKaIEpEz3je48Z8xYwYXfBjcpnccuNjiBvW9fWOla4JXsfQOWJkpAiigBKaIEpIgSkCJKQIooASmiBKSIEpAiSkDKSw7krrT4cKVr4Xua7aiUSQlIOfVALsCzTEpAiigBKaIEpIgSkCJKQIooASmiBKSIEpAiSkCKKAEpogSk/AUI/MVbhV/bNgAAAABJRU5ErkJggg==\n",
|
||||
"text/plain": [
|
||||
"<matplotlib.figure.Figure at 0x1176fcf60>"
|
||||
"<Figure size 360x180 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"plot_maze(random_maze(40, 20, deque.popleft))"
|
||||
"plot_maze(M, figsize=(5, 2.5), path=solution)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The `deque.popleft` maze is interesting as a design, but to me it doesn't work well as a maze.\n",
|
||||
"# Strategies for the `pop` parameter of `random_maze`\n",
|
||||
"\n",
|
||||
"(3) We can select a cell at random by shuffling the frontier before popping an element off of it:"
|
||||
"Now I want to compare how the maze varies based on three different choices for the `pop` parameter. \n",
|
||||
"\n",
|
||||
"# `pop=deque.pop`\n",
|
||||
"\n",
|
||||
"The default pop method, `deque.pop`, means that the tree is created **depth-first**; we always select the `node` at the **end** of the `frontier`, so the tree follows a single branch along a randomly-twisted path until the path doubles back on itself and there are no more neighbors. At that point we select the most recent square for which there are neighbors and continue from there. The maze with `deque.pop` looks pretty good. Reminds me of those [cyber brain](https://www.vectorstock.com/royalty-free-vector/cyber-brain-vector-3071965) images. I'll `show` the maze without and then with the solution path."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def show(pop):\n",
|
||||
" \"\"\"Using this `pop` parameter, show a 70x70 maze, first without and then with the solution path.\"\"\"\n",
|
||||
" M = random_maze(70, 70, pop)\n",
|
||||
" plot_maze(M)\n",
|
||||
" plt.show()\n",
|
||||
" solution = breadth_first_search(M)\n",
|
||||
" plot_maze(M, path=[(0, -1)] + solution + [(M.width - 1, M.height)])\n",
|
||||
" return len(solution)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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\nxnjoO1WPSQgAAAAASUVORK5CYII=\n",
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAxsAAAMHCAYAAABRykopAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nO3d0Y7kthIlwPJi//+XvU8FNO56usSaPMokFfE8FimKUnWSBs8///777wsAAKDa/+nuAAAAcCbFBgAAEKHYAAAAIhQbAABAhGIDAACIUGwAAAARig0AACBCsQEAAEQoNgAAgAjFBgAAEKHYAAAAIhQbAABAhGIDAACIUGwAAAARig0AACBCsQEAAEQoNgAAgAjFBgAAEKHYAAAAIhQbAABAhGIDAACIUGwAAAARig0AACBCsQEAAEQoNgAAgAjFBgAAEKHYAAAAIhQbAABAhGIDAACIUGwAAAARig0AACBCsQEAAEQoNgAAgAjFBgAAEKHYAAAAIhQbAABAhGIDAACIUGwAAAARig0AACDi/3Z34Kp//vnn39fr9fr333//ufLvAADgZJ/+Lp7AzgYAABCxzc7GVVcrvKs7JTuovpcdxqarj09rd8UT5+EOpo9jZ/+mj81JfB/+bPq9JPr3xPmQuucd2NkAAAAiFBsAAECEYgMAAIhQbAAAABGKDQAAIEKxAQAARCg2AACAiONyNk48x7jrHOrqe7+a/p44y/uqrnnTec9XnTIPr7Q94b2/YuWZXL2n6rG5e9588x3pGptqlWOzOt5VuvIXks9u+r08KT9jwu9j1fdmJ3Y2AACAiON2Nq5arRg70227TF9JWFF1L13z5pvr3P38Uu9I5+rOCatzJz6XKtW7a6vX842d0943K9PT34HO35+7r7nD33QT2u5iZwMAAIhQbAAAABGKDQAAIEKxAQAARCg2AACACMUGAAAQodgAAAAijs3ZuPvc8c7Ezi67nDV+RWq+TDj//k9W+zb1Xjr71Z2WO/WZvF7986sji6Q7TTsxH6quuct8SKRpp/793ddLtD31W7fD2O3EzgYAABBx7M7G1PTkCcm/d7e7w85HV/LvVYl2u55LatVmh3lWZYd7re7jTrvHU9O0J/z+nPDtvKrrHUi127nrM/1vuo7f5p12QOxsAAAAEYoNAAAgQrEBAABEKDYAAIAIxQYAABCh2AAAACIUGwAAQMSxORufTEhmndjmN+3ucNbz3eeTp84Qrzwnv7qP3fOgu/1KJ93LVZ25HHf/DqS+sTt8Rz6Znr/wt//Nb6b//iTarm63+h3o/Btxhzylq+xsAAAAEcftbExPv56Q4Dq1/eTYTE1jT+y4dK3YTB3jpNTYdL/7v+lMZL6i811ZNTUVeeWa31y74nqd794p78ATV+NP+d3bjZ0NAAAgQrEBAABEKDYAAIAIxQYAABCh2AAAACIUGwAAQIRiAwAAiNguZ6MrrfTqmd+TE8mrTUjJrtL9PE7K26jW+Ww604nvvN5PU+959brJTIkqO2TidKWmd1+3Qvc9d2RAdD+PU373Xq+zMjzsbAAAABHb7WxMTrVeccKKw4TVtqpx7FrlSLQ7PSF1evru67XPez95DFfnYee9TB7HFYn7mDpnO+dXIpW80kmr8dX/B0Xq/8i4YsK3roudDQAAIEKxAQAARCg2AACACMUGAAAQodgAAAAiFBsAAECEYgMAAIjYLmfjkyenWnedbz0hb2NXyeT56dkhn+yQkl0tcc+npBh3vuvd35nJY7h6zSd+b6rb7vp+db8HEyWyM04cZzsbAABAxHE7G12r7Ceuwt6dxJlYtT9FYrdn+k7Yt//+N7usGFW/e1euWW2H+bBq6hhelez/7mPz1jG/Ov/vgI7rJZzYx8TvQBc7GwAAQIRiAwAAiFBsAAAAEYoNAAAgQrEBAABEKDYAAIAIxQYAABCxXc7GDqnWle3efa0JTrufK3ZIV5+QOfNbezucsz7Z1XHsToyufFe672Xy70o1Kdl1ut4p/swY/s7OBgAAELHdzsbuyZg7JEF3Xe+bselKrZ085qs7EFUr2V3tnmSH3ZnuFedE+zuM+2+q370Vp/zfBp1z4KTfs6ftND/lPv+WnQ0AACBCsQEAAEQoNgAAgAjFBgAAEKHYAAAAIhQbAABAhGIDAACI2C5n45NUIuzkM8K78wgmp+quZjp0PefEM+xKJ3bu+J/tMIZ3v8/V5/In3qVdvrGdeQnTv4mdz7Brble3e9LfLVedMG8msLMBAABEHLez8cTV3FRy80lVdZXUmExO9O1aiTxp/nXucK2avMOwonL+TJ+LJ66+Tl6VX237k9W+Xf0Nr243cc2p75a/l2rZ2QAAACIUGwAAQIRiAwAAiFBsAAAAEYoNAAAgQrEBAABEHHf0bSr0pvNowanH73YfbZnoQ9VYp/q18ownPJ+/sXv/f9rpXqZ+bxK6n8vd35FvTOjDf5kQBjn974cTghFXj6C9avJRyCeyswEAAEQct7NRvdKQCgnsCHGrVh0mOEF1Hzvu+ZTnklg17L7X7vZ/M7lvr9eMALJPqldhTwqL7ZIImdvh+U1ov0PVPSfH7pTg1BV2NgAAgAjFBgAAEKHYAAAAIhQbAABAhGIDAACIUGwAAAARig0AACDiuJyN1XOHq88pTuRndJ2VffdZ4okE1+kJqsn5Ov0s7+r51XnmeFXbJ9zDlPYT95PK5bjbE78jJ7U9/ZlMtsM9d//tl2BnAwAAiDhuZ+OtuvI/ocJMJKkmdCQFd62yJ1J6J+/WvV71KctviV3FO9tcuV5yde6k977KKe/UBFP7mNhZn26HlPiud2+nd2oHdjYAAIAIxQYAABCh2AAAACIUGwAAQIRiAwAAiFBsAAAAEYoNAAAgYrucjeqk3h2SOK9ee4fE7yrdabRdeRsJ0zMErjopFTlh95TzVD5Lh64Mmc68nqt2yOG5KjWGk39/up/L3fN2h+/NBHY2AACAiO12Nu5enehM3d4lubJzpbFqjFJj3fEMr7Y5fX4l7qN7pfGTZGptV9r5JzvsrE2fN8l3+ZTvxF3XSV6ze9dgctvd87S7/ensbAAAABGKDQAAIEKxAQAARCg2AACACMUGAAAQodgAAAAiFBsAAEDEdjkbn6yeQ119fnpnmuQJybrVpp+P3z1fO+2Qr3DV3Um9iXmQmovdWQNX7JB6P9X0b1iyvap3YDVfp+qeOuff9HmTcMI9fMvOBgAAEHHczsbb3StVyeTfq21f9cSky6kryhNW46fOh87+TU9hT6amP+ld6dqdSe32TH5Xrpq+G71il3vp+L8iuncTO75zd+9cTWJnAwAAiFBsAAAAEYoNAAAgQrEBAABEKDYAAIAIxQYAABCh2AAAACKOzdn4ZKez55+UKrqq+p53OTP+iql5CW+d/et6LtXvcmd2wPR3JZlFcvf1TnpXpuf6rDjpXk7pQ/W7UvmMu5LiJ7CzAQAARBy3s7FahU5OAO5OBq+6XmKMd1kZmJxa2/WunLRSmlrlPyEJenq73W1fMX1XccXk3bpJ7Vfo3EHtTga/25P/vllhZwMAAIhQbAAAABGKDQAAIEKxAQAARCg2AACACMUGAAAQodgAAAAijsvZmJ4E22n1XrryEFbanX4m+hNTkacnS6+Yet55or3UPdz9Duz0zZ78/UrlPE1/Ponfn92/xROe2Un5HbtkzlSyswEAAEQct7ORWmk4IU17+spA56p9tR1SkVPp19WurmRX3kfX96HzXelMHa5sN9X+FVfn4k5px9XjOPV7c9I7NWFHfequymq/Or8jJ7GzAQAARCg2AACACMUGAAAQodgAAAAiFBsAAECEYgMAAIhQbAAAABHb5WxMTV5Mnos8/czlqedpJ1XNw+Q9333G+w65HV19XG33pLyNqzra3SG7Y0XHN7QrHTt13c536mp73Rk2lW3ufi/ftDf9O5JgZwMAAIjYbmfj7pXiHdImu6RWECaP4fSE+pW2J4/zisR9dI1NYn6dkgy+w9hUt9t1vder/hvW9U1Mprrv/g2dMG+qdf2fFlfafdpv8092NgAAgAjFBgAAEKHYAAAAIhQbAABAhGIDAACIUGwAAAARig0AACBiu5yNajukTXaZnOy5S8psVT+/SVvtmms7vFPTx+aJmRIdZ9BPnwep651wjn8qgbpS9+/U9HyYFZ2/pdWemLdhZwMAAIg4bmejK/G7s1KtrtR3SgafmsQ7YUXi7gTe1etV+Wa3Z3pCeNf1rlwzNYZVCc/f9Mt82KvNFRP6d/fvSuc3+4Q5nWp3wlzsYmcDAACIUGwAAAARig0AACBCsQEAAEQoNgAAgAjFBgAAEHHc0bfdYUmJY2OnHmFaLdG/1WvuFLr4t1LHI3aFQXYGHk49Fjh1zc52O+7nlHs5aX5Naf+K6d+bzt/H075P09qcws4GAAAQcdzORmrVvmM34JQqeOpOyn+Z2teTQpWqgzc7x+aJAZhXr9M9b65YfX6nhH5Onl9d7U3+vxK67RRGfPeYn/j7k2BnAwAAiFBsAAAAEYoNAAAgQrEBAABEKDYAAIAIxQYAABCh2AAAACKOy9nYJY1ywpnxU9udnFLanasweWxS1+tsd+o5+d+cKb/TmexTnTKGHdkAq2M3NXX7p6t9rMwKSpj87ezK4+ier6/X3N+fb9jZAAAAIo7b2Xjrqgg7046nJrh2JXv+NDXNuGpl7L+u2ZWkOnUevl4z5uLdUqm1U1cuv2n7qifNm+5d3B1M/V3pbDfxm3bC9VZ073Ql2NkAAAAiFBsAAECEYgMAAIhQbAAAABGKDQAAIEKxAQAARCg2AACAiGNzNj7pPse4o/1UImZXwmx321euV50MnkiMvjvdNpXg2nFWfEpl+13jmMrjuGqH3I4uk5PnT8r3SH077/ZNu9PvvSOhfpfvQ4KdDQAAIOKxOxtvT0qC3iWdtGOFoDvVtiN5vivpddVJyfNV7T1Line truncated
|
||||
"text/plain": [
|
||||
"<matplotlib.figure.Figure at 0x117710550>"
|
||||
"<Figure size 1008x1008 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1179"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1008x1008 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def poprandom(seq):\n",
|
||||
" \"Shuffle a mutable sequence (deque or list) and then pop an element.\"\n",
|
||||
" random.shuffle(seq)\n",
|
||||
" return seq.pop()\n",
|
||||
"\n",
|
||||
"plot_maze(random_maze(40, 20, poprandom))"
|
||||
"show(deque.pop)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This is an interesting compromise: it has some structure, but still works nicely as a maze, in my opinion.\n",
|
||||
"# `pop=deque.popleft`\n",
|
||||
"\n",
|
||||
"This creates the maze roughly **breadth-first**—we start at some root square , add an edge to it, and from then on we always select first a parent edge before we select a child edge. The net result is a design that appears to radiate out in concentric layers from the root (which is chosen by `random_tree` and is not necessarily the top-left square; below it appears the root is in the lower-right quadrant). The `deque.popleft` maze is interesting as a design, but to me it doesn't work well as a maze. It is too easy to solve: follow the path from the start to the root, then consider the path from the end to the root, and see how they match up."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1008x1008 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"139"
|
||||
]
|
||||
},
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAxsAAAMHCAYAAABRykopAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nO3d3ZLcytEd0B6Hn458XvL12hcW/cmSqCnM1EZmFtaKODeKEYAq/DQT2V374/1+vwAAAHb7X9UHAAAAnEmxAQAARCg2AACACMUGAAAQodgAAAAiFBsAAECEYgMAAIhQbAAAABGKDQAAIEKxAQAARCg2AACACMUGAAAQodgAAAAiFBsAAECEYgMAAIhQbAAAABGKDQAAIEKxAQAARCg2AACACMUGAAAQodgAAAAiFBsAAECEYgMAAIhQbAAAABGKDQAAIEKxAQAARCg2AACACMUGAAAQodgAAAAiFBsAAECEYgMAAIhQbAAAABGKDQAAIEKxAQAARCg2AACACMUGAAAQodgAAAAiFBsAAECEYgMAAIiYXWx8fLxfHx/v6sMAAAD+3exiAwAAaEuxAQAARCg2AACACMUGAAAQ8b+rD2DVxz9+CP5+vz9W/g4AAE722b+LO9DZAAAAIsZ0Nq66u9Jb7bzAV7i+/i4xN+b7+3bPYeU5OeV6OGkOr37bYdffpbbZWeXcnPJ3yX1PoLMBAABEKDYAAIAIxQYAABCh2AAAACIUGwAAQIRiAwAAiFBsAAAAEcfmbOxef3h1nexJ6x7Th+vr7xJzY76/b/ccVp6TU66Hk+ZwejbFFd2vK/gunQ0AACDi2M7G7pTSu/aXdEqa6QSpBNAnnbvEvZd6y35CSvaq3dds5T3Q/fm++znylXvl7n0/+S1/1/v+yeeEPXQ2AACACMUGAAAQodgAAAAiFBsAAECEYgMAAIhQbAAAABGKDQAAIOLYnI0nrv190li6q153/wSVicNVz4cJ92jXtf7/qLwedts915W5Id0zS05w0vOfZ9HZAAAAIo7tbFQkrq6oTAg+6Y1S1TzufrN09fgrzmF1qnViv11TkZNvnHcneX9m91gSXaFTOpSVczOhW7di0ufj6r08aUycTWcDAACIUGwAAAARig0AACBCsQEAAEQoNgAAgAjFBgAAEHHs0rerJixB+DSVYVzdQ7YS103VvictG7tr392v1yu6Lw98UjDihLmZvszqEz+TnzhmauhsAAAAEY/vbNz9Nmb625+vSISKdZ/Hyrf2u+f7iW9prx7DXdv546S39qvbrexwrereuap8jkzR/bNlRSqQceXvT7se2ENnAwAAiFBsAAAAEYoNAAAgQrEBAABEKDYAAIAIxQYAABCh2AAAACIen7NhTejve2KGQPf05CuemAy+qupaPGnMT3w+rHri9XWC7v9u6H58PI/OBgAAEPHYzsZJb3auJr3u+rvdJpyTqkTfRLp61Xx3SAb/TCJZt0rV9dC9Q3LFKWPxxnuv7unqnZ9LPIvOBgAAEKHYAAAAIhQbAABAhGIDAACIUGwAAAARig0AACBCsQEAAEQcm7NRvb71Tt3XZJ+Qm9DdE+dwQm5C1XmZcD10z+W44pSxyFXgipM+T+hNZwMAAIg4trNx9xueCUnCq6pSsid4Yrp6KqG+wu6OQdUb8Q7Xw267xtThWXzSWLqaMCfdP0uvPg9X/l63hP9EZwMAAIhQbAAAABGKDQAAIEKxAQAARCg2AACACMUGAAAQodgAAAAijs3ZOCnR95R02wkmJDfvNmG9+hVXzolk8L+ruh4qU92756VMyF/Z7YQx776Pu28P/kZnAwAAiDi2s7E7wfWu/X1l36s6vwFK2Z3Am3oDunO7T0sdvjLOq/dU1+dI8rq5W+Xb/ao09Mouzqrq58fdc5i0ayyr27uqwxxxNp0NAAAgQrEBAABEKDYAAIAIxQYAABCh2AAAACIUGwAAQMSxS992X1pwwlK5E3QPSZuwrGf3ZQ8nLD9ddZ4rg+tWdZ/DSlVjmXDdrDrpethF+B/d6GwAAAARx3U2uofdVIYvnRT2NiEk7YqdoU/duz2rJgSaVXdQV7a7+xiv3iN3B5pVXtepOdylQ+DhZ7rPYWLfqbFUHGf3zxVq6GwAAAARig0AACDijK9R9W7b/X693z+rDwIAAO42vbPxu/oAFvyoPgAAAKgwu7PRvWPQu+MCAABR0zsbAABAU4oNAAAgYtzXqLqv4XxSinGVypTs7knxkn977ntCinH3a3uC7tkTT7wOV+0c8ykJ3d3/LcA5dDYAAICIcZ2N1fTYqr/7zJW3Kye9zUqpOC8rUvutSIze7YnJ4LslnyNdE7pPSoyu/FypkroOK6/X3fu++3pIXDcTrkXup7MBAABEKDYAAIAIxcYdPj7em//7VT0kAAD4jGIjK5VwLpUcAID2xv1AfJREwrml6gAAGEJnAwAAiFBsAAAAEb5GtdmEbIDuKhOjV03IazgtY2TXfq/su/t1k9h3lQn3yqrKa3vFSc8b/t0p/xbgHDobAABAhM7GJh3etu1KXK0y4W3u7nOS2M7T3tpfmZsJXZfd+5+S+F2RBL071Xq36uvwCsnzeVXJ4InrX/flWXQ2AACACMUGAAAQodgAAAAiFBsAAECEYgMAAIhQbAAAABGKDQAAIELOxqIJCa6rTlrvvPt5qVxL/GnJ4Im8jVUTxrxb92OsTJSfkDlziu7X4U4TsikmHCP309kAAAAidDY+Ufmme/e+q1JFE2+eur+lqkxwfVoyeOU9tVv343u9Zhzj67X3XqnqfKx64tvkqufSFal7ZcI9OOEYuY/OBgAAEKHYAAAAIhQbAABAhGIDAACIUGwAAAARig0AACDi8UvfnrRcZ/eAqAnLM54y11d0X67zpCUuJ1w3E45xt+7X4oQ53O2EsL6qZ9KEz9oJx8g+OhsAAEDEYzsb3d/SprZZsd+rbwOvhHHd/VZr91g6vN2pOsaKMMhVu6+vCV3CCce4okM46K7tdng+3K1zp+KPq8fY/RlbOecTPiP5Pp0NAAAgQrEBAABEKDam+vh4b/zvV/VwAAA4j2Jjnt+Bbf4IbBMAgId77A/Ex3q/f27dnh9fAQAQorMBAABEKDYAAICIY79G1T0ZPJG3seqkFNwJa/nzPZWZEiddXxPu5xUnJcU/8TlywnX4tOyHp42X/XQ2AACAiGM7G3cnuCbelFanVXdOwU2lHXdNjD5Jh+T5XfteVZnUe8q1mLgeVlV+DnS3O017VYc57Hpv7X7GfvXvd+g6x1yjswEAAEQoNgAAgAjFBgAAEKHYAAAAIhQbAABAhGIDAACIUGwAAAARx+ZsVK2LXpm3sVt1JsFOJ6WmdzdhDquO8aS8hqr7vkOm0a7tTdD9s7Szqvuz8rnQIf+EfnQ2AACAiOM6G7vfhlS+ral6+zqhC7A7Nf2Et2h/VI2l+5vu7/5//pPqTkUiJbvr9dPhGXvSc+Iz3TsVk87F3cd69dztuve+cgy76Kj0prMBAABEKDYAAIAIxQb/18fHe+G/X9WHCQDAHIoNfl/42x+xowAA4DjH/UCci97vn0t/58dXAABcpLMBAABEKDYAAICIcV+jqlpLufua41f2vWpSbsKufe9Wma7ePdl9QoZN9/v+iSnZE54j3TMgKq+b3brP9Yrdczchc2LCMbKPzgYAABAxrrOxmnS5++92H99OT3uDfWWb3d++rpqQPL+qOnV7ZdsdjnHHfq/s+5Q32JXP2O6dj8R+d6dVr5rwZvzqWO/+90gHu/6tRm86GwAAQIRiAwAAiFBsAAAAEYoNAAAgQrEBAABEKDYAAICIcUvfPpGlTPvpPoeJfa86ZZnQ16t/WF/lEri7TbgHul/blc/O7nNToftnWffju+KksZxIZwMAAIjQ2dgk+RamawjShDdZ3YOkJgQe7g7AnBDO9sTQtSodjm96mNoTu3+T3P3svKrzuakORmQPnQ0AACBCsQEAAEQoNrjm4+O98N+v6sMEAKCeYoNVvy/87Y/YUQAAMIYfiLPm/f659Hd+fAUAwD/obAAAABGKDQAAIMLXqBZ1yIq4e98n5SaYw+/ve1X3uU6oPH+7dT8vnTMB/phw36+qOsYJ5/luVddB5fU34ZnJ53Q2AACACJ2NT0x449w91Xp3UnVy37u2V9lx2Z2aXjXmVZVzU93FmfD29+65vmL3/FVtL3E9VF/bT9L9GXt1/xX7fuJ1M4nOBgAAEKHYAAAAIhQbZEgaBwB4PMUGu0kaBwDg9Xr5gTi7SRoHAOAfdDYAAIAIxQYAABDx+K9RTVhLvHvC80np6qdkllzZ5m7dr9fENrtfN4l9rzrpGdv9nprwHJmQDXO3kzIiThoL++hsAAAAEY/vbFSlUu5Myd65zyv77dD52HX+dif1Xj13O7eXGsvd90ribXPqrer0RPIr+15V9Qa7ssN1db93P7+ukAx+vxO6PrvHcMKcoLMBAACEKDYAAIAIxQa11pLGpY0DAAyk2KDKlaTx10vaOADAOI//gThFVpPGXy9p4wAAQ+lsAAAAEYoNAAAgwteoDtI9H2NChkD3Nb07ZErs0j3FO7HvCWNZ1T1X5aSU7AkZFd2fNyeYcB1MOEbup7MBAABE6GxssvpWZ0LS64S3ubvnu/ubzRPfGlakJ09Ibl7Z7uo4ku5OTV/db2X3r7rbs3M8U+6VXbof3z/rfKyV1yx96WwAAAARig3mWA8AFBIIANCAYoMJrgYArhASCAAQ5jcb9HclAHCF1TIAAG6hswEAAEQoNgAAgAhfo1pUuTRqVUjOhCVwV1UtcXnSmKtMCINcVbnUavd7YLcJ95Tg1D5OCqOrHMvufZ90Xp5MZwMAAIh4bGcjFQpXETi1OxzniW/bVj1xzNUq3oRW3vcrKjujU4IyV/6++/nTQb1f5/FM+HfL1WO4e3unfT5OobMBAABEKDYAAIAIxQYAABCh2AAAACILine truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1008x1008 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"show(deque.popleft)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# `pop=poprandom`\n",
|
||||
"\n",
|
||||
"We can select a cell at random fromthe frontier.\n",
|
||||
"This is an interesting compromise: it has some structure, and looks rather nice as a maze, in my opinion. However, I have to say that I was surprised that the path is almost as straight and short as in `pop=popleft`; not nearly as twisty as in `pop=deque.pop`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def poprandom(seq):\n",
|
||||
" \"\"\"Select and return a random element; remove it from the sequence.\"\"\"\n",
|
||||
" element = random.choice(seq)\n",
|
||||
" seq.remove(element)\n",
|
||||
" return element"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1008x1008 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"143"
|
||||
]
|
||||
},
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1008x1008 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"show(poprandom)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"What other variations can you come up with to generate interesting mazes?"
|
||||
]
|
||||
}
|
||||
@@ -447,7 +571,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.6.0"
|
||||
"version": "3.7.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div align=\"right\" style=\"text-align: right\"><i>Peter Norvig<br>Aug 2019</i></div>\n",
|
||||
"\n",
|
||||
"# Photography Focal Length Charts\n",
|
||||
"\n",
|
||||
"Photographers with interchangable-lens cameras often obsess about which lenses they use. This is especially true when planning a trip, and trying to balance how much to pack versus what might be needed. One big factor is the **focal lengths** to be covered. This notebook will help understand what range of focal lengths was used in a collection of photos, to help you plan the next trip. Charts from this notebook appear in my [Galapagos Photography Trip Report](https://norvig.com/galapagos-photography.html).\n",
|
||||
"\n",
|
||||
"<p><a href=\"https://www.the-digital-picture.com/Photography-Tips/packing-for-landscape-photography-trip.aspx\"><img src=\"https://media.the-digital-picture.com/Images/Other/Lowepro-Photo-Trekker-AW-II-Backpack-Loaded.jpg\" width=\"300\"></a>\n",
|
||||
"\n",
|
||||
"# Preparing the Focal Length Data\n",
|
||||
"\n",
|
||||
"In your photo editing system, export (or copy down manually) a table of focal lengths of photos. (I did it only for my **best** photos.) Arrange the data in the following Python format:\n",
|
||||
"\n",
|
||||
" {trip_name: {\n",
|
||||
" lens_name: [focal_length, ...]}}\n",
|
||||
" \n",
|
||||
"For example:\n",
|
||||
" \n",
|
||||
" {2008: {\n",
|
||||
" '100-400': [100, 100, 100, 115, 130, ... 400, 400, 400, 400, 400, 400]}}\n",
|
||||
" \n",
|
||||
"For each of my photos I considered two variations of focal length:\n",
|
||||
"1. The actual focal length setting of the lens when the photo was taken (as recorded in the EXIF data).\n",
|
||||
"2. The [35mm equivalent focal length](https://en.wikipedia.org/wiki/35_mm_equivalent_focal_length), taking account both the crop factor of the sensor (if any), and any cropping done in photo editing. For example, if the focal length of the lens/photo was 200mm, but I was using a camera with a 1.5x crop, and then I further cropped the image down to half the size in both width and height, then the equivalent focal length would be 200mm x 1.5 x 2 = 600mm. \n",
|
||||
"\n",
|
||||
"I collected these two data sets at different times, and over time my definition of **best** changed, so they are not for the exact same photos.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"actual = { # Actual focal lengths \n",
|
||||
" 2008: {\n",
|
||||
" '17-40': (*[17]*9, *[40]*5),\n",
|
||||
" '24-105': (24, 24, 105),\n",
|
||||
" '100-400': (*[100]*3, 115, 130, 135, *[150]*3, 210, 220, 260, 300, 330, \n",
|
||||
" 350, 350, 360, 365, 365, 380, 380, *[400]*36)},\n",
|
||||
" 2014: {\n",
|
||||
" '16-35': (*[16]*7, 17, 26, *[35]*4),\n",
|
||||
" '24-70': (*[24]*3, 28, 28, 45, 47, 58, *[70]*3),\n",
|
||||
" '70-300': (70, 70, 116, 170, 170, 176, 200, 221, 244, 260, 277, 277, *[300]*24),\n",
|
||||
" '150-600': (150, 165, 173, 200, 250, 256, 300, 309, 309, 309, 309, 329, 350, \n",
|
||||
" 350, 350, 400, 400, 428, 450, 483, 483, 483, 483, 483, 483, \n",
|
||||
" 500, 500, 500, 500, 500, 552, 552, *[600]*25)},\n",
|
||||
" 2019: {\n",
|
||||
" '16-35': (*[35]*5, *[16]*3, 23, 24),\n",
|
||||
" '70-200': (*[200]*9, *[280]*5, 168, 170, 142, 168, 185, *[180]*5, 105, 190, 145, 155),\n",
|
||||
" '100-400': (*[400]*28, 278, 286, 300, 286, 371, 300, 300, 300, 349, 330, 371, 200, 164, \n",
|
||||
" 349, 397, 100),\n",
|
||||
" '400': (*[400]*6, 560, *[800]*3)}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"(150, 165, 173, 200, 250, 256, 300, 309, 309, 309, 309, 329, 350, 350, 350, 400, 400, 428, 450, 483, 483, 483, 483, 483, 483, 500, 500, 500, 500, 500, 552, 552)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(tuple(sorted((150, 150, 165, 173, 173, 182, 200, 250, 250, 256, 256, 300, 300, \n",
|
||||
" *[309]*9, 329, 329, *[350]*5, 375, *[400]*4, 428, 428, 450, 450, \n",
|
||||
" *[483]*11, *[500]*11, *[552]*4))[0::2]))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"equiv = { # 35mm equivalent focal lengths (after cropping)\n",
|
||||
" 2008: {\n",
|
||||
" '17-40': (75, 55, 27, 62, 17, 17, 17, 17, 17, 24, 17, 17, 17, 17),\n",
|
||||
" '24-105': (58, 105, 116),\n",
|
||||
" '100-400': (668, 400, 766, 549, 509, 130, 150, 548, 428, 588, 405, 247, 656, 858,\n",
|
||||
" 400, 159, 559, 398, 295, 427, 513, 554, 694, 317, 666, 562, 507, 491,\n",
|
||||
" 552, 861, 528, 252, 304, 100, 554, 400, 248, 400, 545, 542, 145, 533,\n",
|
||||
" 234, 538, 577, 667, 648, 400, 188, 407, 400, 400, 400, 400, 400, 400, 200)},\n",
|
||||
" 2014: {\n",
|
||||
" '16-35': (16, 26, 24, 45, 36, 25, 16, 35, 18, 16, 47, 16),\n",
|
||||
" '24-70': (24, 47, 49, 24),\n",
|
||||
" '70-300': (262, 270, 208, 1037, 675, 425, 382, 738, 428, 673, 70, 309, 215, 416,\n",
|
||||
" 221, 503, 462, 300, 551, 512, 471, 296, 423, 947, 758, 232, 373, 476,\n",
|
||||
" 656, 532, 664, 1004, 664, 537, 439),\n",
|
||||
" '150-600': (1406, 331, 254, 552, 393, 758, 934, 725, 935, 875, 727, 991, 751, 949,\n",
|
||||
" 804, 1172, 1304, 1037, 197, 472, 422, 468, 413, 892, 797, 965, 1191,\n",
|
||||
" 1061, 885, 167, 395, 1059, 946, 868, 1227, 453, 444, 592, 1468, 372,\n",
|
||||
" 1469, 970, 240, 1103, 689, 592)},\n",
|
||||
" 2019: {\n",
|
||||
" '16-35': (35, 26, 35, 35, 34, 16, 24, 16, 16),\n",
|
||||
" '70-200': (200, 200, 168, 170, 142, 207, 475, 168, 185, 180, 141, 280, 280, 385,\n",
|
||||
" 425, 254, 358, 262, 200, 190, 206, 187, 221, 212, 336, 242, 209, 477),\n",
|
||||
" '100-400': (438, 724, 583, 703, 278, 300, 286, 286, 400, 431, 371, 335, 481, 400, 400,\n",
|
||||
" 621, 525, 308, 400, 406, 427, 371, 606, 400, 400, 261, 476, 188, 588, 397),\n",
|
||||
" '400': (1414, 1381, 800, 836, 668, 491, 668, 491, 400, 400)}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Plotting Histograms\n",
|
||||
"\n",
|
||||
"I want to see the range of shots made with each lens, and I also want to see the overall number and range of shots. So I got the idea to make a plot where every shot gets a hashmark, where the x-axis of the hashmark is the focal length, and the y-axis is the cumulative number of shots for that lens, sorted in ascending focal-length order. In other words, a kind of cumulative step histogram. This would get messy if the lenses overlapped in focal length a lot, but for me they didn't. The function `shot_plots` does this. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%matplotlib inline\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"\n",
|
||||
"def shot_plots(table, label='35mm equivalent', hidden=None):\n",
|
||||
" \"\"\"Display and save to a file a chart of focal lengths.\"\"\"\n",
|
||||
" for name, trip in table.items():\n",
|
||||
" filename = f'shots-{name}-{label[:2]}.png'.lower()\n",
|
||||
" N = sum(len(trip[lens]) for lens in trip)\n",
|
||||
" plt.figure(figsize=(6.5, 4.5))\n",
|
||||
" plt.rcParams.update({'font.size': 12})\n",
|
||||
" plt.xlabel(f'Focal length ({label})')\n",
|
||||
" plt.ylabel('Cumulative number of shots')\n",
|
||||
" plt.title(f'{name} Best {N} shots ({label} focal length)')\n",
|
||||
" plt.grid(b=True, which='major', color='grey', linestyle=':')\n",
|
||||
" if hidden: plt.plot(*hidden, color='white', alpha=0)\n",
|
||||
" for lens, mark in zip(trip, '_x.+'*9):\n",
|
||||
" X = sorted(trip[lens])\n",
|
||||
" Y = range(1, len(X) + 1)\n",
|
||||
" plt.plot(X, Y, '_', ms=9, mew=1.5, label=lens + f'mm: {len(X)}', marker=mark)\n",
|
||||
" plt.legend(loc=2, fontsize='small')\n",
|
||||
" plt.savefig(filename)\n",
|
||||
" plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Actual Focal Length Plots\n",
|
||||
"\n",
|
||||
"The actual lens focal length data. This makes it clear how often I was at the very far end of my 400mm, 300mm, and 600mm zooms:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 468x324 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 468x324 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 468x324 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"shot_plots(actual, label='actual', hidden=([800], [60]))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 35mm Equivalent Focal Length Plots"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 468x324 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 468x324 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 468x324 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"shot_plots(equiv, hidden=([1400], [60]))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Totals (Actual and Equivalent)\n",
|
||||
"\n",
|
||||
"Here I aggregate all the shots (all lens and all trips) together. Again, the two data sets (actual and equivalent) were collected at different times with different definitions of **best** photos, so they have different totals."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAasAAAE5CAYAAAAuvw8lAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvnQurowAAIABJREFUeJzsnXt4HVW5uN8vbdpCb7QUSqHQFrkUamiFckBQKOJBUFFOQUBAKXI7Iv70iLfj8VJB5eKFA3LQooAKCHhpQw0SIZYgtCWHxhNogVYbaaChRBpK2qa0abq/3x8zO0xm7yST7LVnZjff+zzzZO+Z2TPvrD07a75Za9YnqophGIZhpJmypAUMwzAMoy+ssjIMwzBSj1VWhmEYRuqxysowDMNIPVZZGYZhGKnHKivDMAwj9VhlVSKISK2I/Dzw/hciUpOk0+6GiKwTka8n7RFEROaKyHMiUhK/VRGZIyIqIpP7WO+zIrJeRDIiMj8mPXy3i3pZHsk/TkRkvois7WOdd4vIyyKyR1xecVMSP4DBgIgcICI7RORVERnqaJvr/B9edvqniDwkItNdbD+wnxoR+UWE9T4qIn8UkddEZJuIPC8inxMRCa33PhF5UkTaROQNEXlMRGYHlu8hIlX+j3O7iLSISKWIHOnyuAaKiHSKyDwH2xkK/AD4lqpmChZ7e7tr46wg8ux/f+C/geuBA/COcdAjIpP93+mc/n5WVZcDq4BrnIulBKus0sOlQBXwJnCmw+3eCEwC9gc+DIwFHna4/f4wB1gO/BvwTuD7eP+wvpxdQUQOwiuHBuBY4CSgDfiTiIz0V1PgMeBc4HDgQ8BQ4M8iMiKOA4mJfwNGAIuTFnHMwXj/exar6gZV3Zq00G7Cz4HPiEh50iJFQVVtSnjC++E24VVSXwEeybNOLfDzwPtfADV9bHcd8PXQvDPx/tmPC8w7BPg9XkW5CXgUqAgsHwPcDbwG7ABeAX4U8NDQNKcfx34rUB94f5a/jdGBeRX+vJm9bGdmhHUm+8e5EdgO/AP4Uqi8rgVuAd4AWoCbgaGBdcqBG4BmoAN4AbggtI1u5dFXGfbiWwncEZo3DVgIvApsA1YCn8jz2c/4bjuAfwK/D5xH4e9rKt6FhAKTQ9vpBOYF3n8XeNHf9yvAT4GxgeV5txNYPj/f/v1lF/vOHcB64DvBsu/tuPxlFwB1eBc3G/Euyg4LfV6Bi3op8xx/+v59zPPL6UTgr37Z1APHhrZ9qv99bQeeA04O+uQpl3WBMlsLfBRYDbT73+Ohoe2P8MvldJf/n9IyWWSVDs4AhgOPAPcAp4rIVNc7EZG98H7QL6rqJn/eROApvB/+e4HjgTVArYjs43/0O8DReD+WQ4Hz8P5hAXwOeBL4DV4ENwlY1g+tvfB+fFnqgbeAK0Sk3L8HfynQiPdDzXdco4HL8CqQv/eyr9vxIsv3A9P97a4PrfNZYANwnP/6arx/olm+B1wOfB4vOrwXuFdETvWXHwvs8pdnywN6L8OeOBn439C8UcASvHOmArgDuFtETsmuICLfxouob/fXOR3vnyjAXLwK9YcBv1f68AjyFnAFcCTeP+k5eBccUfkBcLb/+ujs/kXkQ8BdeOf/O/FuZ30G+FbE4wLvN5Qt53/F+x4eFpFh/fDrRsTfB3gXnNfj/R6O9tf/TfaWvogcAPwBrzI9GvgPvAuhIEf7f8/GK5djA8smAZ8GLgROAEbjlVcXqrodeBY4hd2RpGtLmxTgIeCHgffVwHdC69QysMhqB7AVr0JQvGji8MA684GnQ58TvMrh8wG/X/Syn5relvfyuTnATuDM0Pzj8CLNTiCDV0kdnOfzN/rHpnhX24f0sb9ngfl9lNfi0LxHgPv913v65XlVaJ1FwJLA+27RSJQyzOOyl39cZ0Q8f37mvx6JV6F8sZf114bLgYiRVZ5t/ZtfJmW9baevfeFf8ITW+5x/LMOiHFee/Yz393NiYF6/IquIv495/meODp3Div9bw4tI1wFDAuucTvfIajJ57kz4Dp3APoF55/m/jRGhdRcCv41aRqU0WWSVMP4V14fwKp8svwQ+5aijxf8As/Buk70X72q+yo9GwLt6O0ZEtmYnYAveraFD/XVuB84RkVUicouInFFo7zQROR7vNtd8Vf1DYP6+eLfL/oD3gz/Rd/5jwDnL94F34V1J/gNYlGedIP8NfE1E6kTkRhE5Kc86DaH3rwIT/deH4P3j/EtonSeAGb3sF/pfhtleXduDM0VkTxG5we+c8ob/fX0QmOKvMgPvdtCjffgMCL934l/8jkBbgfvwymS/Ajc9g/zlOgJ4BxGOS0RmicgiEXlJRLYAL/uLpvT0mQhE+X2AV8k8G3j/qv83e+4cCTyjqrsC6yzvh8erqvp6aPsC7Btabztvnzu7FU56nRkFcSkwBPi/UKe4IXjtS4sK3P4bqprt9rpWRC7Fu811Hl6DbBnwZ7zbXWHaAFT1T37Hhw/gXXneC6wUkVNDP75I+L2d/gBcr6rfCy2+GhBVvTqw/vl4bQVZZ3yvjXhtE38XkWVAK95tkp/m26+q3i0i1XhXtKcAj4jIIlUNdmXuCH8MBx2RBlCGG/19jw/N/z7ercQv4N2Oase7pTe2QMVsb8Ouk1BEhhA4dhE5Dvgt3u2uL+F9J8fjXVwN+FabC0RkT7yK7CngErz2RoDnKcytz9+HTyb0PWrg8+F5AyHfeRnePnjny4YC9pNaLLJKEP/K+lK8dpBZoel+vLYB12R/UNmrrxV4V63rVXVtaOq6klPVN1T1flW9Ei8SPBnvahG8H9KQKDv32yb+iBdRhSsq8G73hLtpZ/xJclfvvnm8q+8eUa/32d2q+km8sr9QRMZEcce7fbYDr4dikJPxug1nyVsefZRheN2d/jbDEdtJwH2q+htVfRYvojwssPwFvKvr03o5jnx+//T/7h+YN4vuZf4eYKOqfl1V61T1b3i3rlzwPPnL9S28W259HdcRwD7Af6lqraq+CIyj73OmLyL9PiLwAnCsfwGQ5fjQOtkKKdJvqQcq8Jx3O6yySpYzgAOBBaq6Kjjh3RY8zUFHi1Eisp8/zQR+gvcP4E/+8tvwfhwPich7RWSqiLxHRL4rIicA+K/nisjhInIoXvSylbdvs7yEd6vkHSIyoaeusyLyMbxI8QfAfQGvYEP1YuAI/1bX4SJSwds9Dh/ztzNHRD4tIjNF5CARORH4HV6FtrCnghCR20Tkg77nDLzOBq/g3dbpE1XdhteZ4DoR+ZiIHCYiX8OLdIIV70vAKSKyv4hMiFiG+fgj3j/sIGuAj4rIv4j3XNkdBCoY9bqB/xCYLyKf8R1nish/hvxO9Mtugn/RtBavnXC+iEwXkffgdQAIRgNrgH1E5FIROVhEPglcFaHoonA9cLaIfNV3PhevreaHqtoR4bia8C4kPut/v6fi9eosJJqBCL+PiNyOd0vwJyJyhHgdYr7rL8s6bsQ7J07zfxfj+iPqn1eT8NpZdz+SbjQbzBNew/jyHpYNBV7H72jBwDtYBLvCtuLd0jgptN4UvLaH1/F+8E14t6mm+cu/gXeVvxXv1scTwHsCnz8Yr70h29lhTg8+tSGfbl10A+udjdcLbjNeF/Ilof0d52+rNeR7ZB/l8T/A3/Aq61a8rs0zQuUV7ur/c6A28L7Xruv+OqfjtbN18HbX9V7LsAffg/E6oBwYmHcg3oVGO97tnm8Dd4YcBa9zwhrfoYVAozswG68X3Vt07zp+HG/3xnwWr40z3HX9On977XiV6cdD25jDADpY+PMvDpRbM94/86H9OK5z8HqDbgf+D6+iD/v3q4NFxN/HPKAztJ2czhJ4vVBX+dt4Du9iVYGzA+t8Eu9iopNQ1/XQ9t8TLHd/3reBPxX7/1ZSk/gHaRhGChGRO4Etqvr5pF0Mt/gdfJ4AjlLVlQVuaxRedHyWqj7twi9t2G1Aw0g3/wm8VmjvSyN5/FvXJ/i3Ej8I/AyoK7Si8pmGd1dgt6yoAIusDMMw4kBEbsB7KH8i3kgmjwFfUdXWRMVKBKusDMMwjNRjtxYMwzCM1GOVlWEYhpF6dtsRLCZMmKBTp07tc71MJkNZWfrrbPN0i3m6xTzdMpg86+vrN6rqPn2umHTf+WJNxxxzjEbhkUceibRe0pinW8zTLebplsHkCazQwfyc1ezZs3XFit1y1BHDMIzdBhGpV9XZfa2X/jizyCxc2OPoPKnCPN1inm4xT7eYZy4WWRmGYRiJETWy2m07WORj586drF+/nu3b304RtG3bNvbcc88EraJhnh4jRoxg8uTJlJfnHSs3MgsXLmTu3LmOrIqHebrFPN0Sp+egiqxeeuklRo8ezd57700od5RRAqgqra2tbNmyhWnTpiWtYxiGA6zNKg/bt2/PqajefPPNBI2iY54gIuy9997dIuOBUlVV5cCo+JinW8zTLXF6DqrI6sUXX+SII47oNm8wPc8QB3F45vse+0tHRwfDhiWa3DYS5ukW83SLC0+LrCLS3t6etEKP1NbWsmzZMmDgnqNGjXKp1CdBz/vuu4+jjjqKiooKTjjhBJ599tmuZTfffDMzZszgne98Jx//+Me7oqVLL72UmTNnctRRR3HOOeewdevWonhmyzXtmKdbzNMtcXoOqg4W+dhjjz36XikhamtrGTVqFCeccEKqPYMEPadNm8YTTzzBuHHjeOSRR7jiiiuoq6ujubmZW2+9lRdeeIE99tiDc889lwceeIB58+Zx8803M2aMl2X+C1/4Arfddhtf/epXnXtWVFQ432YxME+3mGdhnLdgebf3nZ1l/GRN93kPXvnuoux70EdWO3bsiHV/Z511FscccwwzZszgjjvu6JpfXV3N0UcfzcyZMzn11FNZt24dP/3pT7n55puZNWsWS5YsYd68efzud7/r+kw2atq6dSunnnoqRx99NBUVFTz00EO9Oqxbt47p06czb94Line truncated
|
||||
"text/plain": [
|
||||
"<Figure size 468x324 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 468x324 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def all_shots(table): return [m for y in table for lens in table[y] \n",
|
||||
" for m in table[y][lens]]\n",
|
||||
"\n",
|
||||
"shot_plots({'All': {'actual ': all_shots(actual)}}, 'actual') \n",
|
||||
"shot_plots({'All': {'equiv ': all_shots(equiv)}})"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"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.7.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
+1275
-314
File diff suppressed because it is too large.
Load diff
@@ -2249,7 +2249,7 @@
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"A table and a plot will give a feel for the `util` function. Notice the characteristics concave-down shape of the plot."
|
||||
"A table and a plot will give a feel for the `util` function. Notice the characterisitc concave-down shape of the plot."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3094,7 +3094,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.5.3"
|
||||
"version": "3.7.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,524 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"\n",
|
||||
"<div style=\"text-align:right\">Peter Norvig<br>Nov 2019</div>\n",
|
||||
"\n",
|
||||
"# Riddler Lottery\n",
|
||||
"\n",
|
||||
"The 538 Riddler [poses](https://fivethirtyeight.com/features/can-you-decode-the-riddler-lottery/) this problem:\n",
|
||||
"\n",
|
||||
"> Five friends with a lot in common are playing the [Riddler Lottery](https://fivethirtyeight.com/features/can-you-decode-the-riddler-lottery/), in which each must choose exactly five numbers from 1 to 70. After they all picked their numbers,\n",
|
||||
"- The first friend notices that no number was selected by two or more friends. \n",
|
||||
"- The second friend observes that all 25 selected numbers are composite (i.e., not prime). \n",
|
||||
"- The third friend points out that each selected number has at least two distinct prime factors. \n",
|
||||
"- The fourth friend excitedly remarks that the product of selected numbers on each ticket is exactly the same. \n",
|
||||
"- The fifth friend is left speechless. (You can tell why all these people are friends.)\n",
|
||||
"\n",
|
||||
"> 1. What is the product of the selected numbers on each ticket?\n",
|
||||
"2. How many _different_ ways could the friends have selected five numbers each so that all their statements are true?\n",
|
||||
"\n",
|
||||
"# Preliminary Analysis\n",
|
||||
"\n",
|
||||
"The fourth friend's statement was a bit unclear, but I take it to mean that each friend multiplied together their own five numbers, and they all got the same product. To be concrete, here's an example of a solution in a simplified version of the problem where each friend only selects two tickets, not five:\n",
|
||||
"\n",
|
||||
" Friend Selection Product Factors\n",
|
||||
" 1 ( 6, 60) 360 [2, 3] + [2, 2, 3, 5]\n",
|
||||
" 2 (10, 36) 360 [2, 5] + [2, 2, 3, 3]\n",
|
||||
" 3 (12, 30) 360 [2, 2, 3] + [2, 3, 5]\n",
|
||||
" 4 (15, 24) 360 [3, 5] + [2, 2, 2, 3]\n",
|
||||
" 5 (18, 20) 360 [2, 3, 3] + [2, 2, 5]\n",
|
||||
"\n",
|
||||
"And here's a list of the key concepts:\n",
|
||||
"\n",
|
||||
"- **number**: An integer from 1 to 70, e.g. the int `42`.\n",
|
||||
"- **factors**: Every positive integer has a unique prime factorization, e.g. `factors(12) == [2, 2, 3]`: two distinct primes factors. But `factors(8) == [2, 2, 2]`: one distinct prime factor.\n",
|
||||
"- **selection**: A collection of 5 numbers, e.g. the sorted tuple `(12, 15, 20, 28, 30)`.\n",
|
||||
"- **product**: The result of multiplying together the 5 numbers in a selection, e.g. the int `3024000`.\n",
|
||||
"- **candidate**: A candidate solution is a set of 5 selections e.g. `{( 6, 60), (10, 36), (12, 30), (15, 24), (18, 20)}` in my simplified version where each selection has only two numbers.\n",
|
||||
"- **solution**: A solution is a candidate that satisifes each of the four friends' statements.\n",
|
||||
"\n",
|
||||
"Can I use brute force and enumerate all the possible candidates? \n",
|
||||
"\n",
|
||||
"There are (70 choose 5) × (65 choose 5) × (60 choose 5) × (55 choose 5) × (50 choose 5) / 5! or [about](https://www.wolframalpha.com/input/?i=%2870+choose+5%29+*+%2865+choose+5%29+*+%2860+choose+5%29+*+%2855+choose+5%29+*+%2850+choose+5%29+%2F+5%21) $10^{31}$ candidates, so no. We'll have to be more clever."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Valid Numbers\n",
|
||||
"\n",
|
||||
"There will be fewer candidates to consider if we can reduce the number of valid numbers to select from. The __third__ friend stated that the numbers all have at least two distinct prime factors. So let's find the numbers that have that property. (The numbers we are dealing with are small, so don't worry about the inefficiency of my function `factors`.)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from itertools import combinations\n",
|
||||
"from collections import Counter, defaultdict"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"41 {6, 10, 12, 14, 15, 18, 20, 21, 22, 24, 26, 28, 30, 33, 34, 35, 36, 38, 39, 40, 42, 44, 45, 46, 48, 50, 51, 52, 54, 55, 56, 57, 58, 60, 62, 63, 65, 66, 68, 69, 70}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def factors(n) -> list:\n",
|
||||
" \"List of prime factors that multiply together to give n.\"\n",
|
||||
" return ([] if n == 1\n",
|
||||
" else next([p] + factors(n // p) \n",
|
||||
" for p in range(2, n + 1) if n % p == 0))\n",
|
||||
"\n",
|
||||
"distinct = set\n",
|
||||
"\n",
|
||||
"numbers = {n for n in range(1, 71) if len(distinct(factors(n))) >= 2}\n",
|
||||
"\n",
|
||||
"print(len(numbers), numbers)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Great; we got it down from 70 to 41 possible numbers.\n",
|
||||
"\n",
|
||||
"Now the __fourth__ friend's statement says that each friend picks five numbers that have the same product. \n",
|
||||
"In my simplified version where the friends pick two numbers each, they all picked a selection with the product 360. The prime factorization of 360 is `[2, 2, 2, 3, 3, 5]`; that means that all five friends had to find a different way of allocating these factors to their numbers. \n",
|
||||
"\n",
|
||||
"For each number that is selected by any friend, and for each prime factor $p$ of that number, it must be the case that there are at least four other valid numbers that also have $p$ as a factor; otherwise the product couldn't be the same for all the friends. So let's count in how many numbers each prime appears:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Counter({2: 29,\n",
|
||||
" 3: 20,\n",
|
||||
" 5: 12,\n",
|
||||
" 7: 8,\n",
|
||||
" 11: 5,\n",
|
||||
" 13: 4,\n",
|
||||
" 17: 3,\n",
|
||||
" 19: 2,\n",
|
||||
" 23: 2,\n",
|
||||
" 29: 1,\n",
|
||||
" 31: 1})"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"prime_counts = Counter(p for n in numbers for p in distinct(factors(n)))\n",
|
||||
"\n",
|
||||
"prime_counts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This says that the prime factor 2 appears in 29 valid numbers and the prime factor 31 appears in only 1 valid number. Only factors that appear in at least 5 valid numbers can be part of a solution, so that's `{2, 3, 5, 7, 11}`.\n",
|
||||
"\n",
|
||||
"Let's update the set of valid numbers to contain only numbers $n$ such that every prime factor $p$ of $n$ appears in at least 5 valid numbers:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"28 {6, 10, 12, 14, 15, 18, 20, 21, 22, 24, 28, 30, 33, 35, 36, 40, 42, 44, 45, 48, 50, 54, 55, 56, 60, 63, 66, 70}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"numbers = {n for n in numbers if all(prime_counts[p] >= 5 for p in distinct(factors(n)))}\n",
|
||||
"\n",
|
||||
"print(len(numbers), numbers)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"There are now only 28 valid numbers; a nice reduction from 70 to 41 to 28.\n",
|
||||
"\n",
|
||||
"# Valid Selections\n",
|
||||
"\n",
|
||||
"Now let's switch attention from individual numbers to selections of five numbers. There are (28 choose 5) = 98,280 possible selections; a manageable number. But that means there are (98,280 choose 5) = $\\approx 10^{23}$ candidate solutions; an unmanageable number.\n",
|
||||
"\n",
|
||||
"My first thought to reduce the number of candidates is to say that we should only consider candidates where all five selections in the candidate have the same product. To do that, we can group selections by product.\n",
|
||||
"\n",
|
||||
"We'll make `products` be a `dict` where each key is the product of the five numbers in a selection, and the corresponding value is a list of all the selections of five numbers with that product:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def multimap(items, key) -> dict:\n",
|
||||
" \"A dict of {key(item): [item, ...]}\"\n",
|
||||
" result = defaultdict(list)\n",
|
||||
" for x in items:\n",
|
||||
" result[key(x)].append(x)\n",
|
||||
" return result\n",
|
||||
"\n",
|
||||
"def product(nums) -> int: \n",
|
||||
" \"Multiply nums together (similar to sum(nums)).\"\n",
|
||||
" result = 1\n",
|
||||
" for num in nums:\n",
|
||||
" result *= num\n",
|
||||
" return result\n",
|
||||
"\n",
|
||||
"products = multimap(combinations(numbers, 5), key=product)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Here is an entry in the `products` dict:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[(6, 10, 12, 14, 30),\n",
|
||||
" (6, 10, 12, 15, 28),\n",
|
||||
" (6, 10, 12, 20, 21),\n",
|
||||
" (6, 10, 14, 15, 24),\n",
|
||||
" (6, 10, 14, 18, 20),\n",
|
||||
" (6, 12, 14, 15, 20)]"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"products[6 * 12 * 14 * 15 * 20]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This says there are 6 selections whose product is 6 * 12 * 14 * 15 * 20 = 302,400; if there were a way to choose 5 of these 6 with all distinct numbers, that would be a solution. Sadly, there is no such way. Since every one of the selections contains a 6; we can't even choose two disjoint selections, let alone five.\n",
|
||||
"\n",
|
||||
"Let's see how many different products there are, and how many have at least five selections:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"(4042, 2407)"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"len(products), len([n for n in products if len(products[n]) >= 5])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"It seems reasonable to go through all the products and see if one of the 29,506 with 5 or more selections can come up with 5 disjoint selections. The function `k_disjoint(k, selections)` finds all ways to choose `k` different elements of `selections` such that there is no shared number among any selection. The function keeps track of a `partial_solution`—a set of previously-found selections—as it recursively searches for a complete solution. Any new selection must be disjoint from all the selections in `partial_solution`. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def k_disjoint(k, selections, start=0, partial_solution=set()) -> list:\n",
|
||||
" \"All ways of picking k elements of selections that have all disjoint members.\"\n",
|
||||
" if len(partial_solution) == k:\n",
|
||||
" yield partial_solution\n",
|
||||
" elif len(partial_solution) + (len(selections) - start) >= k:\n",
|
||||
" for i in range(start, len(selections)):\n",
|
||||
" selection = selections[i]\n",
|
||||
" if all(is_disjoint(selection, s) for s in partial_solution):\n",
|
||||
" yield from k_disjoint(k, selections, i + 1, partial_solution | {selection})\n",
|
||||
" \n",
|
||||
"def is_disjoint(A, B) -> bool: return not any(a in B for a in A)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Here's an example of how `k_disjoint` works. Out of the following six selections (which in this simplified example have only two numbers each, not five), there are two ways to pick five selections without having a duplicate number:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{(6, 60), (10, 36), (12, 30), (15, 24), (18, 20)},\n",
|
||||
" {(6, 60), (10, 35), (12, 30), (15, 24), (18, 20)}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"selections = [(6, 60), (10, 36), (12, 30), (15, 24), (18, 20), (10, 35)]\n",
|
||||
"list(k_disjoint(5, selections))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"But for the six selections whose product is 302,400, there are no disjoint solutions:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[]"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"selections = [(6, 10, 12, 14, 30),\n",
|
||||
" (6, 10, 12, 15, 28),\n",
|
||||
" (6, 10, 12, 20, 21),\n",
|
||||
" (6, 10, 14, 15, 24),\n",
|
||||
" (6, 10, 14, 18, 20),\n",
|
||||
" (6, 12, 14, 15, 20)]\n",
|
||||
"list(k_disjoint(5, selections))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now we're ready to solve the problem.\n",
|
||||
"\n",
|
||||
"# 1. What is the product of the selected numbers on each ticket?\n",
|
||||
"\n",
|
||||
"That is, find the number `N` in `products` that can form 5 disjoint selections."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"The product is 19,958,400; factors are [2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 5, 5, 7, 11]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"N = next(n for n in products if any(k_disjoint(5, products[n])))\n",
|
||||
"\n",
|
||||
"print(f'The product is {N:,d}; factors are {factors(N)}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 2. How many different ways could the friends have selected five numbers?\n",
|
||||
"\n",
|
||||
"I'll compute all of the results for `k_disjoint`, and see how many there are:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"There are 12,781 different ways.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"different_ways = list(k_disjoint(5, products[N]))\n",
|
||||
"print(f'There are {len(different_ways):,d} different ways.')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"That's too many to look at all of them, but I can peek at every thousandth one:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{(6, 15, 56, 60, 66),\n",
|
||||
" (10, 14, 48, 54, 55),\n",
|
||||
" (12, 18, 42, 44, 50),\n",
|
||||
" (20, 21, 33, 36, 40),\n",
|
||||
" (22, 24, 28, 30, 45)},\n",
|
||||
" {(6, 18, 55, 56, 60),\n",
|
||||
" (10, 21, 30, 48, 66),\n",
|
||||
" (12, 22, 28, 50, 54),\n",
|
||||
" (14, 20, 36, 44, 45),\n",
|
||||
" (15, 24, 33, 40, 42)},\n",
|
||||
" {(6, 20, 54, 55, 56),\n",
|
||||
" (10, 21, 36, 44, 60),\n",
|
||||
" (12, 28, 33, 40, 45),\n",
|
||||
" (14, 18, 24, 50, 66),\n",
|
||||
" (15, 22, 30, 42, 48)},\n",
|
||||
" {(6, 21, 48, 50, 66),\n",
|
||||
" (10, 24, 33, 45, 56),\n",
|
||||
" (12, 18, 28, 55, 60),\n",
|
||||
" (14, 20, 30, 44, 54),\n",
|
||||
" (15, 22, 36, 40, 42)},\n",
|
||||
" {(6, 22, 50, 54, 56),\n",
|
||||
" (10, 14, 36, 60, 66),\n",
|
||||
" (12, 18, 40, 42, 55),\n",
|
||||
" (15, 21, 30, 44, 48),\n",
|
||||
" (20, 24, 28, 33, 45)},\n",
|
||||
" {(6, 24, 42, 50, 66),\n",
|
||||
" (10, 18, 33, 56, 60),\n",
|
||||
" (12, 28, 30, 36, 55),\n",
|
||||
" (14, 15, 40, 44, 54),\n",
|
||||
" (20, 21, 22, 45, 48)},\n",
|
||||
" {(6, 28, 30, 60, 66),\n",
|
||||
" (10, 15, 44, 54, 56),\n",
|
||||
" (12, 22, 36, 42, 50),\n",
|
||||
" (14, 24, 33, 40, 45),\n",
|
||||
" (18, 20, 21, 48, 55)},\n",
|
||||
" {(6, 28, 40, 45, 66),\n",
|
||||
" (10, 30, 33, 42, 48),\n",
|
||||
" (12, 21, 24, 55, 60),\n",
|
||||
" (14, 18, 36, 44, 50),\n",
|
||||
" (15, 20, 22, 54, 56)},\n",
|
||||
" {(6, 28, 44, 50, 54),\n",
|
||||
" (10, 21, 33, 48, 60),\n",
|
||||
" (12, 14, 40, 45, 66),\n",
|
||||
" (15, 22, 30, 36, 56),\n",
|
||||
" (18, 20, 24, 42, 55)},\n",
|
||||
" {(6, 30, 36, 55, 56),\n",
|
||||
" (10, 15, 42, 48, 66),\n",
|
||||
" (12, 28, 33, 40, 45),\n",
|
||||
" (14, 20, 22, 54, 60),\n",
|
||||
" (18, 21, 24, 44, 50)},\n",
|
||||
" {(6, 30, 44, 45, 56),\n",
|
||||
" (10, 15, 42, 48, 66),\n",
|
||||
" (12, 14, 40, 54, 55),\n",
|
||||
" (18, 24, 28, 33, 50),\n",
|
||||
" (20, 21, 22, 36, 60)},\n",
|
||||
" {(6, 33, 40, 45, 56),\n",
|
||||
" (10, 12, 42, 60, 66),\n",
|
||||
" (14, 22, 24, 50, 54),\n",
|
||||
" (15, 28, 30, 36, 44),\n",
|
||||
" (18, 20, 21, 48, 55)},\n",
|
||||
" {(6, 36, 40, 42, 55),\n",
|
||||
" (10, 20, 33, 54, 56),\n",
|
||||
" (12, 18, 28, 50, 66),\n",
|
||||
" (14, 15, 44, 45, 48),\n",
|
||||
" (21, 22, 24, 30, 60)}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"different_ways[::1000]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"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.7.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
+8474
-18460
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,639 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div style=\"text-align: right\"><i>Peter Norvig<br>April 2020</i></div>\n",
|
||||
"\n",
|
||||
"# The Stable Matching Problem\n",
|
||||
"\n",
|
||||
"The **[stable matching problem](https://en.wikipedia.org/wiki/Stable_marriage_problem#Algorithmic_solution)** involves two equally-sized disjoint sets of actors that want to pair off in a way that maximizes happiness. It could be a set of women and a set of men that want to pair off in heterosexual marriage; or a a set of job-seekers and a set of employers. Every year, there is a large-scale application of this problem in which:\n",
|
||||
"- Graduating medical students state which hospitals they would prefer to be residents at.\n",
|
||||
"- Hospitals in turn state which students they prefer. \n",
|
||||
"- An algorithm finds a stable matching.\n",
|
||||
"\n",
|
||||
"Each actor has **preferences** for who they would prefer to be matched with. In the default way of stating the problem, preferences are expressed as an **ordering**: each actor rates the possible matches on the other side from most preferred to least preferred. But we will go beyond that, allowing each actor to say more: to express their preference for each possible match as a **utility**: a number between 0 and 1. For example actor $A$ on one side could say that they would like to be paired with actor $β$ on the other side with utility 0.9 (meaning a very desireable match) and with actor $γ$ on the other side with utility 0.1 (meaning an undesireable match). The algorithm we present actually pays attention only to the ordering of preferences, but we will use the utilities to analyze how well each side does, on average.\n",
|
||||
"\n",
|
||||
"A matching is **stable** if it is **not** the case that there is an actor from one side and an actor from the other side who both have a higher preference for each other than they have for who they are currently matched with. \n",
|
||||
"\n",
|
||||
"# Gale-Shapley Matching Algorithm\n",
|
||||
"\n",
|
||||
"The **[Gale-Shapley Stable Matching Algorithm](https://en.wikipedia.org/wiki/Gale%E2%80%93Shapley_algorithm)** (*Note: David Gale was my father's [PhD advisor](https://www.genealogy.math.ndsu.nodak.edu/id.php?id=10282&fChrono=1).*) works as follows: one side is chosen to be the **proposers** and the other side the **acceptors**. Until everyone has been matched the algorithm repeats the following steps:\n",
|
||||
"- An unmatched proposer, $p$, proposes a match to the highest-ranked acceptor, $a$, that $p$ has not yet proposed to.\n",
|
||||
"- If $a$ is unmatched, then $a$ tentatively accepts the proposal to be a match.\n",
|
||||
"- If $a$ is matched, but prefers $p$ to their previous match, then $a$ breaks the previous match and tentatively accepts $p$.\n",
|
||||
"- If $a$ is matched and prefers their previous match to $p$, then $a$ rejects the proposal.\n",
|
||||
"\n",
|
||||
"I will define the function `stable_matching(P, A)`, which is passed two preference arrays: $N \\times N$ arrays of utility values such that `P[p][a]` is the utility that proposer `p` has for being matched with `a`, and `A[a][p]` is the utility that acceptor `a` has for being matched with `p`. The function returns a set of matches, `{(p, a), ...}`. To implement the algorithm sketched above, we keep track of the following variables:\n",
|
||||
"- `ids`: If there are $N$ actors on each side, we number them $0$ to $N-1$; `ids` is the collection of these numbers.\n",
|
||||
"- `unmatched`: the set of proposers that have not yet been matched to any acceptor.\n",
|
||||
"- `matched`: A mapping from acceptors to their matched proposers: `matched[a] = p`.\n",
|
||||
"- `proposals`: Keeps track of who each proposer should propose to next. `proposals[p]` is a list of acceptors sorted by increasing utility, which means that `proposals[p].pop()` returns (and removes) the best acceptor for $p$ to propose to next. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"from statistics import mean, stdev\n",
|
||||
"import random\n",
|
||||
"\n",
|
||||
"def stable_matching(P, A) -> set:\n",
|
||||
" \"\"\"Compute a stable match, a set of (p, a) pairs.\n",
|
||||
" P and A are square preference arrays: P[p][a] is how much p likes a;\n",
|
||||
" A[a][p] is how much a likes p. Stable means there is no (p, a)\n",
|
||||
" such that both prefer each other over the partner they are matched with.\"\"\"\n",
|
||||
" ids = range(len(P)) # ID numbers of all actors on (either) side\n",
|
||||
" unmatched = set(ids) # Members of P that are not yet matched to anyone\n",
|
||||
" matched = {} # {a: p} mapping of who acceptors are matched with\n",
|
||||
" proposals = orderings(P, ids) # proposals[p] is an ordered list of who p should propose to\n",
|
||||
" while unmatched:\n",
|
||||
" p = next(iter(unmatched)) # p is an arbitrary unmatched Proposer\n",
|
||||
" a = proposals[p].pop() # a is p's most preferred remaining acceptor\n",
|
||||
" if a not in matched:\n",
|
||||
" unmatched.remove(p)\n",
|
||||
" matched[a] = p\n",
|
||||
" elif A[a][p] > A[a][matched[a]]:\n",
|
||||
" unmatched.add(matched[a])\n",
|
||||
" unmatched.remove(p)\n",
|
||||
" matched[a] = p\n",
|
||||
" return {(p, a) for (a, p) in matched.items()}\n",
|
||||
"\n",
|
||||
"def orderings(P, ids): return [sorted(ids, key=lambda a: P[p][a]) for p in ids]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The algorithm has the following properties:\n",
|
||||
"- The algorithm will always terminate.\n",
|
||||
"- The output of the algorithm will always be a stable matching.\n",
|
||||
"- Out of all possible stable matchings, it will produce the one that is optimal for proposers: each proposer gets the best possible match they could get. That's true because the proposers propose in order of preference, so the acceptor that they most prefer who also prefers them will accept their proposal.\n",
|
||||
"- The acceptors have no such luck; they might not get their best possible match, because a proposer who is a better match for them might not ever propose to them.\n",
|
||||
"\n",
|
||||
"What I want to get a handle on is: *how bad is this for the acceptors?* What's the gap in expected utility between the proposers and the acceptors?\n",
|
||||
"\n",
|
||||
"# Preference Arrays\n",
|
||||
"\n",
|
||||
"Let's define some preference arrays. `I` is the identity matrix: it says that every proposer number $i$ likes acceptor $i$ best, and dislikes the others equally. `X` is the same as the identity matrix for indexes 0, 1, and 2, but it says that 3 prefers 4 and 4 prefers 3."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"I = [[1, 0, 0, 0, 0],\n",
|
||||
" [0, 1, 0, 0, 0],\n",
|
||||
" [0, 0, 1, 0, 0],\n",
|
||||
" [0, 0, 0, 1, 0],\n",
|
||||
" [0, 0, 0, 0, 1]]\n",
|
||||
"\n",
|
||||
"X = [[1, 0, 0, 0, 0],\n",
|
||||
" [0, 1, 0, 0, 0],\n",
|
||||
" [0, 0, 1, 0, 0],\n",
|
||||
" [0, 0, 0, 0, 1],\n",
|
||||
" [0, 0, 0, 1, 0]]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's see what happens when `I` is the proposing side, and when `X` is the proposing side:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)}"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"stable_matching(I, X)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{(0, 0), (1, 1), (2, 2), (3, 4), (4, 3)}"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"stable_matching(X, I)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"When `I` is the proposing side, every actor in `I` gets their first-choice match.\n",
|
||||
"\n",
|
||||
"Likewise, when `X` is the proposer, every actor in `X` gets their first-choice match.\n",
|
||||
"\n",
|
||||
"We can measure the average utility to each side for any matching:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def mean_utilities(P, A):\n",
|
||||
" \"\"\"The mean utility over all members of P, and the mean utility over all members of A,\n",
|
||||
" for the matching given by stable_matching(P, A).\"\"\"\n",
|
||||
" matching = stable_matching(P, A)\n",
|
||||
" return (mean(P[p][a] for (p, a) in matching),\n",
|
||||
" mean(A[a][p] for (p, a) in matching))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"(1, 0.6)"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"mean_utilities(I, X)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"(1, 0.6)"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"mean_utilities(X, I)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Is it Fair?\n",
|
||||
"\n",
|
||||
"We see that in both cases, the proposers get 100% of their maximum possible utility, and the acceptors gets only 60% (averaged over all five acceptors). Is this a problem? If the Gale-Shapley algorithm is used in high-stakes applications like matching medical residents to hospitals, does it make a big difference which side is the proposers? I want to address that question with some experiments."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Preferences with Common and Private Values\n",
|
||||
"\n",
|
||||
"I will create a bunch of randomized preference arrays and get a feeling for how they perform. But I don't want them to be completely random; I want them to reflect, in a very abstract way, some properties of the real world:\n",
|
||||
"- Some choices have intrinsic properties that make them widely popular (or unpopular). For example, Massachusetts General Hospital is considered an excellent choice by the vast majority of aspiring residents. The amount of utility that is commonly agreed upon is called the **common value** of a choice.\n",
|
||||
"- Some choices have ideosyncratic properties that appeal only to specific choosers. For example, you might really like your hometown hospital, even if it is not highly-regarded by others. We call this the **private value** of a choice.\n",
|
||||
"- In real world situations there is usually a mix of common and private value.\n",
|
||||
"\n",
|
||||
"The function call `preferences(N, 0.75)`, for example, creates an NxN array of preferences, where each preference is 75% common value and 25% individual value. I implement individual value as being proportional to the ID number (`a` in the code):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def preferences(N=25, c=0.75):\n",
|
||||
" \"\"\"Create an NxN preference array, weighted: c × common + (1 - c) × random.\"\"\"\n",
|
||||
" return [[round(c * (a + 0.5) / N + (1 - c) * random.uniform(0, 1), 4)\n",
|
||||
" for a in range(N)] \n",
|
||||
" for p in range(N)]\n",
|
||||
"\n",
|
||||
"random.seed(42)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Below is a 7x7 preference array that is half common, half private. You can see as you go across a row that the utilities tend to increase, but not always monotonically:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[[0.3554, 0.1196, 0.3161, 0.3616, 0.6897, 0.7312, 0.9104],\n",
|
||||
" [0.0792, 0.3181, 0.1935, 0.3593, 0.5741, 0.4061, 0.5637],\n",
|
||||
" [0.3607, 0.3796, 0.2888, 0.5446, 0.7261, 0.3961, 0.8672],\n",
|
||||
" [0.3848, 0.2773, 0.2563, 0.7286, 0.4897, 0.4392, 0.5126],\n",
|
||||
" [0.4595, 0.409, 0.5821, 0.6149, 0.5895, 0.8794, 0.6536],\n",
|
||||
" [0.3117, 0.5218, 0.4878, 0.6809, 0.6101, 0.7451, 0.4872],\n",
|
||||
" [0.1497, 0.2518, 0.2185, 0.3664, 0.3719, 0.5318, 0.7821]]"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"preferences(7, 0.5)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Here's a preference array with no common value; the utilities are completely random, uncorrelated to their position:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[[0.3648, 0.3702, 0.2095, 0.267, 0.9367, 0.648, 0.6091],\n",
|
||||
" [0.1711, 0.7291, 0.1634, 0.3795, 0.9895, 0.64, 0.5569],\n",
|
||||
" [0.6846, 0.8429, 0.776, 0.229, 0.0321, 0.3155, 0.2677],\n",
|
||||
" [0.211, 0.9429, 0.8764, 0.3147, 0.6554, 0.3956, 0.9145],\n",
|
||||
" [0.4589, 0.2649, 0.2466, 0.5614, 0.2627, 0.5846, 0.8978],\n",
|
||||
" [0.3994, 0.2193, 0.9975, 0.5095, 0.0909, 0.0471, 0.1096],\n",
|
||||
" [0.6274, 0.7921, 0.4222, 0.0635, 0.3816, 0.9961, 0.5291]]"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"preferences(7, 0.0)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"And here's a preference array with 100% common value: every row is identical, and the utilities monotonically increase across the row:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[[0.1, 0.3, 0.5, 0.7, 0.9],\n",
|
||||
" [0.1, 0.3, 0.5, 0.7, 0.9],\n",
|
||||
" [0.1, 0.3, 0.5, 0.7, 0.9],\n",
|
||||
" [0.1, 0.3, 0.5, 0.7, 0.9],\n",
|
||||
" [0.1, 0.3, 0.5, 0.7, 0.9]]"
|
||||
]
|
||||
},
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"preferences(5, 1.0)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The `preferences` function has been designed so that the average utility value is close to 0.5:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"0.5000443999999999"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"mean(map(mean, preferences(100)))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now for one more helpful function: `examples` returns a list of the form `[(P, A), ...]` where `P` and `A` are preference arrays."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def examples(N=25, c=0.5, repeat=10000):\n",
|
||||
" \"\"\"A list of pairs of preference arrays, (P, A), of length `repeat`.\"\"\"\n",
|
||||
" return [(preferences(N, c), preferences(N, c)) for _ in range(repeat)]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[([[0.5238, 0.6412, 0.7176, 0.733],\n",
|
||||
" [0.4411, 0.358, 0.7271, 0.6545],\n",
|
||||
" [0.2763, 0.5521, 0.6802, 0.4661],\n",
|
||||
" [0.1171, 0.3816, 0.6731, 0.7765]],\n",
|
||||
" [[0.4918, 0.6565, 0.5443, 0.7238],\n",
|
||||
" [0.3467, 0.447, 0.3938, 0.6281],\n",
|
||||
" [0.4353, 0.6498, 0.3784, 0.4451],\n",
|
||||
" [0.4623, 0.663, 0.3553, 0.5625]]),\n",
|
||||
" ([[0.3339, 0.3563, 0.5098, 0.7453],\n",
|
||||
" [0.2433, 0.5171, 0.7037, 0.8426],\n",
|
||||
" [0.3665, 0.2497, 0.5967, 0.598],\n",
|
||||
" [0.1948, 0.2769, 0.5439, 0.6575]],\n",
|
||||
" [[0.2957, 0.6704, 0.5526, 0.8597],\n",
|
||||
" [0.1714, 0.2168, 0.7819, 0.5757],\n",
|
||||
" [0.3677, 0.3445, 0.3445, 0.5274],\n",
|
||||
" [0.4455, 0.3256, 0.747, 0.5011]])]"
|
||||
]
|
||||
},
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"examples(N=4, repeat=2)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Histograms of Acceptor/Proposer Utility\n",
|
||||
"\n",
|
||||
"Now we're readsy to answer the original question: how much worse is it to be an acceptor rather than a proposer? The function `show` displays two overlapping histograms of mean utilities: one for acceptors and one for proposers. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def show(N=25, c=0.5, repeat=10000, bins=50):\n",
|
||||
" \"\"\"Show two histograms of mean utility values over examples, for proposers and acceptors.\"\"\"\n",
|
||||
" pr, ac = transpose(mean_utilities(P, A) for (P, A) in examples(N, c, repeat))\n",
|
||||
" plt.hist(pr, bins=bins, alpha=0.5)\n",
|
||||
" plt.hist(ac, bins=bins, alpha=0.5);\n",
|
||||
" print(f'''{repeat:,d} examples with N = {N} actors, common factor c = {c}\n",
|
||||
" Acceptors: {mean(ac):4.3f} ± {stdev(ac):4.3f}\n",
|
||||
" Proposers: {mean(pr):4.3f} ± {stdev(pr):4.3f}''')\n",
|
||||
" \n",
|
||||
"def transpose(matrix): return list(zip(*matrix))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We'll start with preferences that are completely private; no common value:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"10,000 examples with N = 25 actors, common factor c = 0.0\n",
|
||||
" Acceptors: 0.730 ± 0.064\n",
|
||||
" Proposers: 0.870 ± 0.035\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"show(c=0.0)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The acceptors (the orange histogram) have a mean utility of 0.730 while the proposers (blue histogram) do much better with a mean of 0.870. Both sides do much better than the 0.5 average utility that they would average if we just used a random (non-stable) matching. \n",
|
||||
"\n",
|
||||
"It is clear that proposers do much better than acceptors. That suggests that the `stable_matching` algorithm is very unfair. But before drawing that conclusion, let's consider preferences with a 50/50 mix of private/common value. We'll do that for two different population sizes, 25 and 50:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"10,000 examples with N = 25 actors, common factor c = 0.5\n",
|
||||
" Acceptors: 0.621 ± 0.022\n",
|
||||
" Proposers: 0.638 ± 0.019\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"show(c=0.5, N=25)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"10,000 examples with N = 50 actors, common factor c = 0.5\n",
|
||||
" Acceptors: 0.647 ± 0.014\n",
|
||||
" Proposers: 0.661 ± 0.012\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"show(c=0.5, N=50)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We see that the gap between proposer and acceptor has been greatly reduced (but not eliminated). With more actors, the variance is smaller (the histogram is not as wide). \n",
|
||||
"\n",
|
||||
"What happens with 90% common value?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 19,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"10,000 examples with N = 25 actors, common factor c = 0.9\n",
|
||||
" Acceptors: 0.509 ± 0.005\n",
|
||||
" Proposers: 0.510 ± 0.005\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"show(c=0.9)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We see that there is very little difference between the two sides. So the conclusion is: when there is a lot of common value, the Gale-Shapley Matching Algorithm is fair. So it is probably okay to use it for matching medical residents, because there is a lot of common value in the perception of the quality of hospitals, and likewise for the quality of students.\n",
|
||||
"\n",
|
||||
"But when there is mostly private value, the algorithm is unfair, favoring the proposers over the acceptors."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"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.7.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div align=\"right\" style=\"text-align: right\"><i>Peter Norvig<br>Aug 2018</i></div>\n",
|
||||
"\n",
|
||||
"# Tour de 538\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"You are the coach for a bicycle racing team, one of 20 teams entered in the [Tour de FiveThirtyEight](https://fivethirtyeight.com/features/can-you-win-the-tour-de-fivethirtyeight/) time trial, which has these rules:\n",
|
||||
"\n",
|
||||
">*Each team rides as a group throughout the course at some fixed pace, specified by that team’s coach. Teams that can’t maintain their pace are said to have **cracked,** and don’t **finish** the course.\n",
|
||||
"The team that finishes the course with the fastest pace is the winner.\n",
|
||||
"Teams ride the course one at a time. After each team completes its attempt, the next team quickly consults with its coach (who assigns a pace) and then begins its ride. Coaches are aware of the results of all previous teams when choosing their own team’s pace.\n",
|
||||
"Assume that all teams are of equal ability: At any given pace, they have the exact same probability of cracking, and the faster the pace, the greater the probability of cracking. Teams’ chances of cracking are independent, and each team’s coach knows exactly what a team’s chances of cracking are for each pace. You are the first team to attempt the course. To maximize your chances of winning, what’s the probability that your team will finish the course? What’s the probability you’ll ultimately win? How about the other teams?*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# PRELIMINARIES\n",
|
||||
"%matplotlib inline\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import random\n",
|
||||
"from functools import lru_cache\n",
|
||||
"from collections import Counter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Analysis\n",
|
||||
"\n",
|
||||
"At first it might seem like we don't have enough information: we don't know the relation between probability of finishing and the actual pace/speed. But after a little thought, it is clear we don't need to know how fast each team goes. Probability of finishing may not be related linearly to speed, but whatever the function that relates probability of finishing to speed, it is **monotonic**: the the faster the pace, the lower the probability of finishing. Thus, the team that chooses the lowest probability and actually finishes will be the winner (even if we don't know their speed/time).\n",
|
||||
"\n",
|
||||
"Let's start with **a simpler case: just two teams**. The first team chooses a probability $p$ of finishing. If the first team cracks, the second team chooses probability 1 of finishing, rides the race at a nice leisurly pace, and wins. If the first team finishes, the second team chooses probability $p - 𝜖$ of finishing, where 𝜖 is an arbitrarily small number; that gives them an ever-so-slightly-faster pace than the first team. But let's just write $p$ instead of $p - 𝜖$, with the understanding that the $𝜖$ is so small that it doesn't matter for our calculations—another way of saying this is that a tie goes to the team that goes later. So the first team wins if they finish and the second team doesn't, and since \"chances of cracking are independent,\" that joint event has probabilty $p (1 - p)$. What pace $p$ maximizes that? I'm going to enumerate 1001 different $p$ values and find the one that maximizes $p (1 - p)$:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"0.5"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"probabilities = [x/1000 for x in range(1001)] # [0.000, 0.001, 0.002, ... 1.000]\n",
|
||||
"\n",
|
||||
"max(probabilities, key=lambda p: p * (1 - p))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"That says the first team's best choice is a pace that gives them a probability 0.5 of finishing. They win the race with probability 0.5 × (1 - 0.5) = 0.25; the second team wins with probability 0.75. There is value in the information gained by going second! \n",
|
||||
"\n",
|
||||
"Now for **the case of $n$ teams**. The first team wins if they finish (with probability $p$), and if *all* the other teams crack (with probability $(1 - p)^{n - 1}$, assuming they also choose pace $p$). All together, that's $p(1 - p)^{n - 1}$. Again, I can try out all the probabilities in `probabilities` and see which is the best pace for the first team, for various values of `n` teams in the race:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{1: 1.0, 2: 0.5, 3: 0.333, 4: 0.25, 5: 0.2, 6: 0.167}"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def best_pace(n) -> float:\n",
|
||||
" \"\"\"The pace p with highest probability of winning for the first of `n` teams.\"\"\"\n",
|
||||
" return max(probabilities, key=lambda p: win_prob(p, n))\n",
|
||||
"\n",
|
||||
"def win_prob(p, n) -> float:\n",
|
||||
" \"\"\"Probability of first team winning if all n teams choose pace `p`.\"\"\"\n",
|
||||
" return p * (1 - p) ** (n - 1)\n",
|
||||
" \n",
|
||||
"{n: best_pace(n) for n in range(1, 7)}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Notice a pattern? It looks like `best_pace(n)` is $1/n$. But can we prove it? \n",
|
||||
"\n",
|
||||
"# Using Calculus to Find Best Pace \n",
|
||||
"\n",
|
||||
"Calculus says that the extrema (maximum or minimum) of a function is where the derivative is zero. What's the derivative of $p (1 - p)^{n - 1}$?\n",
|
||||
"\n",
|
||||
"$$\\frac{d}{dp} p (1 - p)^{n - 1} = (1 - p)^{n - 2} (1 - n p) $$\n",
|
||||
"\n",
|
||||
"This is indeed zero when $p = 1/n$. We can check that `best_pace(n)` agrees with `1 / n` (to within 3 decimal places):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for n in range(1, 21):\n",
|
||||
" assert abs(best_pace(n) - 1/n) < 0.001 "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We now know how every race will play out:\n",
|
||||
"- The first of $n$ teams chooses pace $p = 1/n$.\n",
|
||||
"- If they crack, it is like starting a race with $n - 1$ teams, and the next team chooses pace $1/(n - 1)$.\n",
|
||||
"- If they finish, every team from then on will choose a pace that is $𝜖$ faster than the previous finisher.\n",
|
||||
"- The only way for the first team to win is if every other chooses $p$ and cracks.\n",
|
||||
"- This confirms that the win probability for the first team is $p(1 - p)^{n - 1}$, where $p = 1/n$.\n",
|
||||
"\n",
|
||||
"# Tabulating Results\n",
|
||||
"\n",
|
||||
"We can print a table of three columns: the number of teams $n$, the best pace for the first of $n$ teams, and the probability that the first team will win if it adopts that pace:\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" n pace P(win)\n",
|
||||
" 1 1.00000 1.00000\n",
|
||||
" 2 0.50000 0.25000\n",
|
||||
" 3 0.33333 0.14815\n",
|
||||
" 4 0.25000 0.10547\n",
|
||||
" 5 0.20000 0.08192\n",
|
||||
" 6 0.16667 0.06698\n",
|
||||
" 7 0.14286 0.05665\n",
|
||||
" 8 0.12500 0.04909\n",
|
||||
" 9 0.11111 0.04330\n",
|
||||
"10 0.10000 0.03874\n",
|
||||
"11 0.09091 0.03505\n",
|
||||
"12 0.08333 0.03200\n",
|
||||
"13 0.07692 0.02944\n",
|
||||
"14 0.07143 0.02726\n",
|
||||
"15 0.06667 0.02538\n",
|
||||
"16 0.06250 0.02374\n",
|
||||
"17 0.05882 0.02230\n",
|
||||
"18 0.05556 0.02102\n",
|
||||
"19 0.05263 0.01989\n",
|
||||
"20 0.05000 0.01887\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(' n pace P(win)')\n",
|
||||
"for n in range(1, 21):\n",
|
||||
" print(f'{n:2d} {1 / n:0.5f} {win_prob(1 / n, n):0.5f}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Visualizing Results\n",
|
||||
"\n",
|
||||
"Here is a plot for various numbers of teams of the probability of the first team winning, as a function of the pace that the first team chooses. Each curve is for a different number of teams participating in the race (as shown by the label number)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def annotate(x, y, text):\n",
|
||||
" plt.text(x - 0.04, y - 0.014, text)\n",
|
||||
" plt.plot([x], [y], 'kx')\n",
|
||||
" \n",
|
||||
"for n in [2, 3, 4, 5, 7, 10, 20]:\n",
|
||||
" plt.plot(probabilities, [win_prob(p, n) for p in probabilities])\n",
|
||||
" annotate(1 / n, win_prob(1 / n, n), f'n={n}')\n",
|
||||
"plt.xlabel('First team pace, denoted by P(finish)')\n",
|
||||
"plt.ylabel('P(first team wins)')\n",
|
||||
"plt.grid(True);"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Calculation of Win Probability for All Teams\n",
|
||||
"\n",
|
||||
"What is the full probability distribution of winners over all the teams? I'll define `winners(teams)` to answer the question, where `teams` is a tuple of teams, such as `(1, 2, 3, 4)`. Here's how it works:\n",
|
||||
"- `winners(teams)` gives the probability of winning the race for each team. The result is a `Dist`, a probability distribution of `{team: probability}` pairs. For example, we want `winners((1, 2))` to return `Dist({1: 0.25, 2: 0.75})`.\n",
|
||||
"- A recursive call to `winners(teams, leader, leader_pace)` means that `teams` is the collection of teams who have not raced yet, `leader` is the fastest finishing team among those who have already raced, and `leader_pace` is the pace that the leader rode.\n",
|
||||
"- If there are no `teams` remaining to race, then the winner (with probability 1) is the `leader` so far.\n",
|
||||
"- If there are $n$ teams left to race, then the next team up will choose a pace $p$ that is the faster of $1/n$ and the current leader's pace. \n",
|
||||
"- There are two possible cases to consider:\n",
|
||||
" - That `team` finishes, with probability `p`, and becomes the new leader. The probability distribution in this case is computed by a recursive call `winners(other_teams, team, p)`.\n",
|
||||
" - That `team` cracks, with probability `1 - p`, and the leader is unchanged. The probability distribution in this case is computed by a recursive call `winners(other_teams, leader, leader_pace)`.\n",
|
||||
"- We combine the two cases, weighted by their probability.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class Dist(Counter):\n",
|
||||
" \"\"\"A {outcome: frequency} distribution. Can multiply values by a constant.\"\"\"\n",
|
||||
" def __mul__(self, c): return Dist({x: c * self[x] for x in self})\n",
|
||||
" __rmul__ = __mul__\n",
|
||||
" \n",
|
||||
"@lru_cache(None)\n",
|
||||
"def winners(teams, leader=None, leader_pace=1) -> Dist:\n",
|
||||
" \"\"\"Calculate the probability of each team winning.\"\"\"\n",
|
||||
" if not teams:\n",
|
||||
" return Dist({leader: 1})\n",
|
||||
" else:\n",
|
||||
" team, other_teams = teams[0], teams[1:]\n",
|
||||
" p = min(1/len(teams), leader_pace)\n",
|
||||
" finish = winners(other_teams, team, p)\n",
|
||||
" crack = winners(other_teams, leader, leader_pace)\n",
|
||||
" return Dist(p * finish + (1 - p) * crack)\n",
|
||||
" \n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Dist({2: 0.75, 1: 0.25})"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"winners((1, 2))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Simulation of Winners\n",
|
||||
"\n",
|
||||
"As a check, let's do a simulation where each team chooses their `pace`, and we randomly see if they crack, and eventually see who wins the race. Repeat that for a million races and count the winners. I'll define `race` to be as similar to `winners` as possible, but to follow one of the two paths (finish or crack) at random, rather than following both."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def race(teams, leader=None, leader_pace=1):\n",
|
||||
" \"Simulate a single race with given teams; return the winning team.\"\n",
|
||||
" if not teams:\n",
|
||||
" return leader\n",
|
||||
" else:\n",
|
||||
" team, other_teams = teams[0], teams[1:]\n",
|
||||
" p = min(1/len(teams), leader_pace)\n",
|
||||
" return (race(other_teams, team, p) if random.random() < p else\n",
|
||||
" race(other_teams, leader, leader_pace))\n",
|
||||
"\n",
|
||||
"def races(teams, repeat=1000000):\n",
|
||||
" \"Simulate `repeat` races with given teams; return a probability distribution over winners.\"\n",
|
||||
" counts = Counter(race(teams) for _ in range(repeat))\n",
|
||||
" return Dist({team: counts[team] / repeat for team in teams})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Dist({1: 0.249669, 2: 0.750331})"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"races((1, 2))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Comparison of Calculation to Simulation\n",
|
||||
"\n",
|
||||
"Now we can compare the theoretical calculated results with the simulated results:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" n Calcul Simul Delta\n",
|
||||
" 1 0.0189 0.0192 -0.0003\n",
|
||||
" 2 0.0199 0.0200 -0.0001\n",
|
||||
" 3 0.0210 0.0211 -0.0001\n",
|
||||
" 4 0.0223 0.0223 +0.0000\n",
|
||||
" 5 0.0237 0.0237 +0.0000\n",
|
||||
" 6 0.0253 0.0253 -0.0000\n",
|
||||
" 7 0.0270 0.0270 -0.0000\n",
|
||||
" 8 0.0290 0.0289 +0.0001\n",
|
||||
" 9 0.0313 0.0313 +0.0001\n",
|
||||
" 10 0.0340 0.0340 +0.0000\n",
|
||||
" 11 0.0370 0.0371 -0.0001\n",
|
||||
" 12 0.0406 0.0406 -0.0001\n",
|
||||
" 13 0.0448 0.0446 +0.0001\n",
|
||||
" 14 0.0498 0.0499 -0.0001\n",
|
||||
" 15 0.0560 0.0561 -0.0000\n",
|
||||
" 16 0.0639 0.0639 +0.0000\n",
|
||||
" 17 0.0742 0.0739 +0.0003\n",
|
||||
" 18 0.0889 0.0889 -0.0001\n",
|
||||
" 19 0.1126 0.1122 +0.0003\n",
|
||||
" 20 0.1799 0.1801 -0.0003\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def compare(teams=range(1, 21)):\n",
|
||||
" \"\"\"Print and plot comparison between `races` simulation and `winners` calculation.\"\"\"\n",
|
||||
" wins = winners(teams)\n",
|
||||
" sims = races(teams)\n",
|
||||
" print(f' n Calcul Simul Delta')\n",
|
||||
" for team in teams:\n",
|
||||
" print(f'{team:3d} {wins[team]:.4f} {sims[team]:.4f} {wins[team]-sims[team]:+.4f}')\n",
|
||||
" plt.plot(teams, [wins[i] for i in teams], 'rx', label='calculation', ms=10, mew=2)\n",
|
||||
" plt.plot(teams, [sims[i] for i in teams], 'b+', label='simulation', ms=10, mew=2)\n",
|
||||
" plt.xticks(ticks=teams); plt.grid(True);\n",
|
||||
" plt.xlabel('Team Number'); plt.ylabel('P(win)'); plt.legend()\n",
|
||||
"\n",
|
||||
"compare()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We see there is excellent agreement The `×` and `+` markers are all on top of each other. The agreement extends to a different number of teams:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" n Calcul Simul Delta\n",
|
||||
" 1 0.0387 0.0387 +0.0001\n",
|
||||
" 2 0.0433 0.0434 -0.0001\n",
|
||||
" 3 0.0489 0.0489 -0.0000\n",
|
||||
" 4 0.0561 0.0554 +0.0007\n",
|
||||
" 5 0.0653 0.0657 -0.0004\n",
|
||||
" 6 0.0775 0.0769 +0.0006\n",
|
||||
" 7 0.0945 0.0948 -0.0003\n",
|
||||
" 8 0.1198 0.1198 +0.0001\n",
|
||||
" 9 0.1629 0.1633 -0.0004\n",
|
||||
" 10 0.2929 0.2931 -0.0002\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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Bsya1SEo3IO7t4G9gTFrVt5Hbyoswsu6spdeChJ51arglafLuTPOOIPGxkY2b94MwDvvvMO7777LkUcGU13df//9nR535plncvfddwPBdBrvvvsulZWVtLW1sXnzZt5//30eeeSRfY7LBMPQoUPZtm1bhxFShx56KFu3bt3nmPHjx/O73/2Ot99+m927dzN//nwmTpzY63+ziCRoxozum8crKhIZ5l/aYRFp/9t93HF707qzuyR7GRijRo3iG9/4BhMnTqS6upprrrmGm2++mfr6empqatqbp7LdeeedNDU1MWbMGGpqalizZg0DBgzgpptuYvz48Zx99tkcd9xx+xz3sY99jMsvv5zRo0dzzjnncPLJJ7dvu/TSS7niiivaO7gzPvGJT3DrrbdSV1dHdXU1Y8eOZcqUKft8toiUsFzmBDkQXp3NDbVmzZruJ0WZNat9HpWtr7227/boXCuzZnX/WXlwIM4NlRH7u+6Fkp3/pwuqI101uPeNOshxbqg+81jVXslcqtXX45HRQe0yVxiNjbp7W0RKWmmHBewNgU7a8oHE2v9ERA4kpd1nISIiOVFYiIhILIWFiIjEUlhkMctMASwiIhkKiyLI59TgI0aM4O233+52n+9+97sdlj/zmc/k5dwiUjoUFkVQ6KnBs8Pi6aefLti5RaRvUFgkbPv27Zx33nlUV1czevRoHnrooQ5Tg5eXlzNz5kxGjRrF5MmTee6556itreXoo49m4cLgkeX33Xdfh4cinX/++TQ3N+9zri984QvU1NQwatQofvzjHwNw/fXXs2PHDsaOHctFF13Ufk4IbsicOXMmo0ePZsyYMTz00EMAPPXUU9TW1jJt2jSOO+44LrroIjyJCfJF5IBR8mGR6aP4yEcO7dBXkVm/v30Yjz76KEcccQQrVqxg1apVTJo0qcP27du3c8YZZ7B69WrKy8u58cYbefzxx1mwYEGH50rkYu7cuSxbtoylS5dy1113sXnzZm699VYOPvhgWlpaeOCBBzrs/8tf/pKWlhZWrFjBE088wcyZM/nTn/4EwPPPP88Pf/hD1qxZw7p16/jDH/7Q+1+CiBzwSj4skjZmzBgef/xxrrvuOp566ql9phofOHBge4BUVVUxceJEBgwYwJgxY3o8Hfhdd93V/pCi9evX8+qrr3a7/+9//3suvPBCysrKqKysZOLEiSxZsgQIJhccPnw4/fr1Y+zYsZqaXKTElfwd3JnWla1bt3LooXuvLvLV6nLssceyfPlyFi1axI033siZZ57ZYXt0CvB+/fpx0EEHtb/PTAeey9Tkzc3NPPHEEzzzzDMccsgh1NbW7tfU5Jk6QFOTi4iuLBK3ceNGDjnkEL70pS8xc+ZMli9f3uPPGDFiBC0tLezZs4f169fz3HPP7bPPu+++y5AhQzjkkEN46aWXePbZZ9u3DRgwgA8//HCfYz73uc/x0EMPsXv3bjZt2sSTTz7J+PHje1yfiPR9JX9lkbSVK1cyc+ZM+vXrx4ABA7j77ru59tpre/QZp512GiNHjqSqqorjjz+ek046aZ99Jk2axI9+9COOP/54Pv3pT3PKKae0b5s+fTonnHACJ510Uod+i6lTp/LMM89QXV2NmXHbbbfx8Y9/vPf/WBHpu3KZmvZAePVqivKIzLTcQQNUzofllaYo76gvTP+cT6ojXTW494060BTlvaMRoiIi+1KfhYiIxOrzYeG6VEicfscifV+fDotBgwaxefNmfZklyN3ZvHkzgwYNKnYpIpKgPt1nMXz4cDZs2MCmTZti9925c2fRv/DSUENv6hg0aBDDhw9PsCIRKbY+HRYDBgxg5MiROe3b3NzMiSeemHBF6a8hTXWISHok2gxlZpPM7GUzW2tm13ey/RozW2NmL5jZb83sqMi23WbWEr4WJlmniIh0L7ErCzMrA2YDZwMbgCVmttDdow9yeB4Y5+5/MbOvArcB/zPctsPdxyZVn4iI5C7JK4vxwFp3X+fuHwDzgCnRHdy9yd3/Ei4+C6jhW0QkhZIMiyOB9ZHlDeG6rnwZ+M/I8iAzW2pmz5rZF5IoUEREcmNJDSs1s2nAJHe/LFy+GJjg7g2d7PsloAGY6O7vh+uOdPe3zOxoYDFwpru/lnXcdGA6QGVlZc28efN6Xe+2bdvaHwpULGmoQXWojgOhjjTU0FfqqKurW+bu42J3zGVOkN68gFOBxyLLNwA3dLLfWcCLQEU3n3UfMK2783U2N1RPpGGOlzTU4K46sqmOjtJQRxpqcO8bdZDj3FBJNkMtAY4xs5FmNhC4AOgwqsnMTgTuASa7e1tk/RAzOyh8PxQ4DYh2jIuISAElNhrK3XeZWQPwGFAGzHX31WZ2C0GSLQRuB8qBxvABQG+6+2TgeOAeM9tD0K9yq3ccRSUiIgWU6E157r4IWJS17qbI+7O6OO5pYEyStYmISO769NxQIiKSHwoLERGJpbAQEZFYCgsREYmlsBARkVgKCxERiaWwEBGRWAoLERGJpbAQEZFYCgsREYmlsBARkVgKCxERiaWwEBGRWAoLERGJpbAQEZFYCgsREYmlsBARkVgKCxERiaWLine truncated
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"compare(range(1, 11))"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"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.7.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
+870
-807
File diff suppressed because it is too large.
Load diff
+453
-437
@@ -1,440 +1,456 @@
|
||||
Ride Thu, 6/14/2012 Coyote Creek Century with Juliet 8:08:15 100.07 mi 1,513 ft
|
||||
Ride Sat, 5/13/2017 Morgan Hill iCare Classic 7:27:21 100.05 mi 4,596 ft
|
||||
Ride Sat, 5/12/2018 ICare Classic, Morgan Hill 6:47:46 91.29 mi 4,160 ft
|
||||
Ride Sat, 5/6/2017 Wine Country Century 7:15:22 89.49 mi 5,246 ft
|
||||
Ride Fri, 8/10/2018 Bike Ride Northwest Day 6 6:14:18 84.70 mi 4,380 ft
|
||||
Ride Sat, 10/1/2016 Half Moon Bay overnight campout 7:30:38 80.07 mi 6,039 ft
|
||||
Ride Tue, 8/7/2018 Bike Ride Northwest Day 3 6:10:39 78.96 mi 5,092 ft
|
||||
Ride Sun, 6/15/2014 Sierra to the Sea Day 1 5:34:06 78.53 mi 4,777 ft
|
||||
Ride Sat, 4/23/2016 Wildflower Century 5:36:10 77.22 mi 4,193 ft
|
||||
Ride Mon, 6/16/2014 Sierra to the Sea Day 2 5:35:06 74.68 mi 2,451 ft
|
||||
Ride Fri, 8/12/2016 Half Moon Bay / Harvey's 73/7300 Birthday Ride 6:44:14 74.35 mi 6,610 ft
|
||||
Ride Wed, 10/14/2015 Half Moon Bay 6:07:55 72.97 mi 7,644 ft
|
||||
Ride Fri, 1/27/2017 Morning Ride 5:16:50 70.07 mi 2,539 ft
|
||||
Ride Tue, 6/17/2014 Sierra to the Sea Day 3 4:48:41 68.64 mi 825 ft
|
||||
Ride Thu, 8/9/2018 Bike Ride Northwest Day 5 4:58:07 68.41 mi 3,862 ft
|
||||
Ride Sat, 4/15/2017 Pescadero 6:13:12 68.34 mi 6,130 ft
|
||||
Ride Sun, 6/4/2017 Sequoia Challenge 6:17:25 66.52 mi 7,520 ft
|
||||
Ride Wed, 8/5/2015 08/05/2015 Palo Alto, CA 4:56:15 66.33 mi 2,054 ft
|
||||
Ride Fri, 8/28/2015 Pescadaro via OLH 5:18:51 66.01 mi 6,137 ft
|
||||
Ride Fri, 11/20/2015 Los Gatos 5:29:40 65.73 mi 5,380 ft
|
||||
Ride Sun, 6/3/2018 The Sequoia 5:58:15 64.92 mi 6,677 ft
|
||||
Ride Thu, 6/19/2014 Sierra to the Sea Day 5 4:32:55 63.69 mi 2,584 ft
|
||||
Ride Mon, 8/3/2015 08/03/2015 Palo Alto, CA 4:45:09 63.61 mi 1,877 ft
|
||||
Ride Fri, 6/10/2016 Morning Ride 4:21:38 62.84 mi 3,196 ft
|
||||
Ride Sun, 6/2/2013 Woodside Loop and Baylands 4:27:42 62.14 mi 2,169 ft
|
||||
Ride Wed, 6/12/2013 ride 4:39:46 61.39 mi 2,207 ft
|
||||
Ride Sat, 4/18/2015 Tunitas + Lobitos Creeks 5:14:38 61.27 mi 6,611 ft
|
||||
Ride Sat, 11/11/2017 Los Gatos / Bay Trail 4:26:24 60.74 mi 1,316 ft
|
||||
Ride Sun, 4/17/2016 La Honda / Skyline 5:10:41 60.35 mi 4,551 ft
|
||||
Ride Mon, 7/3/2017 Los Gatos / Bay Trail 4:25:41 60.28 mi 1,329 ft
|
||||
Ride Sat, 4/16/2016 OLH - San Gregorio - Tunitas 4:57:40 60.12 mi 4,744 ft
|
||||
Ride Sun, 1/29/2017 Los Gatos / Guadalupe / San Tomas / Bay Trails 4:33:38 60.11 mi 1,447 ft
|
||||
Ride Mon, 11/26/2018 Lunch Ride Los Gatos 4:21:35 60.03 mi 1,070 ft
|
||||
Ride Sat, 10/31/2015 Saratoga Ramble 5:01:57 59.10 mi 3,528 ft
|
||||
Ride Sat, 8/18/2018 Tour de Menlo 4:03:48 58.81 mi 2,467 ft
|
||||
Ride Sat, 4/26/2014 OLH - Tunitas Creek 5:15:26 58.69 mi 6,742 ft
|
||||
Ride Sat, 6/15/2013 Palo Alto to Santa Cruz 4:27:54 58.42 mi 4,431 ft
|
||||
Ride Sat, 10/11/2014 OLH / La Honda / Tunitas 5:05:23 58.29 mi 6,044 ft
|
||||
Ride Sat, 7/9/2016 Lunch Ride 3:50:23 58.23 mi 4,042 ft
|
||||
Ride Wed, 6/18/2014 Sierra to the Sea Day 4 4:57:53 57.64 mi 5,561 ft
|
||||
Ride Tue, 7/7/2015 07/07/2015 Palo Alto, CA 4:13:20 57.60 mi 1,280 ft
|
||||
Ride Mon, 8/6/2018 Bike Ride Northwest Day 2 4:35:48 57.58 mi 4,514 ft
|
||||
Ride Fri, 6/20/2014 Sierra to the Sea Day 6 4:33:39 56.91 mi 4,453 ft
|
||||
Ride Sat, 6/10/2017 Los Gatos / Creek Trails 3:50:24 56.28 mi 1,365 ft
|
||||
Ride Sat, 3/4/2017 Lunch Ride 3:58:25 56.26 mi 1,378 ft
|
||||
Ride Sun, 8/5/2018 Bike Ride Northwest Day 1 3:34:42 55.77 mi 1,824 ft
|
||||
Ride Sat, 8/13/2016 Petaluma - Point Reyes 4:30:12 54.75 mi 5,286 ft
|
||||
Ride Sun, 6/7/2015 Tour de Cure 3:59:47 54.65 mi 2,748 ft
|
||||
Ride Wed, 12/16/2015 Los Gatos 4:00:41 53.86 mi 2,595 ft
|
||||
Ride Fri, 6/2/2017 Morning Ride 3:57:45 53.49 mi 1,375 ft
|
||||
Ride Sun, 6/8/2014 Tour de Cure 75K 4:03:05 53.10 mi 2,596 ft
|
||||
Ride Sun, 11/6/2016 Los Gatos 3:38:28 52.49 mi 1,263 ft
|
||||
Ride Fri, 2/10/2017 Morning Ride 3:52:39 52.02 mi 1,739 ft
|
||||
Ride Sat, 2/23/2019 Crystal Springs Dam 4:02:11 51.93 mi 1,946 ft
|
||||
Ride Fri, 8/11/2017 Saratoga with Peter H 4:30:31 51.74 mi 2,871 ft
|
||||
Ride Sat, 2/4/2017 Canada Rd 3:46:34 51.66 mi 1,762 ft
|
||||
Ride Sun, 6/9/2013 Silicon Valley Tour de Cure 75K 3:58:37 51.63 mi 2,929 ft
|
||||
Ride Sun, 9/3/2017 Morning Ride 4:22:45 51.31 mi 2,526 ft
|
||||
Ride Mon, 7/27/2015 Palo Alto Cycling 3:48:23 50.93 mi 1,306 ft
|
||||
Ride Wed, 7/29/2015 07/29/2015 Palo Alto, CA 3:47:53 50.92 mi 1,873 ft
|
||||
Ride Sun, 6/26/2016 Afternoon Ride 3:16:33 50.78 mi 1,181 ft
|
||||
Ride Mon, 8/10/2015 08/10/2015 Palo Alto, CA 3:41:54 50.73 mi 1,325 ft
|
||||
Ride Sat, 10/15/2016 Los Gatos 3:28:16 50.64 mi 1,368 ft
|
||||
Ride Tue, 12/19/2017 Los Gatos 3:46:37 50.49 mi 1,929 ft
|
||||
Ride Fri, 6/28/2013 Kaffeehaus San Mateo 3:38:11 50.38 mi 1,028 ft
|
||||
Ride Thu, 10/5/2017 Big Sur 4:33:33 50.38 mi 4,528 ft
|
||||
Ride Sun, 11/20/2016 Lunch Ride 3:32:01 50.13 mi 1,847 ft
|
||||
Ride Sat, 1/13/2018 Canada to Coyote Point 3:59:24 50.09 mi 1,499 ft
|
||||
Ride Fri, 1/13/2017 Los Gatos Creek 3:52:28 50.01 mi 1,598 ft
|
||||
Ride Sun, 6/11/2017 Tour de Cure with Juliet 4:11:01 49.49 mi 2,713 ft
|
||||
Ride Sun, 9/11/2016 Santa Cruz via Mountain Charlie Rd 4:03:04 48.78 mi 3,300 ft
|
||||
Ride Sat, 7/23/2016 Morning Ride 3:30:50 48.45 mi 1,063 ft
|
||||
Ride Fri, 3/25/2016 Morning Ride 3:39:53 47.93 mi 2,438 ft
|
||||
Ride Sat, 8/20/2016 Tour de Menlo with Juliet 3:30:24 47.52 mi 2,133 ft
|
||||
Ride Sat, 8/15/2015 08/15/2015 Palo Alto, CA 3:35:03 47.12 mi 1,158 ft
|
||||
Ride Wed, 11/11/2015 Morning Ride 3:45:01 46.54 mi 2,559 ft
|
||||
Ride Fri, 7/21/2017 Morning Ride 3:35:00 46.19 mi 748 ft
|
||||
Ride Sat, 9/2/2017 Canada / Sheep Camp Trail 3:45:45 45.72 mi 2,034 ft
|
||||
Ride Mon, 1/19/2015 Canada Rd, etc. 2:57:14 45.64 mi 1,836 ft
|
||||
Ride Sun, 11/27/2016 Morning Ride 3:04:11 45.60 mi 1,378 ft
|
||||
Ride Sun, 7/2/2017 Afternoon Ride 3:10:38 45.30 mi 581 ft
|
||||
Ride Sat, 10/3/2015 Los Gatos 2:59:26 45.21 mi 1,148 ft
|
||||
Ride Sat, 7/2/2016 Morning Ride 3:23:20 45.21 mi 1,991 ft
|
||||
Ride Mon, 1/16/2017 Morning Ride 3:34:28 45.15 mi 1,434 ft
|
||||
Ride Sun, 1/15/2017 Lunch Ride 3:18:47 45.12 mi 1,873 ft
|
||||
Ride Sun, 4/9/2017 Lunch Ride 3:10:28 44.76 mi 636 ft
|
||||
Ride Sun, 10/15/2017 Los Gatos 2:51:53 44.71 mi 1,437 ft
|
||||
Ride Tue, 10/8/2013 work 3:22:55 44.58 mi 961 ft
|
||||
Ride Thu, 7/16/2015 07/16/2015 Mountain View, California 3:29:45 44.34 mi 1,339 ft
|
||||
Ride Sat, 10/28/2017 Mindego Ridge Winery 4:17:45 44.28 mi 4,331 ft
|
||||
Ride Sun, 9/2/2018 Saratoga 3:31:02 44.04 mi 1,900 ft
|
||||
Ride Fri, 9/23/2016 Los Gatos 2:53:22 43.93 mi 1,339 ft
|
||||
Ride Sat, 7/25/2015 07/25/2015 Palo Alto, California 4:02:27 43.62 mi 4,819 ft
|
||||
Ride Sat, 7/8/2017 Dumbarton / Niles 3:14:55 43.54 mi 627 ft
|
||||
Ride Sat, 6/9/2018 Kings Mountain 3:41:47 43.47 mi 3,543 ft
|
||||
Ride Fri, 3/10/2017 Morning Ride 4:13:51 43.22 mi 4,554 ft
|
||||
Ride Sat, 3/26/2016 Morning Ride 3:36:57 43.18 mi 3,173 ft
|
||||
Ride Sun, 10/18/2015 Afternoon Ride 3:00:02 43.04 mi 2,323 ft
|
||||
Ride Sat, 9/20/2014 Kings Mountain 3:26:36 43.00 mi 3,299 ft
|
||||
Ride Sat, 8/11/2018 Bike Ride Northwest Day 7 3:25:20 42.39 mi 2,241 ft
|
||||
Ride Sat, 3/18/2017 Morning Ride 3:37:01 42.29 mi 2,231 ft
|
||||
Ride Sat, 7/11/2015 07/11/2015 Walnut Creek, California 3:09:38 42.29 mi 3,284 ft
|
||||
Ride Thu, 1/22/2015 OLH, etc. 3:25:09 42.15 mi 2,957 ft
|
||||
Ride Sun, 11/9/2014 11/09/2014 Palo Alto, CA 3:24:57 42.10 mi 3,096 ft
|
||||
Ride Sun, 5/15/2016 Afternoon Ride 2:55:51 41.98 mi 1,693 ft
|
||||
Ride Sat, 3/25/2017 Morning Ride 3:37:45 41.94 mi 2,874 ft
|
||||
Ride Sun, 6/14/2015 06/14/2015 Palo Alto, CA 2:46:38 41.67 mi 1,086 ft
|
||||
Ride Sat, 7/28/2018 Coyote Hills / Dumbarton 3:25:39 41.66 mi 1,463 ft
|
||||
Ride Sun, 8/7/2016 Afternoon Ride 2:42:58 41.19 mi 1,526 ft
|
||||
Ride Sat, 2/11/2017 Morning Ride 3:19:34 41.16 mi 2,172 ft
|
||||
Ride Sun, 12/3/2017 Bay Trail 2:53:58 40.86 mi 568 ft
|
||||
Ride Mon, 5/29/2017 Morning Ride 3:52:04 40.83 mi 3,678 ft
|
||||
Ride Sat, 2/18/2017 Lunch Ride 3:01:13 40.82 mi 630 ft
|
||||
Ride Wed, 11/9/2016 Lunch Ride 2:52:06 40.82 mi 1,667 ft
|
||||
Ride Sat, 5/20/2017 Lunch Ride 3:07:43 40.82 mi 709 ft
|
||||
Ride Sat, 3/24/2018 Morning Ride 2:56:29 40.69 mi 561 ft
|
||||
Ride Sun, 11/19/2017 Belmont / Bay Trail 2:53:36 40.60 mi 453 ft
|
||||
Ride Sun, 3/26/2017 Lunch Ride 2:54:38 40.57 mi 518 ft
|
||||
Ride Sat, 5/25/2019 Crestview 3:17:16 40.56 mi 2,890 ft
|
||||
Ride Sat, 9/16/2017 Tour de Coop 3:24:53 40.55 mi 1,125 ft
|
||||
Ride Thu, 9/3/2015 Morning Ride 3:02:58 40.40 mi 1,181 ft
|
||||
Ride Sun, 2/5/2017 Morning Ride 2:53:48 40.40 mi 1,608 ft
|
||||
Ride Sun, 10/23/2016 Lunch Ride 2:41:33 40.33 mi 1,699 ft
|
||||
Ride Sat, 12/8/2018 Morning Ride 3:27:10 40.33 mi 2,300 ft
|
||||
Ride Sun, 2/12/2017 Lunch Ride 3:39:38 40.27 mi 2,717 ft
|
||||
Ride Sat, 12/2/2017 Dumbarton / Niles 2:57:48 40.26 mi 610 ft
|
||||
Ride Sun, 11/12/2017 Morning Ride 2:54:57 40.24 mi 656 ft
|
||||
Ride Sat, 11/15/2014 Kings Mountain again 3:25:14 40.18 mi 2,952 ft
|
||||
Ride Sun, 10/9/2016 Lunch Ride 2:41:06 40.14 mi 1,106 ft
|
||||
Ride Mon, 11/6/2017 Morning Ride 2:48:12 40.13 mi 509 ft
|
||||
Ride Sat, 12/17/2016 Morning Ride 3:11:31 40.10 mi 2,133 ft
|
||||
Ride Fri, 1/12/2018 Morning Ride 2:58:30 40.09 mi 427 ft
|
||||
Ride Sun, 12/18/2016 Morning Ride 3:04:01 40.03 mi 1,886 ft
|
||||
Ride Sat, 6/11/2016 Ring of Fire (Partial; Garmin troubles) 3:00:00 40.00 mi 0 ft
|
||||
Ride Sun, 1/25/2015 Canada Rd 2:45:51 39.99 mi 1,772 ft
|
||||
Ride Sat, 6/21/2014 Sierra to the Sea Day 7 3:36:53 39.77 mi 3,325 ft
|
||||
Ride Sat, 9/23/2017 Canada / Crestview 3:25:13 39.77 mi 2,444 ft
|
||||
Ride Fri, 12/27/2013 OLH to Page Mill 3:24:14 39.73 mi 3,365 ft
|
||||
Ride Sat, 4/30/2016 Ridge to Bridge MTB 5:40:57 39.67 mi 5,377 ft
|
||||
Ride Fri, 10/30/2015 OLH - West Alpine 3:28:46 39.51 mi 4,505 ft
|
||||
Ride Sat, 9/5/2015 Ring of Fire 3:31:55 39.18 mi 3,553 ft
|
||||
Ride Wed, 8/28/2013 Train Track Crash 2:50:55 39.16 mi 928 ft
|
||||
Ride Sun, 2/22/2015 Canada Rd Plus 2:57:25 39.14 mi 1,834 ft
|
||||
Ride Sat, 4/8/2017 Lunch Ride 3:28:52 39.09 mi 3,117 ft
|
||||
Ride Sat, 11/14/2015 Morning Ride 3:09:31 39.00 mi 2,254 ft
|
||||
Ride Sun, 3/1/2015 Canada Rd 2:38:11 38.81 mi 1,856 ft
|
||||
Ride Wed, 11/22/2017 Morning Ride 2:55:22 38.77 mi 892 ft
|
||||
Ride Sat, 10/29/2016 Morning Ride 3:14:22 38.72 mi 2,201 ft
|
||||
Ride Sat, 4/6/2019 Coyote Hills / Dumbarton 2:50:52 38.71 mi 650 ft
|
||||
Ride Wed, 7/22/2015 07/22/2015 Palo Alto, CA 2:55:45 38.68 mi 627 ft
|
||||
Ride Mon, 6/22/2015 06/22/2015 Palo Alto, CA 2:51:10 38.55 mi 725 ft
|
||||
Ride Fri, 12/16/2016 Morning Ride 2:54:02 38.46 mi 1,053 ft
|
||||
Ride Sun, 10/7/2018 Westridge, Canada with Ted 3:09:25 38.29 mi 1,667 ft
|
||||
Ride Sat, 2/21/2015 Kings Mountain 3:13:05 38.21 mi 3,015 ft
|
||||
Ride Sat, 9/12/2015 Morning Ride 3:06:03 38.20 mi 3,342 ft
|
||||
Ride Sun, 2/15/2015 Canada Rd 2:35:29 38.06 mi 1,470 ft
|
||||
Ride Sun, 11/16/2014 Canada Rd 2:30:41 37.88 mi 1,548 ft
|
||||
Ride Sun, 10/5/2014 Canada Road Sunday 2:39:39 37.84 mi 1,644 ft
|
||||
Ride Mon, 11/23/2015 Home from Pigeon Point 3:29:29 37.70 mi 3,133 ft
|
||||
Ride Sat, 1/6/2018 Morning Ride 2:53:45 37.66 mi 1,503 ft
|
||||
Ride Sun, 1/7/2018 Dumbarton / Niles 2:39:17 37.64 mi 472 ft
|
||||
Ride Sun, 12/1/2013 Mt Hamilton 3:47:01 37.56 mi 4,921 ft
|
||||
Ride Sun, 3/18/2018 Lunch Ride 2:47:48 37.52 mi 827 ft
|
||||
Ride Sat, 9/22/2018 Morning Ride 2:52:01 37.45 mi 1,798 ft
|
||||
Ride Sun, 10/22/2017 Afternoon Ride 2:33:27 37.41 mi 1,024 ft
|
||||
Ride Sun, 5/4/2014 MTV-Woodside 2:41:21 37.41 mi 1,495 ft
|
||||
Ride Sat, 1/23/2016 Morning Ride 3:39:10 37.35 mi 2,949 ft
|
||||
Ride Sun, 9/16/2018 Dumbarton / Niles 2:38:41 37.26 mi 722 ft
|
||||
Ride Sun, 6/10/2018 Canada / Emerald Hills 3:21:31 37.26 mi 2,621 ft
|
||||
Ride Mon, 5/4/2015 Commute 2:43:06 37.25 mi 189 ft
|
||||
Ride Sat, 7/7/2018 Runymede Trail 3:01:51 37.07 mi 1,834 ft
|
||||
Ride Sun, 12/11/2016 Afternoon Ride 2:34:13 37.05 mi 1,463 ft
|
||||
Ride Sun, 11/29/2015 Mt Hamilton 3:40:48 37.00 mi 4,902 ft
|
||||
Ride Sat, 1/30/2016 Morning Ride 2:31:37 36.88 mi 1,345 ft
|
||||
Ride Mon, 2/18/2019 Niles 2:39:39 36.83 mi 571 ft
|
||||
Ride Sun, 1/21/2018 Alviso 2:35:58 36.82 mi 404 ft
|
||||
Ride Sat, 8/26/2017 Lunch Ride 2:41:49 36.71 mi 1,115 ft
|
||||
Ride Sat, 9/13/2014 Emerald Hills 2:55:30 36.71 mi 2,143 ft
|
||||
Ride Sun, 5/22/2016 Lunch Ride 2:12:20 36.68 mi 1,332 ft
|
||||
Ride Sat, 11/25/2017 Mt. Hamilton 3:41:22 36.65 mi 4,806 ft
|
||||
Ride Fri, 10/13/2017 Voyage 2:46:15 36.63 mi 505 ft
|
||||
Ride Tue, 5/12/2015 05/12/2015 Palo Alto, CA 2:47:15 36.54 mi 499 ft
|
||||
Ride Mon, 4/20/2015 Commute 2:57:44 36.31 mi 119 ft
|
||||
Ride Fri, 5/13/2016 Morning Ride 2:41:03 36.21 mi 456 ft
|
||||
Ride Sat, 12/3/2016 Morning Ride 3:08:37 36.20 mi 2,881 ft
|
||||
Ride Sun, 12/7/2014 12/07/2014 Palo Alto, CA 2:43:35 36.16 mi 1,569 ft
|
||||
Ride Sun, 9/30/2018 Canada 2:48:41 36.12 mi 1,480 ft
|
||||
Ride Fri, 3/30/2018 Morning Ride 2:38:07 36.11 mi 285 ft
|
||||
Ride Sun, 11/5/2017 Lunch Ride 3:24:01 35.91 mi 2,231 ft
|
||||
Ride Sun, 2/8/2015 Commute 2:28:46 35.85 mi 1,142 ft
|
||||
Ride Tue, 6/26/2018 Return from Chabot Lake with Juliet MTB 3:21:30 35.81 mi 817 ft
|
||||
Ride Sat, 3/30/2019 Morning Ride 3:23:55 35.78 mi 2,861 ft
|
||||
Ride Sun, 11/22/2015 Pigeon Point 3:14:41 35.73 mi 3,230 ft
|
||||
Ride Mon, 9/19/2016 Sunnyvale 2:37:04 35.68 mi 745 ft
|
||||
Ride Thu, 4/9/2015 Palo Alto Cycling 2:35:47 35.51 mi 117 ft
|
||||
Ride Sun, 8/21/2016 Canada 2:20:21 35.42 mi 1,348 ft
|
||||
Ride Tue, 10/28/2014 Woodside 2:36:36 35.39 mi 1,560 ft
|
||||
Ride Sun, 1/27/2019 Crystal Springs Dam Bridge 2:28:21 35.38 mi 1,480 ft
|
||||
Ride Thu, 4/26/2018 San Mateo 2:38:26 35.31 mi 666 ft
|
||||
Ride Thu, 12/22/2016 Half Moon Bay 2:53:00 35.26 mi 2,671 ft
|
||||
Ride Tue, 6/7/2016 Los Altos 2:41:13 35.18 mi 1,070 ft
|
||||
Ride Sat, 3/15/2014 Canada - Steve 2:51:50 35.01 mi 1,607 ft
|
||||
Ride Sun, 12/6/2015 Canada Rd 2:15:05 34.67 mi 1,237 ft
|
||||
Ride Wed, 9/13/2017 Healdburg / Jimtown 2:08:03 34.45 mi 912 ft
|
||||
Ride Thu, 10/2/2014 10/02/2014 Mountain View, California 3:08:06 34.28 mi 2,864 ft
|
||||
Ride Sat, 2/1/2014 OLH to Page Mill 3:07:43 34.26 mi 3,099 ft
|
||||
Ride Sat, 10/21/2017 Pescadero 5:20:08 67.05 mi 4,938 ft
|
||||
Ride Sat, 7/30/2016 Morning Ride 2:50:15 34.19 mi 2,799 ft
|
||||
Ride Mon, 7/10/2017 Morning Ride 2:38:59 34.18 mi 984 ft
|
||||
Ride Thu, 7/31/2014 Work commute 2:36:53 34.15 mi 564 ft
|
||||
Ride Sat, 12/15/2018 Lunch Ride 2:50:19 34.10 mi 1,683 ft
|
||||
Ride Sat, 1/20/2018 Afternoon Ride 2:28:18 34.02 mi 525 ft
|
||||
Ride Sun, 12/4/2016 Morning Ride 2:29:29 34.00 mi 1,161 ft
|
||||
Ride Sat, 10/8/2016 Lunch Ride 2:25:17 33.95 mi 1,375 ft
|
||||
Ride Thu, 10/16/2014 Commute 2:40:01 33.76 mi 615 ft
|
||||
Ride Fri, 11/11/2016 OLH 2:44:59 33.69 mi 2,175 ft
|
||||
Ride Sat, 9/3/2016 Morning Ride 3:07:28 33.65 mi 2,792 ft
|
||||
Ride Sun, 4/19/2015 Canada Rd 2:23:17 33.56 mi 1,466 ft
|
||||
Ride Sat, 8/12/2017 Afternoon Ride 2:41:24 33.54 mi 633 ft
|
||||
Ride Sat, 2/14/2015 Rancho San Antonio 2:45:23 33.53 mi 2,075 ft
|
||||
Ride Sat, 2/27/2016 Morning Ride 2:47:15 33.51 mi 2,005 ft
|
||||
Ride Sun, 9/17/2017 Canada and off road 2:36:04 33.50 mi 1,772 ft
|
||||
Ride Sat, 2/24/2018 Los Altos Hills on and off road 3:10:47 33.49 mi 2,343 ft
|
||||
Ride Sun, 1/26/2014 Canada Rd 2:05:59 33.12 mi 1,446 ft
|
||||
Ride Sat, 4/27/2019 Morning Ride 2:31:48 33.04 mi 1,722 ft
|
||||
Ride Sun, 5/12/2019 Peninsula Bike Trail to Bay Trail 2:29:05 33.00 mi 846 ft
|
||||
Ride Sun, 8/20/2017 Lunch Ride 2:23:02 32.96 mi 1,407 ft
|
||||
Ride Sun, 3/6/2016 Lunch Ride 2:57:54 32.86 mi 2,566 ft
|
||||
Ride Sun, 3/5/2017 Lunch Ride 2:57:09 32.82 mi 1,713 ft
|
||||
Ride Sat, 12/16/2017 Morning Ride 2:48:58 32.72 mi 2,320 ft
|
||||
Ride Tue, 8/23/2016 Morning Ride 2:32:15 32.61 mi 354 ft
|
||||
Ride Fri, 12/9/2016 Morning Ride 3:04:52 32.55 mi 2,365 ft
|
||||
Ride Sun, 11/1/2015 Lunch Ride 2:20:35 32.49 mi 1,553 ft
|
||||
Ride Sat, 4/14/2018 Lunch Ride 2:21:49 32.43 mi 666 ft
|
||||
Ride Fri, 1/26/2018 Morning Ride 2:23:38 32.41 mi 295 ft
|
||||
Ride Sun, 8/16/2015 08/16/2015 Palo Alto, CA 2:29:53 32.40 mi 1,650 ft
|
||||
Ride Sat, 5/9/2015 OLH 2:30:17 32.33 mi 2,788 ft
|
||||
Ride Wed, 1/31/2018 Morning Ride 3:04:15 32.28 mi 2,526 ft
|
||||
Ride Sat, 4/4/2015 Rancho San Antonio 2:55:37 32.26 mi 2,136 ft
|
||||
Ride Sat, 3/16/2019 Morning Ride 2:58:43 32.26 mi 2,316 ft
|
||||
Ride Sun, 12/27/2015 Canada Rd with Juliet 2:24:56 32.22 mi 1,491 ft
|
||||
Ride Mon, 5/18/2015 05/18/2015 Palo Alto, CA 2:24:45 32.22 mi 807 ft
|
||||
Ride Sun, 4/8/2018 Lunch Ride 2:21:54 32.18 mi 1,296 ft
|
||||
Ride Sun, 5/3/2015 Canada Rd 2:12:24 32.02 mi 1,384 ft
|
||||
Ride Wed, 6/17/2015 06/17/2015 Mountain View, California 2:13:42 32.01 mi 650 ft
|
||||
Ride Sat, 11/7/2015 Crystal Springs Part 1 2:15:00 32.00 mi 0 ft
|
||||
Ride Sun, 8/23/2015 Afternoon Ride 2:24:29 31.90 mi 2,444 ft
|
||||
Ride Sun, 4/12/2015 Palo Alto Cycling 2:01:49 31.76 mi 1,210 ft
|
||||
Ride Sun, 11/13/2016 Afternoon Ride 2:23:46 31.71 mi 1,273 ft
|
||||
Ride Fri, 10/7/2016 Morning Ride 2:26:46 31.65 mi 2,382 ft
|
||||
Ride Thu, 8/15/2013 08/15/2013 Palo Alto, CA 2:19:27 31.64 mi 555 ft
|
||||
Ride Wed, 10/30/2013 work 2:22:00 31.55 mi 995 ft
|
||||
Ride Sun, 9/18/2016 Morning Ride 2:20:46 31.48 mi 1,506 ft
|
||||
Ride Tue, 6/18/2013 work etc (headwinds) 2:03:34 31.48 mi 809 ft
|
||||
Ride Sun, 3/11/2018 Lunch Ride 2:23:14 31.42 mi 686 ft
|
||||
Ride Sat, 3/12/2016 Lunch Ride 2:10:01 31.39 mi 1,198 ft
|
||||
Ride Sun, 1/22/2017 Dumbarton 2:23:36 31.27 mi 591 ft
|
||||
Ride Sat, 9/29/2018 Kings 2:36:47 31.23 mi 1,949 ft
|
||||
Ride Sat, 3/23/2019 Morning Ride 2:33:27 31.19 mi 1,529 ft
|
||||
Ride Sat, 4/13/2019 Alviso 2:10:04 31.18 mi 397 ft
|
||||
Ride Mon, 11/10/2014 Commute 2:22:39 31.18 mi 800 ft
|
||||
Ride Mon, 5/25/2015 05/25/2015 Palo Alto, CA 2:19:26 31.14 mi 1,591 ft
|
||||
Ride Mon, 2/16/2015 Portola Valley Loop 2:09:15 31.11 mi 1,283 ft
|
||||
Ride Wed, 9/21/2016 Morning Ride 2:16:04 31.09 mi 551 ft
|
||||
Ride Sat, 3/11/2017 Afternoon Ride 2:19:05 31.04 mi 1,368 ft
|
||||
Ride Sun, 7/29/2018 Lunch Ride 2:26:56 30.92 mi 1,578 ft
|
||||
Ride Fri, 11/10/2017 Lunch Ride 2:20:46 30.92 mi 312 ft
|
||||
Ride Mon, 6/12/2017 Morning Ride 2:20:16 30.90 mi 554 ft
|
||||
Ride Thu, 12/29/2016 Morning Ride 2:13:46 30.86 mi 1,083 ft
|
||||
Ride Sat, 4/22/2017 Lunch Ride 2:21:31 30.80 mi 1,237 ft
|
||||
Ride Mon, 9/7/2015 Healdsburg 2:09:43 30.76 mi 1,037 ft
|
||||
Ride Sat, 1/14/2017 Afternoon Ride 2:12:37 30.71 mi 466 ft
|
||||
Ride Mon, 9/4/2017 Kings Mountain 2:40:57 30.69 mi 2,431 ft
|
||||
Ride Sun, 8/9/2015 08/09/2015 Palo Alto, CA 2:15:39 30.66 mi 1,348 ft
|
||||
Ride Sat, 12/12/2015 Lunch Ride 2:16:20 30.56 mi 1,434 ft
|
||||
Ride Thu, 12/28/2017 Morning Ride 2:17:38 30.54 mi 430 ft
|
||||
Ride Mon, 1/30/2017 Morning Ride 2:19:47 30.49 mi 696 ft
|
||||
Ride Sun, 8/27/2017 Lunch Ride 2:16:36 30.47 mi 774 ft
|
||||
Ride Fri, 10/12/2018 Lunch Ride 2:16:25 30.44 mi 348 ft
|
||||
Ride Mon, 6/25/2018 Chabot Lake Overnight Bike packing with Juliet MTB 3:02:49 30.41 mi 1,250 ft
|
||||
Ride Sat, 2/28/2015 Palo Alto Cycling 2:40:24 30.37 mi 2,535 ft
|
||||
Ride Sat, 6/24/2017 Afternoon Ride 2:06:05 30.31 mi 338 ft
|
||||
Ride Tue, 6/2/2015 06/02/2015 Mountain View, California 2:09:58 30.30 mi 784 ft
|
||||
Ride Mon, 5/27/2019 Canada / Sheep camp / Water dog lake 2:37:11 30.27 mi 1,444 ft
|
||||
Ride Sat, 1/2/2016 Los Altos Hills 2:29:48 30.27 mi 1,952 ft
|
||||
Ride Fri, 6/23/2017 Morning Ride 2:17:16 30.27 mi 515 ft
|
||||
Ride Sun, 4/2/2017 Morning Ride 2:20:47 30.23 mi 407 ft
|
||||
Ride Sat, 8/25/2018 Morning Ride 2:22:42 30.22 mi 2,257 ft
|
||||
Ride Sun, 9/14/2014 09/14/2014 Palo Alto, CA 2:06:48 30.18 mi 1,204 ft
|
||||
Ride Sat, 8/19/2017 Lunch Ride 2:06:39 30.17 mi 627 ft
|
||||
Ride Sun, 3/12/2017 Morning Ride 2:18:13 30.16 mi 600 ft
|
||||
Ride Sat, 11/21/2015 Morning Ride 2:20:54 30.16 mi 1,499 ft
|
||||
Ride Wed, 10/12/2016 Morning Ride 2:19:51 30.11 mi 328 ft
|
||||
Ride Sun, 7/8/2018 Lunch Ride 2:12:19 30.04 mi 768 ft
|
||||
Ride Tue, 11/22/2016 Morning Ride 2:18:57 30.04 mi 518 ft
|
||||
Ride Wed, 3/4/2015 Commute 2:15:56 30.02 mi 1,125 ft
|
||||
Ride Sun, 3/25/2018 Dumbarton / Coyote Hills / Middle of the Bay 2:31:51 30.01 mi 709 ft
|
||||
Ride Tue, 5/31/2016 Google Bike around campus 2:30:00 30.00 mi 0 ft
|
||||
Ride Tue, 2/14/2017 Morning Ride 2:22:16 29.92 mi 463 ft
|
||||
Ride Sun, 5/31/2015 05/31/2015 Palo Alto, California 2:35:24 29.85 mi 2,568 ft
|
||||
Ride Tue, 11/1/2016 Morning Ride 2:15:55 29.78 mi 515 ft
|
||||
Ride Sun, 6/5/2016 Morning Ride 2:18:02 29.70 mi 1,293 ft
|
||||
Ride Sun, 5/26/2019 San Carlos / Bay Trail 2:17:51 29.55 mi 620 ft
|
||||
Ride Thu, 8/13/2015 08/13/2015 Palo Alto, CA 2:13:58 29.51 mi 778 ft
|
||||
Ride Sun, 7/14/2013 07/14/2013 Palo Alto, CA 2:23:07 29.34 mi 739 ft
|
||||
Ride Sun, 4/3/2016 Lunch Ride 2:03:38 29.31 mi 810 ft
|
||||
Ride Mon, 3/9/2015 Palo Alto Cycling 1:57:50 29.21 mi 115 ft
|
||||
Ride Fri, 2/23/2018 Morning Ride 2:08:28 29.21 mi 166 ft
|
||||
Ride Sat, 10/7/2017 Afternoon Ride 2:02:43 29.18 mi 394 ft
|
||||
Ride Sat, 1/17/2015 Palo Alto Cycling 2:08:38 29.06 mi 1,587 ft
|
||||
Ride Sat, 1/18/2014 Los Altos Hills 2:32:23 29.03 mi 1,918 ft
|
||||
Ride Sat, 6/20/2015 06/20/2015 Palo Alto, CA 2:06:45 28.87 mi 1,650 ft
|
||||
Ride Sun, 11/3/2013 work 2:11:08 28.74 mi 841 ft
|
||||
Ride Wed, 8/16/2017 Lunch Ride 2:15:48 28.67 mi 732 ft
|
||||
Ride Sat, 1/9/2016 Morning Ride 2:07:35 28.54 mi 1,289 ft
|
||||
Ride Sat, 9/19/2015 Los Altos Hills 2:31:24 28.51 mi 1,611 ft
|
||||
Ride Wed, 8/12/2015 08/12/2015 Palo Alto, CA 2:02:14 28.48 mi 751 ft
|
||||
Ride Sun, 7/23/2017 Morning Ride 2:57:35 28.43 mi 2,562 ft
|
||||
Ride Mon, 8/24/2015 Morning Ride 2:10:31 28.35 mi 427 ft
|
||||
Ride Wed, 6/26/2013 work 2:01:29 28.33 mi 627 ft
|
||||
Ride Sun, 4/28/2019 Arastradsero 2:29:59 28.21 mi 1,355 ft
|
||||
Ride Sun, 7/19/2015 07/19/2015 Palo Alto, CA 2:00:27 28.17 mi 1,037 ft
|
||||
Ride Fri, 12/28/2018 Lunch Ride with Juliet 2:17:33 28.10 mi 331 ft
|
||||
Ride Tue, 5/17/2016 Morning Ride 2:06:55 28.09 mi 551 ft
|
||||
Ride Wed, 10/5/2016 Morning Ride 2:10:47 28.05 mi 276 ft
|
||||
Ride Wed, 6/22/2016 Morning Ride 2:00:40 27.99 mi 374 ft
|
||||
Ride Mon, 3/16/2015 Commute 2:06:51 27.97 mi 70 ft
|
||||
Ride Sun, 2/25/2018 Lunch Ride 2:10:54 27.88 mi 958 ft
|
||||
Ride Sat, 10/27/2018 Morning Ride 2:29:13 27.88 mi 1,581 ft
|
||||
Ride Mon, 6/20/2016 Morning Ride 1:52:13 27.77 mi 531 ft
|
||||
Ride Sun, 12/2/2018 Lunch Ride 2:34:14 27.72 mi 1,506 ft
|
||||
Ride Mon, 3/19/2018 Morning Ride 2:06:35 27.62 mi 860 ft
|
||||
Ride Mon, 12/17/2018 Morning Ride 2:16:12 27.60 mi 246 ft
|
||||
Ride Sat, 10/14/2017 Afternoon Ride 2:04:53 27.52 mi 387 ft
|
||||
Ride Sat, 10/18/2014 Bikepacking Monte Bello 2:54:20 27.44 mi 2,992 ft
|
||||
Ride Sun, 4/15/2018 Morning Ride 2:17:16 27.43 mi 1,667 ft
|
||||
Ride Mon, 4/10/2017 Morning Ride 2:03:19 27.40 mi 282 ft
|
||||
Ride Tue, 5/7/2019 Lunch Ride 2:07:55 27.37 mi 820 ft
|
||||
Ride Sun, 11/25/2018 Afternoon Ride 1:56:26 27.34 mi 203 ft
|
||||
Ride Wed, 4/29/2015 Palo Alto Cycling 2:03:14 27.33 mi 75 ft
|
||||
Ride Tue, 1/17/2017 Morning Ride 2:07:17 27.30 mi 535 ft
|
||||
Ride Tue, 9/30/2014 09/30/2014 Palo Alto, CA 2:07:33 27.28 mi 636 ft
|
||||
Ride Sat, 9/17/2016 TourDeCoop.org 2:04:27 27.24 mi 479 ft
|
||||
Ride Sat, 1/12/2019 Lunch Ride 1:58:47 27.24 mi 1,079 ft
|
||||
Ride Mon, 3/30/2015 Woodside Loop 1:47:46 27.22 mi 1,081 ft
|
||||
Ride Tue, 11/17/2015 Afternoon Ride 2:04:48 27.12 mi 830 ft
|
||||
Ride Sun, 8/13/2017 Woodside Loop (Battery ran down) 2:00:00 27.00 mi 1,000 ft
|
||||
Ride Sat, 1/31/2015 Alpine Rd 2:29:26 26.98 mi 2,362 ft
|
||||
Ride Sun, 2/28/2016 Woodside Loop 1:43:34 26.93 mi 843 ft
|
||||
Ride Sun, 4/1/2018 Lunch Ride 2:30:35 26.92 mi 1,831 ft
|
||||
Workout Wed, 5/3/2017 Palo Alto Road Cycling 2:06:05 26.86 mi 1,411 ft
|
||||
Ride Fri, 9/9/2016 Morning Ride 1:56:44 26.85 mi 1,086 ft
|
||||
Ride Sun, 9/23/2018 Morning Ride 1:57:48 26.83 mi 512 ft
|
||||
Ride Sat, 10/4/2014 Woodside Loop 1:52:09 26.71 mi 1,213 ft
|
||||
Ride Thu, 5/7/2015 Commute 1:55:36 26.58 mi 68 ft
|
||||
Ride Sun, 3/24/2019 Afternoon Ride 2:42:45 26.57 mi 2,267 ft
|
||||
Ride Thu, 8/31/2017 Morning Ride 2:00:02 26.54 mi 817 ft
|
||||
Ride Sun, 4/24/2016 Afternoon Ride 1:50:08 26.53 mi 935 ft
|
||||
Ride Sun, 12/17/2017 Lunch Ride 1:50:51 26.53 mi 217 ft
|
||||
Ride Tue, 7/4/2017 Lunch Ride 1:53:04 26.50 mi 459 ft
|
||||
Ride Tue, 5/8/2018 Afternoon Ride 2:34:11 26.50 mi 1,791 ft
|
||||
Ride Tue, 5/26/2015 05/26/2015 Palo Alto, CA 2:01:47 26.50 mi 591 ft
|
||||
Ride Thu, 8/4/2016 Afternoon Ride 1:55:47 26.49 mi 659 ft
|
||||
Ride Wed, 6/8/2016 Lunch Ride 1:48:19 26.48 mi 597 ft
|
||||
Ride Sun, 9/9/2018 Lunch Ride 2:00:49 26.46 mi 479 ft
|
||||
Ride Sat, 5/21/2016 Maker Faire 1:57:14 26.44 mi 207 ft
|
||||
Ride Wed, 4/1/2015 Commute 1:55:15 26.44 mi 71 ft
|
||||
Ride Wed, 6/29/2016 Morning Ride 1:49:33 26.39 mi 561 ft
|
||||
Ride Sat, 9/9/2017 Lunch Ride 1:59:01 26.38 mi 1,112 ft
|
||||
Ride Fri, 6/3/2016 Morning Ride 1:56:57 26.38 mi 502 ft
|
||||
Ride Fri, 3/31/2017 Morning Ride 2:01:41 26.36 mi 495 ft
|
||||
Ride Sat, 12/13/2014 Westridge (up the back way; after the rain) 2:01:01 26.35 mi 1,495 ft
|
||||
Ride Sun, 4/14/2019 Huddart - Paul Hopkins 2:08:57 26.34 mi 1,601 ft
|
||||
Ride Sat, 6/29/2013 Untitled 1:48:50 26.33 mi 1,091 ft
|
||||
Ride Sat, 4/25/2015 Woodside 1:54:30 26.28 mi 1,220 ft
|
||||
Ride Tue, 1/31/2017 Morning Ride 2:01:18 26.27 mi 384 ft
|
||||
Ride Tue, 6/28/2016 Morning Ride 1:55:31 26.27 mi 571 ft
|
||||
Ride Sat, 5/19/2018 Morning Ride 2:00:24 26.25 mi 958 ft
|
||||
Ride Thu, 6/30/2016 Morning Ride 1:52:16 26.24 mi 499 ft
|
||||
Ride Sun, 2/19/2017 Afternoon Ride 1:56:46 26.23 mi 1,138 ft
|
||||
Ride Thu, 8/18/2016 Morning Ride 1:58:06 26.23 mi 712 ft
|
||||
Ride Sun, 4/20/2014 Afternoon Ride 1:52:52 26.23 mi 102 ft
|
||||
Ride Sun, 5/10/2015 Arastadero 1:48:58 26.22 mi 1,148 ft
|
||||
Ride Thu, 5/19/2016 Morning Ride 1:52:56 26.14 mi 938 ft
|
||||
Ride Mon, 12/31/2018 Morning Ride 2:00:24 26.11 mi 545 ft
|
||||
Ride Fri, 12/15/2017 Afternoon Ride 1:51:36 26.07 mi 226 ft
|
||||
Ride Sun, 1/18/2015 Palo Alto Cycling 1:38:16 26.02 mi 1,257 ft
|
||||
Ride Mon, 10/3/2016 Morning Ride 1:54:45 26.01 mi 646 ft
|
||||
Ride Tue, 7/14/2015 07/14/2015 Mountain View, California 2:02:36 25.99 mi 502 ft
|
||||
Ride Fri, 12/18/2015 Lunch Ride 1:58:57 25.93 mi 909 ft
|
||||
Ride Tue, 6/13/2017 Morning Ride 1:55:30 25.89 mi 289 ft
|
||||
Ride Mon, 8/4/2014 To the Sea 2:17:11 25.87 mi 2,080 ft
|
||||
Ride Wed, 9/25/2013 work and with kris 2:07:40 25.81 mi 593 ft
|
||||
Ride Sun, 9/13/2015 Afternoon Ride 1:49:31 25.78 mi 1,152 ft
|
||||
Ride Sun, 3/3/2019 Afternoon Ride 2:00:06 25.73 mi 390 ft
|
||||
Ride Thu, 1/26/2017 Morning Ride 1:56:09 25.67 mi 259 ft
|
||||
Ride Sun, 5/6/2018 Morning Ride 1:55:05 25.67 mi 1,148 ft
|
||||
Ride Sat, 7/29/2017 Lunch Ride 1:53:50 25.65 mi 988 ft
|
||||
Ride Wed, 8/20/2014 08/20/2014 Palo Alto, CA 1:49:35 25.61 mi 497 ft
|
||||
Ride Sat, 4/19/2014 Lunch Ride 1:49:37 25.60 mi 1,111 ft
|
||||
Ride Mon, 9/3/2018 Afternoon Ride 1:59:08 25.54 mi 331 ft
|
||||
Ride Mon, 6/2/2014 Work 1:59:33 25.51 mi 627 ft
|
||||
Ride Fri, 12/11/2015 Afternoon Ride 2:12:40 25.44 mi 1,719 ft
|
||||
Ride Fri, 2/9/2018 Morning Ride 1:56:11 25.41 mi 318 ft
|
||||
Ride Sun, 8/26/2018 Afternoon Ride 1:49:16 25.37 mi 377 ft
|
||||
Ride Sun, 7/9/2017 Afternoon Ride 1:50:30 25.36 mi 682 ft
|
||||
Ride Mon, 6/26/2017 Morning Ride 1:52:26 25.34 mi 226 ft
|
||||
Ride Fri, 9/14/2018 Lunch Ride 1:50:05 25.34 mi 541 ft
|
||||
Ride Fri, 3/1/2019 Morning Ride 2:00:23 25.34 mi 121 ft
|
||||
Ride Sun, 1/13/2019 Lunch Ride 1:51:40 25.33 mi 715 ft
|
||||
Ride Wed, 4/12/2017 Morning Ride 1:59:22 25.33 mi 686 ft
|
||||
Ride Wed, 6/14/2017 Morning Ride 1:51:32 25.33 mi 308 ft
|
||||
Ride Sat, 11/28/2015 Lunch Ride 1:43:56 25.31 mi 1,122 ft
|
||||
Ride Mon, 11/27/2017 Morning Ride 1:55:39 25.31 mi 184 ft
|
||||
Ride Fri, 11/28/2014 11/28/2014 Palo Alto, CA 1:45:13 25.30 mi 1,112 ft
|
||||
Ride Fri, 4/13/2018 Morning Ride 1:53:39 25.28 mi 344 ft
|
||||
Ride Fri, 9/15/2017 Morning Ride 1:56:55 25.28 mi 384 ft
|
||||
Ride Tue, 12/31/2013 Woodside Loop - Last Ride of the Year 1:45:28 25.27 mi 1,207 ft
|
||||
Ride Sun, 10/14/2018 Lunch Ride 2:13:13 25.23 mi 1,421 ft
|
||||
Ride Sat, 11/22/2014 Atherton to Woodside 1:43:10 25.21 mi 1,095 ft
|
||||
Ride Sat, 6/4/2016 Morning Ride 1:57:23 25.19 mi 922 ft
|
||||
Ride Mon, 8/8/2016 Morning Ride 1:52:38 25.17 mi 423 ft
|
||||
Ride Fri, 11/6/2015 Morning Ride 1:51:45 25.15 mi 538 ft
|
||||
Ride Mon, 12/18/2017 Afternoon Ride 1:52:56 25.15 mi 741 ft
|
||||
Ride Fri, 6/24/2016 Afternoon Ride 1:35:25 25.11 mi 623 ft
|
||||
Ride Sat, 6/25/2016 Afternoon Ride 1:44:43 25.10 mi 568 ft
|
||||
Ride Sat, 1/25/2014 Woodside 1:33:38 25.08 mi 1,243 ft
|
||||
Ride Tue, 4/29/2014 Woodside 1:46:03 25.08 mi 886 ft
|
||||
Ride Thu, 4/13/2017 Morning Ride 2:06:13 25.06 mi 266 ft
|
||||
Ride Sun, 6/12/2016 Morning Ride 1:49:25 25.04 mi 387 ft
|
||||
Ride Thu, 6/8/2017 Morning Ride 1:56:19 25.02 mi 285 ft
|
||||
Ride Thu, 10/13/2016 Morning Ride 1:47:30 25.02 mi 253 ft
|
||||
Ride Sun, 1/19/2014 01/19/2014 Palo Alto, CA 1:36:55 25.01 mi 1,201 ft
|
||||
Ride Mon, 9/5/2016 Afternoon Ride 1:42:56 25.00 mi 902 ft
|
||||
Ride Sat, 11/8/2014 Atherton - Woodside 1:40:51 25.00 mi 972 ft
|
||||
Ride Sun, 12/10/2017 Exercise Bike 1:30:00 25.00 mi 0 ft
|
||||
#Ride Date Title mi ft
|
||||
# 2020 Eddington rides only
|
||||
Ride Sun, 1/26/2020 Los Gatos / Bay 5:18:15 67.98 mi 1,286 ft
|
||||
Ride Fri, 1/31/2020 Bay Trail / Guadalupe / Geocaching 5:21:39 69.39 mi 1,125 ft
|
||||
# 2019 Eddington rides only
|
||||
Ride Sun, 6/2/2019 The Sequoia 6:40:43 77.51 mi 6,467 ft
|
||||
Ride Sat, 6/8/2019 Morning Ride 2:53:08 34.42 mi 1,824 ft
|
||||
Ride Sat, 6/15/2019 Morning Ride 2:29:38 29.98 mi 1,785 ft
|
||||
Ride Sat, 6/15/2019 Morning Ride 2:29:38 29.98 mi 1,785 ft
|
||||
Ride Fri, 7/5/2019 Crystal Springs 4:30:55 64.05 mi 1,965 ft
|
||||
Ride Sun, 8/18/2019 Afternoon Ride 2:06:52 27.14 mi 331 ft
|
||||
Ride Sat, 8/24/2019 Tour de Fox 4:43:39 68.66 mi 2,097 ft
|
||||
Ride Sat, 8/31/2019 San Gregorio via OLH 5:13:21 65.31 mi 4,026 ft
|
||||
Ride Fri, 9/6/2019 San Mateo / Bay Trail 4:59:29 68.01 mi 1,158 ft
|
||||
Ride Sat, 11/16/2019 Canada / Crystal Dam / Sawyer Camp / Bay Trail 5:18:46 66.08 mi 1,972 ft
|
||||
# 2012 to 2018 rides over 25 miles
|
||||
Ride Fri, 1/9/2012 Otago Rail Trail 8:00:00 101.00 mi 2,200 ft
|
||||
Ride Thu, 6/14/2012 Coyote Creek Century with Juliet 8:08:15 100.07 mi 1,513 ft
|
||||
Ride Sat, 5/13/2017 Morgan Hill iCare Classic 7:27:21 100.05 mi 4,596 ft
|
||||
Ride Sat, 5/12/2018 ICare Classic, Morgan Hill 6:47:46 91.29 mi 4,160 ft
|
||||
Ride Sat, 5/6/2017 Wine Country Century 7:15:22 89.49 mi 5,246 ft
|
||||
Ride Fri, 8/10/2018 Bike Ride Northwest Day 6 6:14:18 84.70 mi 4,380 ft
|
||||
Ride Sat, 10/1/2016 Half Moon Bay overnight campout 7:30:38 80.07 mi 6,039 ft
|
||||
Ride Tue, 8/7/2018 Bike Ride Northwest Day 3 6:10:39 78.96 mi 5,092 ft
|
||||
Ride Sun, 6/15/2014 Sierra to the Sea Day 1 5:34:06 78.53 mi 4,777 ft
|
||||
Ride Sat, 4/23/2016 Wildflower Century 5:36:10 77.22 mi 4,193 ft
|
||||
Ride Mon, 6/16/2014 Sierra to the Sea Day 2 5:35:06 74.68 mi 2,451 ft
|
||||
Ride Fri, 8/12/2016 Half Moon Bay / Harvey's 73/7300 Birthday Ride 6:44:14 74.35 mi 6,610 ft
|
||||
Ride Wed, 10/14/2015 Half Moon Bay 6:07:55 72.97 mi 7,644 ft
|
||||
Ride Fri, 1/27/2017 Morning Ride 5:16:50 70.07 mi 2,539 ft
|
||||
Ride Tue, 6/17/2014 Sierra to the Sea Day 3 4:48:41 68.64 mi 825 ft
|
||||
Ride Thu, 8/9/2018 Bike Ride Northwest Day 5 4:58:07 68.41 mi 3,862 ft
|
||||
Ride Sat, 4/15/2017 Pescadero 6:13:12 68.34 mi 6,130 ft
|
||||
Ride Sun, 6/4/2017 Sequoia Challenge 6:17:25 66.52 mi 7,520 ft
|
||||
Ride Wed, 8/5/2015 08/05/2015 Palo Alto, CA 4:56:15 66.33 mi 2,054 ft
|
||||
Ride Fri, 8/28/2015 Pescadaro via OLH 5:18:51 66.01 mi 6,137 ft
|
||||
Ride Fri, 11/20/2015 Los Gatos 5:29:40 65.73 mi 5,380 ft
|
||||
Ride Sun, 6/3/2018 The Sequoia 5:58:15 64.92 mi 6,677 ft
|
||||
Ride Thu, 6/19/2014 Sierra to the Sea Day 5 4:32:55 63.69 mi 2,584 ft
|
||||
Ride Mon, 8/3/2015 08/03/2015 Palo Alto, CA 4:45:09 63.61 mi 1,877 ft
|
||||
Ride Fri, 6/10/2016 Morning Ride 4:21:38 62.84 mi 3,196 ft
|
||||
Ride Sun, 6/2/2013 Woodside Loop and Baylands 4:27:42 62.14 mi 2,169 ft
|
||||
Ride Wed, 6/12/2013 ride 4:39:46 61.39 mi 2,207 ft
|
||||
Ride Sat, 4/18/2015 Tunitas + Lobitos Creeks 5:14:38 61.27 mi 6,611 ft
|
||||
Ride Sat, 11/11/2017 Los Gatos / Bay Trail 4:26:24 60.74 mi 1,316 ft
|
||||
Ride Sun, 4/17/2016 La Honda / Skyline 5:10:41 60.35 mi 4,551 ft
|
||||
Ride Mon, 7/3/2017 Los Gatos / Bay Trail 4:25:41 60.28 mi 1,329 ft
|
||||
Ride Sat, 4/16/2016 OLH - San Gregorio - Tunitas 4:57:40 60.12 mi 4,744 ft
|
||||
Ride Sun, 1/29/2017 Los Gatos / Guadalupe / San Tomas / Bay Trails 4:33:38 60.11 mi 1,447 ft
|
||||
Ride Mon, 11/26/2018 Lunch Ride Los Gatos 4:21:35 60.03 mi 1,070 ft
|
||||
Ride Sat, 10/31/2015 Saratoga Ramble 5:01:57 59.10 mi 3,528 ft
|
||||
Ride Sat, 8/18/2018 Tour de Menlo 4:03:48 58.81 mi 2,467 ft
|
||||
Ride Sat, 4/26/2014 OLH - Tunitas Creek 5:15:26 58.69 mi 6,742 ft
|
||||
Ride Sat, 6/15/2013 Palo Alto to Santa Cruz 4:27:54 58.42 mi 4,431 ft
|
||||
Ride Sat, 10/11/2014 OLH / La Honda / Tunitas 5:05:23 58.29 mi 6,044 ft
|
||||
Ride Sat, 7/9/2016 Lunch Ride 3:50:23 58.23 mi 4,042 ft
|
||||
Ride Wed, 6/18/2014 Sierra to the Sea Day 4 4:57:53 57.64 mi 5,561 ft
|
||||
Ride Tue, 7/7/2015 07/07/2015 Palo Alto, CA 4:13:20 57.60 mi 1,280 ft
|
||||
Ride Mon, 8/6/2018 Bike Ride Northwest Day 2 4:35:48 57.58 mi 4,514 ft
|
||||
Ride Fri, 6/20/2014 Sierra to the Sea Day 6 4:33:39 56.91 mi 4,453 ft
|
||||
Ride Sat, 6/10/2017 Los Gatos / Creek Trails 3:50:24 56.28 mi 1,365 ft
|
||||
Ride Sat, 3/4/2017 Lunch Ride 3:58:25 56.26 mi 1,378 ft
|
||||
Ride Sun, 8/5/2018 Bike Ride Northwest Day 1 3:34:42 55.77 mi 1,824 ft
|
||||
Ride Sat, 8/13/2016 Petaluma - Point Reyes 4:30:12 54.75 mi 5,286 ft
|
||||
Ride Sun, 6/7/2015 Tour de Cure 3:59:47 54.65 mi 2,748 ft
|
||||
Ride Wed, 12/16/2015 Los Gatos 4:00:41 53.86 mi 2,595 ft
|
||||
Ride Fri, 6/2/2017 Morning Ride 3:57:45 53.49 mi 1,375 ft
|
||||
Ride Sun, 6/8/2014 Tour de Cure 75K 4:03:05 53.10 mi 2,596 ft
|
||||
Ride Sun, 11/6/2016 Los Gatos 3:38:28 52.49 mi 1,263 ft
|
||||
Ride Fri, 2/10/2017 Morning Ride 3:52:39 52.02 mi 1,739 ft
|
||||
Ride Sat, 2/23/2019 Crystal Springs Dam 4:02:11 51.93 mi 1,946 ft
|
||||
Ride Fri, 8/11/2017 Saratoga with Peter H 4:30:31 51.74 mi 2,871 ft
|
||||
Ride Sat, 2/4/2017 Canada Rd 3:46:34 51.66 mi 1,762 ft
|
||||
Ride Sun, 6/9/2013 Silicon Valley Tour de Cure 75K 3:58:37 51.63 mi 2,929 ft
|
||||
Ride Sun, 9/3/2017 Morning Ride 4:22:45 51.31 mi 2,526 ft
|
||||
Ride Mon, 7/27/2015 Palo Alto Cycling 3:48:23 50.93 mi 1,306 ft
|
||||
Ride Wed, 7/29/2015 07/29/2015 Palo Alto, CA 3:47:53 50.92 mi 1,873 ft
|
||||
Ride Sun, 6/26/2016 Afternoon Ride 3:16:33 50.78 mi 1,181 ft
|
||||
Ride Mon, 8/10/2015 08/10/2015 Palo Alto, CA 3:41:54 50.73 mi 1,325 ft
|
||||
Ride Sat, 10/15/2016 Los Gatos 3:28:16 50.64 mi 1,368 ft
|
||||
Ride Tue, 12/19/2017 Los Gatos 3:46:37 50.49 mi 1,929 ft
|
||||
Ride Fri, 6/28/2013 Kaffeehaus San Mateo 3:38:11 50.38 mi 1,028 ft
|
||||
Ride Thu, 10/5/2017 Big Sur 4:33:33 50.38 mi 4,528 ft
|
||||
Ride Sun, 11/20/2016 Lunch Ride 3:32:01 50.13 mi 1,847 ft
|
||||
Ride Sat, 1/13/2018 Canada to Coyote Point 3:59:24 50.09 mi 1,499 ft
|
||||
Ride Fri, 1/13/2017 Los Gatos Creek 3:52:28 50.01 mi 1,598 ft
|
||||
Ride Sun, 6/11/2017 Tour de Cure with Juliet 4:11:01 49.49 mi 2,713 ft
|
||||
Ride Sun, 9/11/2016 Santa Cruz via Mountain Charlie Rd 4:03:04 48.78 mi 3,300 ft
|
||||
Ride Sat, 7/23/2016 Morning Ride 3:30:50 48.45 mi 1,063 ft
|
||||
Ride Fri, 3/25/2016 Morning Ride 3:39:53 47.93 mi 2,438 ft
|
||||
Ride Sat, 8/20/2016 Tour de Menlo with Juliet 3:30:24 47.52 mi 2,133 ft
|
||||
Ride Sat, 8/15/2015 08/15/2015 Palo Alto, CA 3:35:03 47.12 mi 1,158 ft
|
||||
Ride Wed, 11/11/2015 Morning Ride 3:45:01 46.54 mi 2,559 ft
|
||||
Ride Fri, 7/21/2017 Morning Ride 3:35:00 46.19 mi 748 ft
|
||||
Ride Sat, 9/2/2017 Canada / Sheep Camp Trail 3:45:45 45.72 mi 2,034 ft
|
||||
Ride Mon, 1/19/2015 Canada Rd, etc. 2:57:14 45.64 mi 1,836 ft
|
||||
Ride Sun, 11/27/2016 Morning Ride 3:04:11 45.60 mi 1,378 ft
|
||||
Ride Sun, 7/2/2017 Afternoon Ride 3:10:38 45.30 mi 581 ft
|
||||
Ride Sat, 10/3/2015 Los Gatos 2:59:26 45.21 mi 1,148 ft
|
||||
Ride Sat, 7/2/2016 Morning Ride 3:23:20 45.21 mi 1,991 ft
|
||||
Ride Mon, 1/16/2017 Morning Ride 3:34:28 45.15 mi 1,434 ft
|
||||
Ride Sun, 1/15/2017 Lunch Ride 3:18:47 45.12 mi 1,873 ft
|
||||
Ride Sun, 4/9/2017 Lunch Ride 3:10:28 44.76 mi 636 ft
|
||||
Ride Sun, 10/15/2017 Los Gatos 2:51:53 44.71 mi 1,437 ft
|
||||
Ride Tue, 10/8/2013 work 3:22:55 44.58 mi 961 ft
|
||||
Ride Thu, 7/16/2015 07/16/2015 Mountain View, California 3:29:45 44.34 mi 1,339 ft
|
||||
Ride Sat, 10/28/2017 Mindego Ridge Winery 4:17:45 44.28 mi 4,331 ft
|
||||
Ride Sun, 9/2/2018 Saratoga 3:31:02 44.04 mi 1,900 ft
|
||||
Ride Fri, 9/23/2016 Los Gatos 2:53:22 43.93 mi 1,339 ft
|
||||
Ride Sat, 7/25/2015 07/25/2015 Palo Alto, California 4:02:27 43.62 mi 4,819 ft
|
||||
Ride Sat, 7/8/2017 Dumbarton / Niles 3:14:55 43.54 mi 627 ft
|
||||
Ride Sat, 6/9/2018 Kings Mountain 3:41:47 43.47 mi 3,543 ft
|
||||
Ride Fri, 3/10/2017 Morning Ride 4:13:51 43.22 mi 4,554 ft
|
||||
Ride Sat, 3/26/2016 Morning Ride 3:36:57 43.18 mi 3,173 ft
|
||||
Ride Sun, 10/18/2015 Afternoon Ride 3:00:02 43.04 mi 2,323 ft
|
||||
Ride Sat, 9/20/2014 Kings Mountain 3:26:36 43.00 mi 3,299 ft
|
||||
Ride Sat, 8/11/2018 Bike Ride Northwest Day 7 3:25:20 42.39 mi 2,241 ft
|
||||
Ride Sat, 3/18/2017 Morning Ride 3:37:01 42.29 mi 2,231 ft
|
||||
Ride Sat, 7/11/2015 07/11/2015 Walnut Creek, California 3:09:38 42.29 mi 3,284 ft
|
||||
Ride Thu, 1/22/2015 OLH, etc. 3:25:09 42.15 mi 2,957 ft
|
||||
Ride Sun, 11/9/2014 11/09/2014 Palo Alto, CA 3:24:57 42.10 mi 3,096 ft
|
||||
Ride Sun, 5/15/2016 Afternoon Ride 2:55:51 41.98 mi 1,693 ft
|
||||
Ride Sat, 3/25/2017 Morning Ride 3:37:45 41.94 mi 2,874 ft
|
||||
Ride Sun, 6/14/2015 06/14/2015 Palo Alto, CA 2:46:38 41.67 mi 1,086 ft
|
||||
Ride Sat, 7/28/2018 Coyote Hills / Dumbarton 3:25:39 41.66 mi 1,463 ft
|
||||
Ride Sun, 8/7/2016 Afternoon Ride 2:42:58 41.19 mi 1,526 ft
|
||||
Ride Sat, 2/11/2017 Morning Ride 3:19:34 41.16 mi 2,172 ft
|
||||
Ride Sun, 12/3/2017 Bay Trail 2:53:58 40.86 mi 568 ft
|
||||
Ride Mon, 5/29/2017 Morning Ride 3:52:04 40.83 mi 3,678 ft
|
||||
Ride Sat, 2/18/2017 Lunch Ride 3:01:13 40.82 mi 630 ft
|
||||
Ride Wed, 11/9/2016 Lunch Ride 2:52:06 40.82 mi 1,667 ft
|
||||
Ride Sat, 5/20/2017 Lunch Ride 3:07:43 40.82 mi 709 ft
|
||||
Ride Sat, 3/24/2018 Morning Ride 2:56:29 40.69 mi 561 ft
|
||||
Ride Sun, 11/19/2017 Belmont / Bay Trail 2:53:36 40.60 mi 453 ft
|
||||
Ride Sun, 3/26/2017 Lunch Ride 2:54:38 40.57 mi 518 ft
|
||||
Ride Sat, 5/25/2019 Crestview 3:17:16 40.56 mi 2,890 ft
|
||||
Ride Sat, 9/16/2017 Tour de Coop 3:24:53 40.55 mi 1,125 ft
|
||||
Ride Thu, 9/3/2015 Morning Ride 3:02:58 40.40 mi 1,181 ft
|
||||
Ride Sun, 2/5/2017 Morning Ride 2:53:48 40.40 mi 1,608 ft
|
||||
Ride Sun, 10/23/2016 Lunch Ride 2:41:33 40.33 mi 1,699 ft
|
||||
Ride Sat, 12/8/2018 Morning Ride 3:27:10 40.33 mi 2,300 ft
|
||||
Ride Sun, 2/12/2017 Lunch Ride 3:39:38 40.27 mi 2,717 ft
|
||||
Ride Sat, 12/2/2017 Dumbarton / Niles 2:57:48 40.26 mi 610 ft
|
||||
Ride Sun, 11/12/2017 Morning Ride 2:54:57 40.24 mi 656 ft
|
||||
Ride Sat, 11/15/2014 Kings Mountain again 3:25:14 40.18 mi 2,952 ft
|
||||
Ride Sun, 10/9/2016 Lunch Ride 2:41:06 40.14 mi 1,106 ft
|
||||
Ride Mon, 11/6/2017 Morning Ride 2:48:12 40.13 mi 509 ft
|
||||
Ride Sat, 12/17/2016 Morning Ride 3:11:31 40.10 mi 2,133 ft
|
||||
Ride Fri, 1/12/2018 Morning Ride 2:58:30 40.09 mi 427 ft
|
||||
Ride Sun, 12/18/2016 Morning Ride 3:04:01 40.03 mi 1,886 ft
|
||||
Ride Sat, 6/11/2016 Ring of Fire (Partial; Garmin troubles) 3:00:00 40.00 mi 0 ft
|
||||
Ride Sun, 1/25/2015 Canada Rd 2:45:51 39.99 mi 1,772 ft
|
||||
Ride Sat, 6/21/2014 Sierra to the Sea Day 7 3:36:53 39.77 mi 3,325 ft
|
||||
Ride Sat, 9/23/2017 Canada / Crestview 3:25:13 39.77 mi 2,444 ft
|
||||
Ride Fri, 12/27/2013 OLH to Page Mill 3:24:14 39.73 mi 3,365 ft
|
||||
Ride Sat, 4/30/2016 Ridge to Bridge MTB 5:40:57 39.67 mi 5,377 ft
|
||||
Ride Fri, 10/30/2015 OLH - West Alpine 3:28:46 39.51 mi 4,505 ft
|
||||
Ride Sat, 9/5/2015 Ring of Fire 3:31:55 39.18 mi 3,553 ft
|
||||
Ride Wed, 8/28/2013 Train Track Crash 2:50:55 39.16 mi 928 ft
|
||||
Ride Sun, 2/22/2015 Canada Rd Plus 2:57:25 39.14 mi 1,834 ft
|
||||
Ride Sat, 4/8/2017 Lunch Ride 3:28:52 39.09 mi 3,117 ft
|
||||
Ride Sat, 11/14/2015 Morning Ride 3:09:31 39.00 mi 2,254 ft
|
||||
Ride Sun, 3/1/2015 Canada Rd 2:38:11 38.81 mi 1,856 ft
|
||||
Ride Wed, 11/22/2017 Morning Ride 2:55:22 38.77 mi 892 ft
|
||||
Ride Sat, 10/29/2016 Morning Ride 3:14:22 38.72 mi 2,201 ft
|
||||
Ride Sat, 4/6/2019 Coyote Hills / Dumbarton 2:50:52 38.71 mi 650 ft
|
||||
Ride Wed, 7/22/2015 07/22/2015 Palo Alto, CA 2:55:45 38.68 mi 627 ft
|
||||
Ride Mon, 6/22/2015 06/22/2015 Palo Alto, CA 2:51:10 38.55 mi 725 ft
|
||||
Ride Fri, 12/16/2016 Morning Ride 2:54:02 38.46 mi 1,053 ft
|
||||
Ride Sun, 10/7/2018 Westridge, Canada with Ted 3:09:25 38.29 mi 1,667 ft
|
||||
Ride Sat, 2/21/2015 Kings Mountain 3:13:05 38.21 mi 3,015 ft
|
||||
Ride Sat, 9/12/2015 Morning Ride 3:06:03 38.20 mi 3,342 ft
|
||||
Ride Sun, 2/15/2015 Canada Rd 2:35:29 38.06 mi 1,470 ft
|
||||
Ride Sun, 11/16/2014 Canada Rd 2:30:41 37.88 mi 1,548 ft
|
||||
Ride Sun, 10/5/2014 Canada Road Sunday 2:39:39 37.84 mi 1,644 ft
|
||||
Ride Mon, 11/23/2015 Home from Pigeon Point 3:29:29 37.70 mi 3,133 ft
|
||||
Ride Sat, 1/6/2018 Morning Ride 2:53:45 37.66 mi 1,503 ft
|
||||
Ride Sun, 1/7/2018 Dumbarton / Niles 2:39:17 37.64 mi 472 ft
|
||||
Ride Sun, 12/1/2013 Mt Hamilton 3:47:01 37.56 mi 4,921 ft
|
||||
Ride Sun, 3/18/2018 Lunch Ride 2:47:48 37.52 mi 827 ft
|
||||
Ride Sat, 9/22/2018 Morning Ride 2:52:01 37.45 mi 1,798 ft
|
||||
Ride Sun, 10/22/2017 Afternoon Ride 2:33:27 37.41 mi 1,024 ft
|
||||
Ride Sun, 5/4/2014 MTV-Woodside 2:41:21 37.41 mi 1,495 ft
|
||||
Ride Sat, 1/23/2016 Morning Ride 3:39:10 37.35 mi 2,949 ft
|
||||
Ride Sun, 9/16/2018 Dumbarton / Niles 2:38:41 37.26 mi 722 ft
|
||||
Ride Sun, 6/10/2018 Canada / Emerald Hills 3:21:31 37.26 mi 2,621 ft
|
||||
Ride Mon, 5/4/2015 Commute 2:43:06 37.25 mi 189 ft
|
||||
Ride Sat, 7/7/2018 Runymede Trail 3:01:51 37.07 mi 1,834 ft
|
||||
Ride Sun, 12/11/2016 Afternoon Ride 2:34:13 37.05 mi 1,463 ft
|
||||
Ride Sun, 11/29/2015 Mt Hamilton 3:40:48 37.00 mi 4,902 ft
|
||||
Ride Sat, 1/30/2016 Morning Ride 2:31:37 36.88 mi 1,345 ft
|
||||
Ride Mon, 2/18/2019 Niles 2:39:39 36.83 mi 571 ft
|
||||
Ride Sun, 1/21/2018 Alviso 2:35:58 36.82 mi 404 ft
|
||||
Ride Sat, 8/26/2017 Lunch Ride 2:41:49 36.71 mi 1,115 ft
|
||||
Ride Sat, 9/13/2014 Emerald Hills 2:55:30 36.71 mi 2,143 ft
|
||||
Ride Sun, 5/22/2016 Lunch Ride 2:12:20 36.68 mi 1,332 ft
|
||||
Ride Sat, 11/25/2017 Mt. Hamilton 3:41:22 36.65 mi 4,806 ft
|
||||
Ride Fri, 10/13/2017 Voyage 2:46:15 36.63 mi 505 ft
|
||||
Ride Tue, 5/12/2015 05/12/2015 Palo Alto, CA 2:47:15 36.54 mi 499 ft
|
||||
Ride Mon, 4/20/2015 Commute 2:57:44 36.31 mi 119 ft
|
||||
Ride Fri, 5/13/2016 Morning Ride 2:41:03 36.21 mi 456 ft
|
||||
Ride Sat, 12/3/2016 Morning Ride 3:08:37 36.20 mi 2,881 ft
|
||||
Ride Sun, 12/7/2014 12/07/2014 Palo Alto, CA 2:43:35 36.16 mi 1,569 ft
|
||||
Ride Sun, 9/30/2018 Canada 2:48:41 36.12 mi 1,480 ft
|
||||
Ride Fri, 3/30/2018 Morning Ride 2:38:07 36.11 mi 285 ft
|
||||
Ride Sun, 11/5/2017 Lunch Ride 3:24:01 35.91 mi 2,231 ft
|
||||
Ride Sun, 2/8/2015 Commute 2:28:46 35.85 mi 1,142 ft
|
||||
Ride Tue, 6/26/2018 Return from Chabot Lake with Juliet MTB 3:21:30 35.81 mi 817 ft
|
||||
Ride Sat, 3/30/2019 Morning Ride 3:23:55 35.78 mi 2,861 ft
|
||||
Ride Sun, 11/22/2015 Pigeon Point 3:14:41 35.73 mi 3,230 ft
|
||||
Ride Mon, 9/19/2016 Sunnyvale 2:37:04 35.68 mi 745 ft
|
||||
Ride Thu, 4/9/2015 Palo Alto Cycling 2:35:47 35.51 mi 117 ft
|
||||
Ride Sun, 8/21/2016 Canada 2:20:21 35.42 mi 1,348 ft
|
||||
Ride Tue, 10/28/2014 Woodside 2:36:36 35.39 mi 1,560 ft
|
||||
Ride Sun, 1/27/2019 Crystal Springs Dam Bridge 2:28:21 35.38 mi 1,480 ft
|
||||
Ride Thu, 4/26/2018 San Mateo 2:38:26 35.31 mi 666 ft
|
||||
Ride Thu, 12/22/2016 Half Moon Bay 2:53:00 35.26 mi 2,671 ft
|
||||
Ride Tue, 6/7/2016 Los Altos 2:41:13 35.18 mi 1,070 ft
|
||||
Ride Sat, 3/15/2014 Canada - Steve 2:51:50 35.01 mi 1,607 ft
|
||||
Ride Sun, 12/6/2015 Canada Rd 2:15:05 34.67 mi 1,237 ft
|
||||
Ride Wed, 9/13/2017 Healdburg / Jimtown 2:08:03 34.45 mi 912 ft
|
||||
Ride Thu, 10/2/2014 10/02/2014 Mountain View, California 3:08:06 34.28 mi 2,864 ft
|
||||
Ride Sat, 2/1/2014 OLH to Page Mill 3:07:43 34.26 mi 3,099 ft
|
||||
Ride Sat, 10/21/2017 Pescadero 5:20:08 67.05 mi 4,938 ft
|
||||
Ride Sat, 7/30/2016 Morning Ride 2:50:15 34.19 mi 2,799 ft
|
||||
Ride Mon, 7/10/2017 Morning Ride 2:38:59 34.18 mi 984 ft
|
||||
Ride Thu, 7/31/2014 Work commute 2:36:53 34.15 mi 564 ft
|
||||
Ride Sat, 12/15/2018 Lunch Ride 2:50:19 34.10 mi 1,683 ft
|
||||
Ride Sat, 1/20/2018 Afternoon Ride 2:28:18 34.02 mi 525 ft
|
||||
Ride Sun, 12/4/2016 Morning Ride 2:29:29 34.00 mi 1,161 ft
|
||||
Ride Sat, 10/8/2016 Lunch Ride 2:25:17 33.95 mi 1,375 ft
|
||||
Ride Thu, 10/16/2014 Commute 2:40:01 33.76 mi 615 ft
|
||||
Ride Fri, 11/11/2016 OLH 2:44:59 33.69 mi 2,175 ft
|
||||
Ride Sat, 9/3/2016 Morning Ride 3:07:28 33.65 mi 2,792 ft
|
||||
Ride Sun, 4/19/2015 Canada Rd 2:23:17 33.56 mi 1,466 ft
|
||||
Ride Sat, 8/12/2017 Afternoon Ride 2:41:24 33.54 mi 633 ft
|
||||
Ride Sat, 2/14/2015 Rancho San Antonio 2:45:23 33.53 mi 2,075 ft
|
||||
Ride Sat, 2/27/2016 Morning Ride 2:47:15 33.51 mi 2,005 ft
|
||||
Ride Sun, 9/17/2017 Canada and off road 2:36:04 33.50 mi 1,772 ft
|
||||
Ride Sat, 2/24/2018 Los Altos Hills on and off road 3:10:47 33.49 mi 2,343 ft
|
||||
Ride Sun, 1/26/2014 Canada Rd 2:05:59 33.12 mi 1,446 ft
|
||||
Ride Sat, 4/27/2019 Morning Ride 2:31:48 33.04 mi 1,722 ft
|
||||
Ride Sun, 5/12/2019 Peninsula Bike Trail to Bay Trail 2:29:05 33.00 mi 846 ft
|
||||
Ride Sun, 8/20/2017 Lunch Ride 2:23:02 32.96 mi 1,407 ft
|
||||
Ride Sun, 3/6/2016 Lunch Ride 2:57:54 32.86 mi 2,566 ft
|
||||
Ride Sun, 3/5/2017 Lunch Ride 2:57:09 32.82 mi 1,713 ft
|
||||
Ride Sat, 12/16/2017 Morning Ride 2:48:58 32.72 mi 2,320 ft
|
||||
Ride Tue, 8/23/2016 Morning Ride 2:32:15 32.61 mi 354 ft
|
||||
Ride Fri, 12/9/2016 Morning Ride 3:04:52 32.55 mi 2,365 ft
|
||||
Ride Sun, 11/1/2015 Lunch Ride 2:20:35 32.49 mi 1,553 ft
|
||||
Ride Sat, 4/14/2018 Lunch Ride 2:21:49 32.43 mi 666 ft
|
||||
Ride Fri, 1/26/2018 Morning Ride 2:23:38 32.41 mi 295 ft
|
||||
Ride Sun, 8/16/2015 08/16/2015 Palo Alto, CA 2:29:53 32.40 mi 1,650 ft
|
||||
Ride Sat, 5/9/2015 OLH 2:30:17 32.33 mi 2,788 ft
|
||||
Ride Wed, 1/31/2018 Morning Ride 3:04:15 32.28 mi 2,526 ft
|
||||
Ride Sat, 4/4/2015 Rancho San Antonio 2:55:37 32.26 mi 2,136 ft
|
||||
Ride Sat, 3/16/2019 Morning Ride 2:58:43 32.26 mi 2,316 ft
|
||||
Ride Sun, 12/27/2015 Canada Rd with Juliet 2:24:56 32.22 mi 1,491 ft
|
||||
Ride Mon, 5/18/2015 05/18/2015 Palo Alto, CA 2:24:45 32.22 mi 807 ft
|
||||
Ride Sun, 4/8/2018 Lunch Ride 2:21:54 32.18 mi 1,296 ft
|
||||
Ride Sun, 5/3/2015 Canada Rd 2:12:24 32.02 mi 1,384 ft
|
||||
Ride Wed, 6/17/2015 06/17/2015 Mountain View, California 2:13:42 32.01 mi 650 ft
|
||||
Ride Sat, 11/7/2015 Crystal Springs Part 1 2:15:00 32.00 mi 0 ft
|
||||
Ride Sun, 8/23/2015 Afternoon Ride 2:24:29 31.90 mi 2,444 ft
|
||||
Ride Sun, 4/12/2015 Palo Alto Cycling 2:01:49 31.76 mi 1,210 ft
|
||||
Ride Sun, 11/13/2016 Afternoon Ride 2:23:46 31.71 mi 1,273 ft
|
||||
Ride Fri, 10/7/2016 Morning Ride 2:26:46 31.65 mi 2,382 ft
|
||||
Ride Thu, 8/15/2013 08/15/2013 Palo Alto, CA 2:19:27 31.64 mi 555 ft
|
||||
Ride Wed, 10/30/2013 work 2:22:00 31.55 mi 995 ft
|
||||
Ride Sun, 9/18/2016 Morning Ride 2:20:46 31.48 mi 1,506 ft
|
||||
Ride Tue, 6/18/2013 work etc (headwinds) 2:03:34 31.48 mi 809 ft
|
||||
Ride Sun, 3/11/2018 Lunch Ride 2:23:14 31.42 mi 686 ft
|
||||
Ride Sat, 3/12/2016 Lunch Ride 2:10:01 31.39 mi 1,198 ft
|
||||
Ride Sun, 1/22/2017 Dumbarton 2:23:36 31.27 mi 591 ft
|
||||
Ride Sat, 9/29/2018 Kings 2:36:47 31.23 mi 1,949 ft
|
||||
Ride Sat, 3/23/2019 Morning Ride 2:33:27 31.19 mi 1,529 ft
|
||||
Ride Sat, 4/13/2019 Alviso 2:10:04 31.18 mi 397 ft
|
||||
Ride Mon, 11/10/2014 Commute 2:22:39 31.18 mi 800 ft
|
||||
Ride Mon, 5/25/2015 05/25/2015 Palo Alto, CA 2:19:26 31.14 mi 1,591 ft
|
||||
Ride Mon, 2/16/2015 Portola Valley Loop 2:09:15 31.11 mi 1,283 ft
|
||||
Ride Wed, 9/21/2016 Morning Ride 2:16:04 31.09 mi 551 ft
|
||||
Ride Sat, 3/11/2017 Afternoon Ride 2:19:05 31.04 mi 1,368 ft
|
||||
Ride Sun, 7/29/2018 Lunch Ride 2:26:56 30.92 mi 1,578 ft
|
||||
Ride Fri, 11/10/2017 Lunch Ride 2:20:46 30.92 mi 312 ft
|
||||
Ride Mon, 6/12/2017 Morning Ride 2:20:16 30.90 mi 554 ft
|
||||
Ride Thu, 12/29/2016 Morning Ride 2:13:46 30.86 mi 1,083 ft
|
||||
Ride Sat, 4/22/2017 Lunch Ride 2:21:31 30.80 mi 1,237 ft
|
||||
Ride Mon, 9/7/2015 Healdsburg 2:09:43 30.76 mi 1,037 ft
|
||||
Ride Sat, 1/14/2017 Afternoon Ride 2:12:37 30.71 mi 466 ft
|
||||
Ride Mon, 9/4/2017 Kings Mountain 2:40:57 30.69 mi 2,431 ft
|
||||
Ride Sun, 8/9/2015 08/09/2015 Palo Alto, CA 2:15:39 30.66 mi 1,348 ft
|
||||
Ride Sat, 12/12/2015 Lunch Ride 2:16:20 30.56 mi 1,434 ft
|
||||
Ride Thu, 12/28/2017 Morning Ride 2:17:38 30.54 mi 430 ft
|
||||
Ride Mon, 1/30/2017 Morning Ride 2:19:47 30.49 mi 696 ft
|
||||
Ride Sun, 8/27/2017 Lunch Ride 2:16:36 30.47 mi 774 ft
|
||||
Ride Fri, 10/12/2018 Lunch Ride 2:16:25 30.44 mi 348 ft
|
||||
Ride Mon, 6/25/2018 Chabot Lake Overnight Bike packing with Juliet MTB 3:02:49 30.41 mi 1,250 ft
|
||||
Ride Sat, 2/28/2015 Palo Alto Cycling 2:40:24 30.37 mi 2,535 ft
|
||||
Ride Sat, 6/24/2017 Afternoon Ride 2:06:05 30.31 mi 338 ft
|
||||
Ride Tue, 6/2/2015 06/02/2015 Mountain View, California 2:09:58 30.30 mi 784 ft
|
||||
Ride Mon, 5/27/2019 Canada / Sheep camp / Water dog lake 2:37:11 30.27 mi 1,444 ft
|
||||
Ride Sat, 1/2/2016 Los Altos Hills 2:29:48 30.27 mi 1,952 ft
|
||||
Ride Fri, 6/23/2017 Morning Ride 2:17:16 30.27 mi 515 ft
|
||||
Ride Sun, 4/2/2017 Morning Ride 2:20:47 30.23 mi 407 ft
|
||||
Ride Sat, 8/25/2018 Morning Ride 2:22:42 30.22 mi 2,257 ft
|
||||
Ride Sun, 9/14/2014 09/14/2014 Palo Alto, CA 2:06:48 30.18 mi 1,204 ft
|
||||
Ride Sat, 8/19/2017 Lunch Ride 2:06:39 30.17 mi 627 ft
|
||||
Ride Sun, 3/12/2017 Morning Ride 2:18:13 30.16 mi 600 ft
|
||||
Ride Sat, 11/21/2015 Morning Ride 2:20:54 30.16 mi 1,499 ft
|
||||
Ride Wed, 10/12/2016 Morning Ride 2:19:51 30.11 mi 328 ft
|
||||
Ride Sun, 7/8/2018 Lunch Ride 2:12:19 30.04 mi 768 ft
|
||||
Ride Tue, 11/22/2016 Morning Ride 2:18:57 30.04 mi 518 ft
|
||||
Ride Wed, 3/4/2015 Commute 2:15:56 30.02 mi 1,125 ft
|
||||
Ride Sun, 3/25/2018 Dumbarton / Coyote Hills / Middle of the Bay 2:31:51 30.01 mi 709 ft
|
||||
Ride Tue, 5/31/2016 Google Bike around campus 2:30:00 30.00 mi 0 ft
|
||||
Ride Tue, 2/14/2017 Morning Ride 2:22:16 29.92 mi 463 ft
|
||||
Ride Sun, 5/31/2015 05/31/2015 Palo Alto, California 2:35:24 29.85 mi 2,568 ft
|
||||
Ride Tue, 11/1/2016 Morning Ride 2:15:55 29.78 mi 515 ft
|
||||
Ride Sun, 6/5/2016 Morning Ride 2:18:02 29.70 mi 1,293 ft
|
||||
Ride Sun, 5/26/2019 San Carlos / Bay Trail 2:17:51 29.55 mi 620 ft
|
||||
Ride Thu, 8/13/2015 08/13/2015 Palo Alto, CA 2:13:58 29.51 mi 778 ft
|
||||
Ride Sun, 7/14/2013 07/14/2013 Palo Alto, CA 2:23:07 29.34 mi 739 ft
|
||||
Ride Sun, 4/3/2016 Lunch Ride 2:03:38 29.31 mi 810 ft
|
||||
Ride Mon, 3/9/2015 Palo Alto Cycling 1:57:50 29.21 mi 115 ft
|
||||
Ride Fri, 2/23/2018 Morning Ride 2:08:28 29.21 mi 166 ft
|
||||
Ride Sat, 10/7/2017 Afternoon Ride 2:02:43 29.18 mi 394 ft
|
||||
Ride Sat, 1/17/2015 Palo Alto Cycling 2:08:38 29.06 mi 1,587 ft
|
||||
Ride Sat, 1/18/2014 Los Altos Hills 2:32:23 29.03 mi 1,918 ft
|
||||
Ride Sat, 6/20/2015 06/20/2015 Palo Alto, CA 2:06:45 28.87 mi 1,650 ft
|
||||
Ride Sun, 11/3/2013 work 2:11:08 28.74 mi 841 ft
|
||||
Ride Wed, 8/16/2017 Lunch Ride 2:15:48 28.67 mi 732 ft
|
||||
Ride Sat, 1/9/2016 Morning Ride 2:07:35 28.54 mi 1,289 ft
|
||||
Ride Sat, 9/19/2015 Los Altos Hills 2:31:24 28.51 mi 1,611 ft
|
||||
Ride Wed, 8/12/2015 08/12/2015 Palo Alto, CA 2:02:14 28.48 mi 751 ft
|
||||
Ride Sun, 7/23/2017 Morning Ride 2:57:35 28.43 mi 2,562 ft
|
||||
Ride Mon, 8/24/2015 Morning Ride 2:10:31 28.35 mi 427 ft
|
||||
Ride Wed, 6/26/2013 work 2:01:29 28.33 mi 627 ft
|
||||
Ride Sun, 4/28/2019 Arastradsero 2:29:59 28.21 mi 1,355 ft
|
||||
Ride Sun, 7/19/2015 07/19/2015 Palo Alto, CA 2:00:27 28.17 mi 1,037 ft
|
||||
Ride Fri, 12/28/2018 Lunch Ride with Juliet 2:17:33 28.10 mi 331 ft
|
||||
Ride Tue, 5/17/2016 Morning Ride 2:06:55 28.09 mi 551 ft
|
||||
Ride Wed, 10/5/2016 Morning Ride 2:10:47 28.05 mi 276 ft
|
||||
Ride Wed, 6/22/2016 Morning Ride 2:00:40 27.99 mi 374 ft
|
||||
Ride Mon, 3/16/2015 Commute 2:06:51 27.97 mi 70 ft
|
||||
Ride Sun, 2/25/2018 Lunch Ride 2:10:54 27.88 mi 958 ft
|
||||
Ride Sat, 10/27/2018 Morning Ride 2:29:13 27.88 mi 1,581 ft
|
||||
Ride Mon, 6/20/2016 Morning Ride 1:52:13 27.77 mi 531 ft
|
||||
Ride Sun, 12/2/2018 Lunch Ride 2:34:14 27.72 mi 1,506 ft
|
||||
Ride Mon, 3/19/2018 Morning Ride 2:06:35 27.62 mi 860 ft
|
||||
Ride Mon, 12/17/2018 Morning Ride 2:16:12 27.60 mi 246 ft
|
||||
Ride Sat, 10/14/2017 Afternoon Ride 2:04:53 27.52 mi 387 ft
|
||||
Ride Sat, 10/18/2014 Bikepacking Monte Bello 2:54:20 27.44 mi 2,992 ft
|
||||
Ride Sun, 4/15/2018 Morning Ride 2:17:16 27.43 mi 1,667 ft
|
||||
Ride Mon, 4/10/2017 Morning Ride 2:03:19 27.40 mi 282 ft
|
||||
Ride Tue, 5/7/2019 Lunch Ride 2:07:55 27.37 mi 820 ft
|
||||
Ride Sun, 11/25/2018 Afternoon Ride 1:56:26 27.34 mi 203 ft
|
||||
Ride Wed, 4/29/2015 Palo Alto Cycling 2:03:14 27.33 mi 75 ft
|
||||
Ride Tue, 1/17/2017 Morning Ride 2:07:17 27.30 mi 535 ft
|
||||
Ride Tue, 9/30/2014 09/30/2014 Palo Alto, CA 2:07:33 27.28 mi 636 ft
|
||||
Ride Sat, 9/17/2016 TourDeCoop.org 2:04:27 27.24 mi 479 ft
|
||||
Ride Sat, 1/12/2019 Lunch Ride 1:58:47 27.24 mi 1,079 ft
|
||||
Ride Mon, 3/30/2015 Woodside Loop 1:47:46 27.22 mi 1,081 ft
|
||||
Ride Tue, 11/17/2015 Afternoon Ride 2:04:48 27.12 mi 830 ft
|
||||
Ride Sun, 8/13/2017 Woodside Loop (Battery ran down) 2:00:00 27.00 mi 1,000 ft
|
||||
Ride Sat, 1/31/2015 Alpine Rd 2:29:26 26.98 mi 2,362 ft
|
||||
Ride Sun, 2/28/2016 Woodside Loop 1:43:34 26.93 mi 843 ft
|
||||
Ride Sun, 4/1/2018 Lunch Ride 2:30:35 26.92 mi 1,831 ft
|
||||
Workout Wed, 5/3/2017 Palo Alto Road Cycling 2:06:05 26.86 mi 1,411 ft
|
||||
Ride Fri, 9/9/2016 Morning Ride 1:56:44 26.85 mi 1,086 ft
|
||||
Ride Sun, 9/23/2018 Morning Ride 1:57:48 26.83 mi 512 ft
|
||||
Ride Sat, 10/4/2014 Woodside Loop 1:52:09 26.71 mi 1,213 ft
|
||||
Ride Thu, 5/7/2015 Commute 1:55:36 26.58 mi 68 ft
|
||||
Ride Sun, 3/24/2019 Afternoon Ride 2:42:45 26.57 mi 2,267 ft
|
||||
Ride Thu, 8/31/2017 Morning Ride 2:00:02 26.54 mi 817 ft
|
||||
Ride Sun, 4/24/2016 Afternoon Ride 1:50:08 26.53 mi 935 ft
|
||||
Ride Sun, 12/17/2017 Lunch Ride 1:50:51 26.53 mi 217 ft
|
||||
Ride Tue, 7/4/2017 Lunch Ride 1:53:04 26.50 mi 459 ft
|
||||
Ride Tue, 5/8/2018 Afternoon Ride 2:34:11 26.50 mi 1,791 ft
|
||||
Ride Tue, 5/26/2015 05/26/2015 Palo Alto, CA 2:01:47 26.50 mi 591 ft
|
||||
Ride Thu, 8/4/2016 Afternoon Ride 1:55:47 26.49 mi 659 ft
|
||||
Ride Wed, 6/8/2016 Lunch Ride 1:48:19 26.48 mi 597 ft
|
||||
Ride Sun, 9/9/2018 Lunch Ride 2:00:49 26.46 mi 479 ft
|
||||
Ride Sat, 5/21/2016 Maker Faire 1:57:14 26.44 mi 207 ft
|
||||
Ride Wed, 4/1/2015 Commute 1:55:15 26.44 mi 71 ft
|
||||
Ride Wed, 6/29/2016 Morning Ride 1:49:33 26.39 mi 561 ft
|
||||
Ride Sat, 9/9/2017 Lunch Ride 1:59:01 26.38 mi 1,112 ft
|
||||
Ride Fri, 6/3/2016 Morning Ride 1:56:57 26.38 mi 502 ft
|
||||
Ride Fri, 3/31/2017 Morning Ride 2:01:41 26.36 mi 495 ft
|
||||
Ride Sat, 12/13/2014 Westridge (up the back way; after the rain) 2:01:01 26.35 mi 1,495 ft
|
||||
Ride Sun, 4/14/2019 Huddart - Paul Hopkins 2:08:57 26.34 mi 1,601 ft
|
||||
Ride Sat, 6/29/2013 Untitled 1:48:50 26.33 mi 1,091 ft
|
||||
Ride Sat, 4/25/2015 Woodside 1:54:30 26.28 mi 1,220 ft
|
||||
Ride Tue, 1/31/2017 Morning Ride 2:01:18 26.27 mi 384 ft
|
||||
Ride Tue, 6/28/2016 Morning Ride 1:55:31 26.27 mi 571 ft
|
||||
Ride Sat, 5/19/2018 Morning Ride 2:00:24 26.25 mi 958 ft
|
||||
Ride Thu, 6/30/2016 Morning Ride 1:52:16 26.24 mi 499 ft
|
||||
Ride Sun, 2/19/2017 Afternoon Ride 1:56:46 26.23 mi 1,138 ft
|
||||
Ride Thu, 8/18/2016 Morning Ride 1:58:06 26.23 mi 712 ft
|
||||
Ride Sun, 4/20/2014 Afternoon Ride 1:52:52 26.23 mi 102 ft
|
||||
Ride Sun, 5/10/2015 Arastadero 1:48:58 26.22 mi 1,148 ft
|
||||
Ride Thu, 5/19/2016 Morning Ride 1:52:56 26.14 mi 938 ft
|
||||
Ride Mon, 12/31/2018 Morning Ride 2:00:24 26.11 mi 545 ft
|
||||
Ride Fri, 12/15/2017 Afternoon Ride 1:51:36 26.07 mi 226 ft
|
||||
Ride Sun, 1/18/2015 Palo Alto Cycling 1:38:16 26.02 mi 1,257 ft
|
||||
Ride Mon, 10/3/2016 Morning Ride 1:54:45 26.01 mi 646 ft
|
||||
Ride Tue, 7/14/2015 07/14/2015 Mountain View, California 2:02:36 25.99 mi 502 ft
|
||||
Ride Fri, 12/18/2015 Lunch Ride 1:58:57 25.93 mi 909 ft
|
||||
Ride Tue, 6/13/2017 Morning Ride 1:55:30 25.89 mi 289 ft
|
||||
Ride Mon, 8/4/2014 To the Sea 2:17:11 25.87 mi 2,080 ft
|
||||
Ride Wed, 9/25/2013 work and with kris 2:07:40 25.81 mi 593 ft
|
||||
Ride Sun, 9/13/2015 Afternoon Ride 1:49:31 25.78 mi 1,152 ft
|
||||
Ride Sun, 3/3/2019 Afternoon Ride 2:00:06 25.73 mi 390 ft
|
||||
Ride Thu, 1/26/2017 Morning Ride 1:56:09 25.67 mi 259 ft
|
||||
Ride Sun, 5/6/2018 Morning Ride 1:55:05 25.67 mi 1,148 ft
|
||||
Ride Sat, 7/29/2017 Lunch Ride 1:53:50 25.65 mi 988 ft
|
||||
Ride Wed, 8/20/2014 08/20/2014 Palo Alto, CA 1:49:35 25.61 mi 497 ft
|
||||
Ride Sat, 4/19/2014 Lunch Ride 1:49:37 25.60 mi 1,111 ft
|
||||
Ride Mon, 9/3/2018 Afternoon Ride 1:59:08 25.54 mi 331 ft
|
||||
Ride Mon, 6/2/2014 Work 1:59:33 25.51 mi 627 ft
|
||||
Ride Fri, 12/11/2015 Afternoon Ride 2:12:40 25.44 mi 1,719 ft
|
||||
Ride Fri, 2/9/2018 Morning Ride 1:56:11 25.41 mi 318 ft
|
||||
Ride Sun, 8/26/2018 Afternoon Ride 1:49:16 25.37 mi 377 ft
|
||||
Ride Sun, 7/9/2017 Afternoon Ride 1:50:30 25.36 mi 682 ft
|
||||
Ride Mon, 6/26/2017 Morning Ride 1:52:26 25.34 mi 226 ft
|
||||
Ride Fri, 9/14/2018 Lunch Ride 1:50:05 25.34 mi 541 ft
|
||||
Ride Fri, 3/1/2019 Morning Ride 2:00:23 25.34 mi 121 ft
|
||||
Ride Sun, 1/13/2019 Lunch Ride 1:51:40 25.33 mi 715 ft
|
||||
Ride Wed, 4/12/2017 Morning Ride 1:59:22 25.33 mi 686 ft
|
||||
Ride Wed, 6/14/2017 Morning Ride 1:51:32 25.33 mi 308 ft
|
||||
Ride Sat, 11/28/2015 Lunch Ride 1:43:56 25.31 mi 1,122 ft
|
||||
Ride Mon, 11/27/2017 Morning Ride 1:55:39 25.31 mi 184 ft
|
||||
Ride Fri, 11/28/2014 11/28/2014 Palo Alto, CA 1:45:13 25.30 mi 1,112 ft
|
||||
Ride Fri, 4/13/2018 Morning Ride 1:53:39 25.28 mi 344 ft
|
||||
Ride Fri, 9/15/2017 Morning Ride 1:56:55 25.28 mi 384 ft
|
||||
Ride Tue, 12/31/2013 Woodside Loop - Last Ride of the Year 1:45:28 25.27 mi 1,207 ft
|
||||
Ride Sun, 10/14/2018 Lunch Ride 2:13:13 25.23 mi 1,421 ft
|
||||
Ride Sat, 11/22/2014 Atherton to Woodside 1:43:10 25.21 mi 1,095 ft
|
||||
Ride Sat, 6/4/2016 Morning Ride 1:57:23 25.19 mi 922 ft
|
||||
Ride Mon, 8/8/2016 Morning Ride 1:52:38 25.17 mi 423 ft
|
||||
Ride Fri, 11/6/2015 Morning Ride 1:51:45 25.15 mi 538 ft
|
||||
Ride Mon, 12/18/2017 Afternoon Ride 1:52:56 25.15 mi 741 ft
|
||||
Ride Fri, 6/24/2016 Afternoon Ride 1:35:25 25.11 mi 623 ft
|
||||
Ride Sat, 6/25/2016 Afternoon Ride 1:44:43 25.10 mi 568 ft
|
||||
Ride Sat, 1/25/2014 Woodside 1:33:38 25.08 mi 1,243 ft
|
||||
Ride Tue, 4/29/2014 Woodside 1:46:03 25.08 mi 886 ft
|
||||
Ride Thu, 4/13/2017 Morning Ride 2:06:13 25.06 mi 266 ft
|
||||
Ride Sun, 6/12/2016 Morning Ride 1:49:25 25.04 mi 387 ft
|
||||
Ride Thu, 6/8/2017 Morning Ride 1:56:19 25.02 mi 285 ft
|
||||
Ride Thu, 10/13/2016 Morning Ride 1:47:30 25.02 mi 253 ft
|
||||
Ride Sun, 1/19/2014 01/19/2014 Palo Alto, CA 1:36:55 25.01 mi 1,201 ft
|
||||
Ride Mon, 9/5/2016 Afternoon Ride 1:42:56 25.00 mi 902 ft
|
||||
Ride Sat, 11/8/2014 Atherton - Woodside 1:40:51 25.00 mi 972 ft
|
||||
Ride Sun, 12/10/2017 Exercise Bike 1:30:00 25.00 mi 0 ft
|
||||
Ride Sun, 6/2/2019 The Sequoia 6:40:43 77.51 mi 6,467 ft
|
||||
Ride Sat, 6/8/2019 Morning Ride 2:53:08 34.42 mi 1,824 ft
|
||||
Ride Sat, 6/15/2019 Morning Ride 2:29:38 29.98 mi 1,785 ft
|
||||
Ride Fri, 7/5/2019 Morning Ride 4:30:55 64.05 mi 1,965 ft
|
||||
|
+56
-64
@@ -1,64 +1,56 @@
|
||||
Alma Mountain Charlie, 3.12, 875, 31:49
|
||||
Alpine Westridge, 0.76, 99, 3:26, 3:29, 3:43
|
||||
Alpine last kicker, 0.39, 114, 3:11, 3:36, 5:19
|
||||
Arastradero, 1.19, 193, 6:41, 7:49, 7:55
|
||||
Bring it On Home, 10.81, 237, 37:19
|
||||
Cabrillo Hmy S, 0.46, 153, 4:31
|
||||
Canada South Interval, 1.42, 54, 4:50, 5:08, 5:20
|
||||
Canada to College, 1.37, 119, 5:55, 6:23, 6:35
|
||||
Canon, 0.90, 295, 11:05, 11:56
|
||||
Canon to No Cycling, 0.75, 198, 5:14, 5:41
|
||||
El Camino, 0.50, 15, 2:11, 2:19, 2:22
|
||||
Foothill Arastradero, 1.13, 37, 3:31, 3:44, 3:51
|
||||
Foothill Homestead, 1.22, 126, 5:31, 6:36, 6:53
|
||||
Foothill Magdalena, 1.66, 44, 5:51, 6:21, 6:29
|
||||
Foothill Miramonte, 1.64, 39, 4:55, 5:29, 5:43
|
||||
Haskins, 1.51, 566, 17:48, 18:43, 18:48
|
||||
Highway 9, 0.56, 260, 8:33
|
||||
Highway 9 Mantalvo, 0.45, 35, 1:36, 1:48, 1:53
|
||||
Huddart, 0.92, 385, 10:15, 11:18, 12:55
|
||||
Joaquin, 0.33, 253, 6:19, 7:27
|
||||
Kaboom Portola Rd, 0.67, 102, 3:14, 3:26, 3:33
|
||||
Kings half way, 2.89, 820, 27:26, 29:45, 32:43
|
||||
Kings to Skeggs, 1.10, 273, 9:08, 10:39, 10:54
|
||||
Limantour Spit, 0.47, 303, 5:20
|
||||
Limantour steepest, 0.20, 159, 5:41
|
||||
Lobitas Creek, 0.96, 430, 12:15, 12:07
|
||||
Lower Redwood Gulch, 1.03, 474, 13:00
|
||||
Mapache, 0.24, 111, 2:52, 2:56
|
||||
Mt Eden climb, 1.02, 272, 8:32, 9:43, 11:59
|
||||
Mt Eden to Archery, 0.54, 180, 5:20, 5:39, 6:12
|
||||
Old La Honda, 2.98, 1255, 28:49, 34:04, 36:45
|
||||
Old La Honda Mile 1, 0.99, 370, 8:02, 9:36, 9:51
|
||||
Page Mill to Moody, 0.21, 112, 2:50, 2:55
|
||||
Page Mill to Ventana, 0.47, 196, 4:54, 5:32, 6:05
|
||||
Peach Hill, 0.60, 248, 7:30, 7:50
|
||||
Pescadero Bean Hollow, 2.77, 51, 8:07, 10:48
|
||||
Pomponio Creek, 0.38, 122, 2:47, 2:56, 3:16
|
||||
Red Hill KOM, 0.34, 161, 4:15
|
||||
Redwood Gulch hits, 0.18, 151, 3:36
|
||||
Redwood Gulch wall, 0.43, 258, 6:39
|
||||
Sand Gill Sharon-top, 0.85, 136, 4:04, 4:16, 4:24
|
||||
Sand Hill 280 to horse, 0.49, 95, 2:34, 2:44, 2:51
|
||||
Sand Hill Alpine to 280, 1.67, 180, 7:02, 7:40, 8:02
|
||||
Stair Step, 0.32, 175, 5:25
|
||||
Stanford Ave, 0.63, 85, 3:14, 3:32, 3:49
|
||||
Stevens Canyon, 2.85, 127, 12:51, 13:46, 18:12
|
||||
Stevens Country Park, 1.22, 112, 5:43, 5:59, 7:00
|
||||
Stirrup Wall, 0.17, 125, 3:47, 4:33
|
||||
Stirrup to Moon, 0.36, 159, 3:41, 6:07
|
||||
Summit Rd Climb, 0.60, 275, 8:00
|
||||
Tepa, 0.58, 248, 6:34, 7:14, 8:21
|
||||
The Boneyard, 1.48, 135, 6:05, 6:36, 7:00
|
||||
Try not to fall back, 0.71, 410, 12:45
|
||||
Tunitas Knoll to Star, 1.55, 339, 12:28, 12:56, 13:51
|
||||
Tunitas flattens, 0.42, 166, 3:16, 3:25, 3:51
|
||||
Tunitas lower climb, 1.30, 421, 13:34, 16:00, 17:33
|
||||
Tunitas steep, 1.20, 599, 16:16, 17:48, 20:44
|
||||
Vickrey Fruitvale, 0.99, 68, 3:41, 3:58, 4:27
|
||||
West Alpine switchback, 0.78, 322, 8:56, 10:52, 11:56
|
||||
Westridge, 0.68, 385, 8:28, 9:52, 10:19
|
||||
Westridge 3min, 0.37, 240, 4:41, 5:35, 6:31
|
||||
Westridge Hill 1, 0.51, 104, 3:19, 3:58
|
||||
Westridge Hill 2, 0.51, 166, 4:42, 5:10, 5:21
|
||||
Woodside Climb, 1.71, 295, 8:05, 9:00, 9:35
|
||||
Alma Mountain Charlie, 3.12, 875, 31:49
|
||||
Alpine Westridge, 0.76, 99, 3:26, 3:29, 3:43
|
||||
Alpine last kicker, 0.39, 114, 3:11, 3:36, 5:19
|
||||
Arastradero, 1.19, 193, 6:41, 7:49, 7:55
|
||||
Cabrillo Hmy S, 0.46, 153, 4:31
|
||||
Canada to College, 1.37, 119, 5:55, 6:23, 6:35
|
||||
Canon, 0.90, 295, 11:04, 11:55
|
||||
Canon to No Cycling, 0.75, 198, 5:14, 5:41
|
||||
Foothill Homestead, 1.22, 126, 5:31, 6:36, 6:53
|
||||
Haskins, 1.51, 566, 17:47, 18:43, 18:47
|
||||
Highway 9, 0.56, 260, 8:33
|
||||
Highway 9 Mantalvo, 0.45, 35, 1:36, 1:48, 1:53
|
||||
Huddart, 0.92, 385, 10:15, 11:18, 12:55
|
||||
Joaquin, 0.33, 253, 6:19, 7:27
|
||||
Kaboom Portola Rd, 0.67, 102, 3:14, 3:26, 3:33
|
||||
Kings half way, 2.89, 820, 27:26, 29:45, 32:43
|
||||
Kings to Skeggs, 1.10, 273, 9:07, 10:39, 10:53
|
||||
Limantour Spit, 0.47, 303, 5:19
|
||||
Limantour steepest, 0.20, 159, 5:41
|
||||
Lobitas Creek, 0.96, 430, 12:15, 12:07
|
||||
Lower Redwood Gulch, 1.03, 474, 13:00
|
||||
Mapache, 0.24, 111, 2:52, 2:55
|
||||
Mt Eden climb, 1.02, 272, 8:32, 9:43, 11:59
|
||||
Mt Eden to Archery, 0.54, 180, 5:19, 5:39, 6:12
|
||||
Old La Honda, 2.98, 1255, 28:49, 34:03, 36:44
|
||||
Old La Honda Mile 1, 0.99, 370, 8:02, 9:36, 9:51
|
||||
Page Mill to Moody, 0.21, 112, 2:50, 2:55
|
||||
Page Mill to Ventana, 0.47, 196, 4:54, 5:32, 6:05
|
||||
Peach Hill, 0.60, 248, 7:30, 7:50
|
||||
Pomponio Creek, 0.38, 122, 2:47, 2:55, 3:16
|
||||
Red Hill KOM, 0.34, 161, 4:15
|
||||
Redwood Gulch hits, 0.18, 151, 3:36
|
||||
Redwood Gulch wall, 0.43, 258, 6:39
|
||||
Sand Gill Sharon-top, 0.85, 136, 4:03, 4:16, 4:24
|
||||
Sand Hill 280 to horse, 0.49, 95, 2:34, 2:44, 2:51
|
||||
Sand Hill Alpine to 280, 1.67, 180, 7:02, 7:39, 8:02
|
||||
Stair Step, 0.32, 175, 5:25
|
||||
Stanford Ave, 0.63, 85, 3:14, 3:32, 3:49
|
||||
Stevens Country Park, 1.22, 112, 5:42, 5:58, 7:00
|
||||
Stirrup Wall, 0.17, 125, 3:47, 4:33
|
||||
Stirrup to Moon, 0.36, 159, 3:41, 6:07
|
||||
Summit Rd Climb, 0.60, 275, 8:00
|
||||
Tepa, 0.58, 248, 6:34, 7:14, 8:21
|
||||
The Boneyard, 1.48, 135, 6:05, 6:36, 7:00
|
||||
Try not to fall back, 0.71, 410, 12:45
|
||||
Tunitas Knoll to Star, 1.55, 339, 12:28, 12:56, 13:51
|
||||
Tunitas flattens, 0.42, 166, 3:16, 3:25, 3:51
|
||||
Tunitas lower climb, 1.30, 421, 13:34, 16:00, 17:33
|
||||
Tunitas steep, 1.20, 599, 16:15, 17:47, 20:44
|
||||
Vickrey Fruitvale, 0.99, 68, 3:41, 3:58, 4:26
|
||||
West Alpine switchback, 0.78, 322, 8:56, 10:52, 11:55
|
||||
Westridge, 0.68, 385, 8:28, 9:52, 10:18
|
||||
Westridge 3min, 0.37, 240, 4:41, 5:35, 6:31
|
||||
Westridge Hill 1, 0.51, 104, 3:19, 3:58
|
||||
Westridge Hill 2, 0.51, 166, 4:42, 5:10, 5:20
|
||||
Woodside Climb, 1.71, 295, 8:05, 9:00, 9:35
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
+407
-406
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,559 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"\n",
|
||||
"<div align=\"right\" style=\"text-align:right\"><i>Peter Norvig<br>May 2020</i></div>\n",
|
||||
"\n",
|
||||
"# Flipping Cards: A Guessing Game\n",
|
||||
"\n",
|
||||
"The [538 Riddler for Aug. 17, 2018](\n",
|
||||
"https://fivethirtyeight.com/features/step-1-game-theory-step-2-step-3-profit/) poses this problem:\n",
|
||||
"\n",
|
||||
"> Take a standard deck of cards, and pull out the numbered cards from one suit (the nine cards 2 through 10). Shuffle them, and then lay them face down in a row. Flip over the first card so it is face up. Now guess whether the next card in the row (the first down card) is higher or lower. If you’re right, keep going. If you play this game optimally, what’s the probability that you can get to the end without making any mistakes?<p>\n",
|
||||
"> *Extra credit:* What if there were more cards — 2 through 20, or 2 through 100? How do your chances of getting to the end change?\n",
|
||||
" \n",
|
||||
"First, let's define \"*play this game optimally.*\" The optimal policy is to look at the current flipped-up card and, remembering what cards have already been flipped and thus knowing what cards remain face-down, guess that the first down card will be \"higher\" if the majority of the down cards are higher than the up card, otherwise guess that it will be \"lower\". (*Details*: In case of a tie in the counts, guessing either \"higher\" or \"lower\" is equally optimal. When there are only two cards left (one up, one down) you know for sure what the down card is, so you can always guess right.)\n",
|
||||
" \n",
|
||||
"# Brute Force Algorithm\n",
|
||||
" \n",
|
||||
"We can solve the problem with brute force: for every permutation of the cards, call `guess_all` to determine if the optimal policy guesses right on that sequence of cards. Assuming every permutation is equally likely (that is, the cards were shuffled fairly), the overall probability of guessing right is the proportion of `True` results.\n",
|
||||
" \n",
|
||||
"> *Python Trivia*: `True` is equal to `1` and `False` is equal to `0`, so `sum([True, False, True])` is 2; `mean([True, False, True])` is 2/3, and when `b` is a Boolean and `x` is a number, the expression `(b and x)` is equivalent to `(x if b else 0)` and the expression `(x - b)` is equivalent to `((x - 1) if b else x)`. In other languages you would be required (or at least encouraged) to explicitly cast Booleans to integers, but in Python `bool` is a subclass of `int` and it is considered good style to do without casts.\n",
|
||||
" \n",
|
||||
"I can implement `guess_all` as follows:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%matplotlib inline\n",
|
||||
"from matplotlib import pyplot as plt\n",
|
||||
"from itertools import permutations\n",
|
||||
"from statistics import mean\n",
|
||||
"from functools import lru_cache"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def guess_all(cards) -> bool: \n",
|
||||
" \"\"\"You guess all the cards right if you guess the first right and the rest right.\"\"\"\n",
|
||||
" return len(cards) <= 2 or guess_first(cards) and guess_all(cards[1:])\n",
|
||||
"\n",
|
||||
"def guess_first(cards) -> bool:\n",
|
||||
" \"\"\"Guess that the first down card will be \"higher\" or \"lower\" according to majority \n",
|
||||
" counts of the down cards; return True if the guess is correct.\"\"\"\n",
|
||||
" up, *down = cards\n",
|
||||
" higher = sum(d > up for d in down)\n",
|
||||
" lower = sum(d < up for d in down)\n",
|
||||
" return (higher > lower) == (down[0] > up)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Some simple examples:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# On this sequence, the optimal policy always guesses \"higher\"; \n",
|
||||
"# and it turns out that's always right. \n",
|
||||
"guess_all((2, 3, 4, 5, 6))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"False"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# On this sequence, the optimal policy guesses \"higher\" first; that's wrong.\n",
|
||||
"# There is a 3/4 chance that a down card is higher than 3, but it turns\n",
|
||||
"# out that the first down card, 2, is the one lower card.\n",
|
||||
"guess_all((3, 2, 4, 5, 6)) "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"False"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"guess_first((3, 2, 4, 5, 6))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# The Answer\n",
|
||||
"\n",
|
||||
"The probability that we can guess all nine cards right is the average over all possible permutations of the cards:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"0.17085537918871252"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"cards = \"🃂🃃🃄🃅🃆🃇🃈🃉🃊\" # Use Unicode cards instead of boring ints; why not?\n",
|
||||
"answer = mean(map(guess_all, permutations(cards)))\n",
|
||||
"answer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"That's the correct answer ([per 538](https://fivethirtyeight.com/features/how-many-hoops-will-kids-jump-through-to-play-rock-paper-scissors/)): about 17% chance of guessing all nine cards right.\n",
|
||||
"____\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Extra Credit: Efficient Algorithm\n",
|
||||
"\n",
|
||||
"The extra credit problem asks about 19 and 99 cards. But there are $19! \\approx 10^{17}$ permutations of 19 cards and a whopping $99! \\approx 10^{156}$ permutations of 99 cards. So my brute force algorithm won't work. \n",
|
||||
"\n",
|
||||
"(*Note*: since we are now dealing with the general case of $n$ cards, I'll represent them as `range(n)`, that is, starting at `0` rather than `2`.)\n",
|
||||
"\n",
|
||||
"# Efficient Algorithm: Analysis\n",
|
||||
"\n",
|
||||
"We will need a more efficient way of representing cards, so that we don't need to consider $n!$ possibilities. I see two ideas that will help. Consider what happens when we are deciding whether to guess \"higher\" or \"lower\" in `guess_first(cards)`: \n",
|
||||
"\n",
|
||||
"**First**, the **order** of the `down` cards doesn't matter. The optimal policy makes the same guess whether `down` is `(2, 3, 4, 5)` or a permutation such as `(4, 2, 5, 3)`. So we could represent cards as a set: `{2, 3, 4, 5}`. As we discard flipped cards, we must consider subsets. Since there are $n!$ permutations but only $2^n$ subsets of $n$ cards, this is a nice improvement, good enough to handle $n=19$ (but not good enough for $n=99$).\n",
|
||||
"\n",
|
||||
"**Second**, the **identity** of the `down` cards doesn't matter. What does matter is how many down cards are higher or lower than the up card. For example, when the up card is `3` it doesn't matter if the remaining cards are `{2, 3, 6}` or `{1, 3, 4}` or `{0, 3, 5}` or any other set of three cards in which the `3` is the middle card. For any such set, there is one card higher and one card lower than the up card, so the chance of guessing right is 50%. Note that I can't represent all these situations as \"*there are 3 cards remaining and the up card is 3*\" but I can represent them as \"*there are 3 cards remaining and the up card is the second lowest*\". \n",
|
||||
"\n",
|
||||
"In other words, I can describe the up card by **renumbering** it to be its index into the sorted list of remaining cards (up and down). For each of the three sets described here, the index of the `3` card is `1`; it is the second element of the sorted list of cards:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for cards in ({2, 3, 6}, {1, 3, 4}, {0, 3, 5}):\n",
|
||||
" assert sorted(cards).index(3) == 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Some further examples of renumbering:\n",
|
||||
"\n",
|
||||
" - [Cards: `{2, 3, 6}` Up: `3`] *renumbered to* [Cards: `range(3)` Up: `1`] (i.e., second lowest)\n",
|
||||
" - [Cards: `{1, 3, 4}` Up: `3`] *renumbered to* [Cards: `range(3)` Up: `1`] (i.e., second lowest)\n",
|
||||
" - [Cards: `{0, 3, 5}` Up: `3`] *renumbered to* [Cards: `range(3)` Up: `1`] (i.e., second lowest)\n",
|
||||
" - [Cards: `{3, 5, 7}` Up: `3`] *renumbered to* [Cards: `range(3)` Up: `0`] (i.e., lowest)\n",
|
||||
" - [Cards: `{0, 1, 3}` Up: `3`] *renumbered to* [Cards: `range(3)` Up: `2`] (i.e., third lowest)\n",
|
||||
"\n",
|
||||
" \n",
|
||||
"Now I can describe any situation with just two integers, the number of cards and the index of the up card. That means there are only $n^2$ possible situations to describe, which should be good enough to handle $n=99$ and beyond. \n",
|
||||
"\n",
|
||||
"What I have done is gone from a **concrete** representation of the cards as explicit permutations to an **abstract** representation that says how many cards remain and what the index number of the up card is, but doesn't say exactly what the cards are nor what order they are in. This kind of abstraction is often a good way to fight against combinatorial explosion.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Efficient Algorithm: First Try\n",
|
||||
"\n",
|
||||
"I wanted to define a function, `P_all(n)`, that tells me the probability of guessing all `n` cards right. I wrote down a first try at a definition:\n",
|
||||
"\n",
|
||||
" def P_all(n) -> float:\n",
|
||||
" return 1 if n <= 2 else P_first(n) * P_all(n - 1)\n",
|
||||
" \n",
|
||||
"Could it really be as simple as that? All I have to do is define `P_first(n)` (the probability of guessing the first of `n` cards right) and I'm done? My immediate reaction was that this feels *too* easy. I was suspicious of this definition for three reasons:\n",
|
||||
"\n",
|
||||
"1. It is rare (but not unheard of) for an efficient algorithm to be simpler than the brute force algorithm.\n",
|
||||
"- I said there should be renumbering, but this code doesn't do any renumbering.\n",
|
||||
"- This code says the joint probability of getting the first card right and then getting the rest right is the product of their probabilities. But that's true *only when the two events are independent*. And in flipping cards, the two events are *not* independent, because both events depend on the card that is the first down card in `P_first(n)` and becomes the up card in `P_all(n - 1)`. \n",
|
||||
"\n",
|
||||
"My suspicions were well-founded: this code won't work.\n",
|
||||
"\n",
|
||||
"# Efficient Algorithm: Second Try\n",
|
||||
"\n",
|
||||
"I need to capture the fact that the card that is the first down card for the current guess becomes the up card for the next guess. One way to do that is to define `P_all_given(up, n)` to be the probability of guessing all the cards right given that the flipped-up card has index `up` in the sorted list of `n` remaining cards (including the `up` card).\n",
|
||||
"\n",
|
||||
"Now `P_all(n)` is defined to call `P_all_given(up, n)` for each possible `up` card and average the results:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def P_all(n) -> float:\n",
|
||||
" \"\"\"Probability of guessing all n cards right.\"\"\"\n",
|
||||
" return mean(P_all_given(up, n) for up in range(n))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"`P_all_given(up, n)` works by considering each possible first down card (`down1`), computing if we would guess the first down card right given the up card (`guess_first_given(up, down1, n)`), and if right, calling `P_all_given` recursively with the old first down card (renumbered) being the new up card, and the deck size, `n`, being one card smaller. \n",
|
||||
"When this is all done, we take the average probability value over all the down cards. We also arrange for `P_all_given` to cache its values so that we don't repeat computations; this is crucial for efficiency."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"@lru_cache(None)\n",
|
||||
"def P_all_given(up, n) -> float:\n",
|
||||
" \"\"\"Probability of guessing all n cards right, given the index of the up card.\"\"\"\n",
|
||||
" return (1 if n <= 2 else\n",
|
||||
" mean(guess_first_given(up, down1, n) and P_all_given(renumber(down1, up), n - 1)\n",
|
||||
" for down1 in range(n) if down1 != up))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The function `guess_first_given(up, down1, n)`, defined below, returns true if we would correctly guess the first down card given the up card. The optimal policy is to guess that the first down card will be higher than the renumbered up card when the up card is in the lower half of `range(n)`. \n",
|
||||
"\n",
|
||||
"And finally, `renumber` renumbers a card by decrementing it by one if the card we are discarding (the up card) is lower (meaning that the card we are keeping becomes one closer to the lowest possible card). Otherwise the card's number is unchanged."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def guess_first_given(up, down1, n) -> bool:\n",
|
||||
" \"\"\"Do we guess the first down card right given the up card, \n",
|
||||
" when the set of cards (including up and down) is range(n)?\"\"\"\n",
|
||||
" return (up < n / 2) == (down1 > up)\n",
|
||||
"\n",
|
||||
"def renumber(card, discard) -> int: return card - (discard < card)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can see that `P_all(n)` agrees with the brute-force answer (except for round-off in the 17th decimal place):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0.17085537918871255\n",
|
||||
"0.17085537918871252\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(P_all(9))\n",
|
||||
"print(answer)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# The Extra Credit Answers\n",
|
||||
"\n",
|
||||
"We can now efficiently answer the extra credit questions:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"0.008419397002884993"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"P_all(19)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"6.67213407124781e-14"
|
||||
]
|
||||
},
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"P_all(99)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The moral is: give yourself at least about 100 tries to guess 19 cards right, and don't expect to *ever* guess 99 cards right.\n",
|
||||
"\n",
|
||||
"# Closed Form Formula?\n",
|
||||
"\n",
|
||||
"I would like to come up with a closed-form formula for `P_all(n)` as a function of $n$. I don't see how to define an *exact* formula, but I can see that:\n",
|
||||
"- When the up card is the very middle of the remaining cards, we have a 50% chance of guessing the next card right.\n",
|
||||
"- When the up card is the very highest or very lowest of the remaining cards, we have a 100% chance of guessing the next card right.\n",
|
||||
"- So on average (very roughly speaking), we should have about a 75% chance of guessing right on each card.\n",
|
||||
"- In a full deck we make $n - 2$ guesses (we don't have to guess the first card, and we will know the last card for sure).\n",
|
||||
"- Therefore, the chance of guessing all the cards right is roughly $0.75^{(n-2)}$\n",
|
||||
"\n",
|
||||
"If the exponential formula $0.75^{(n-2)}$ were correct, then it would be true that `P_all(n + 1) / P_all(n) = 0.75` for all `n`. Let's sample some values of `n` and see:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{2: 0.8333333333333334,\n",
|
||||
" 3: 0.8,\n",
|
||||
" 4: 0.775,\n",
|
||||
" 5: 0.7688172043010753,\n",
|
||||
" 6: 0.7587412587412588,\n",
|
||||
" 7: 0.7557603686635944,\n",
|
||||
" 8: 0.7500967866821525,\n",
|
||||
" 9: 0.7483290322580645,\n",
|
||||
" 19: 0.7343228066190575,\n",
|
||||
" 39: 0.7277139939871775,\n",
|
||||
" 59: 0.7255656153885536,\n",
|
||||
" 79: 0.7245013548920631,\n",
|
||||
" 99: 0.7238659386192225,\n",
|
||||
" 199: 0.7226021071682432,\n",
|
||||
" 299: 0.7221828896968574,\n",
|
||||
" 399: 0.7219736649122889}"
|
||||
]
|
||||
},
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"Ns = [*range(2, 10), *range(19, 99, 20), *range(99, 400, 100)]\n",
|
||||
"\n",
|
||||
"{n: P_all(n + 1) / P_all(n) \n",
|
||||
" for n in Ns}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We see that the ratio starts higher than 0.75 and becomes lower. That means that `P_all(n)` is not exactly an exponential function.\n",
|
||||
"\n",
|
||||
"How close is it? Let's draw a plot to see. We'll use a log scale, on which an exponential is a straight (blue) line. I use 0.725 as the base for the exponential, which nicely highlights that the `P_all(n)` data points (red) start out and end up on the blue exponential line, but rise slightly above it in the middle. Still the points are a very good fit to the line, considering that they span 56 orders of magnitude. I also show the line for `0.75 ** (n - 2)`; all the data points except the first few are well below that."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 648x648 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"base = 0.725\n",
|
||||
"\n",
|
||||
"X = range(2, 400, 4)\n",
|
||||
"P = [P_all(n) for n in X]\n",
|
||||
"E = [base ** (n - 2) for n in X]\n",
|
||||
"G = [0.75 ** (n - 2) for n in X]\n",
|
||||
"\n",
|
||||
"plt.figure(figsize=(9, 9))\n",
|
||||
"plt.yscale('log')\n",
|
||||
"plt.xlabel(\"Number of Cards, n\")\n",
|
||||
"plt.ylabel(\"Probability of Guessing All Right (log scale)\")\n",
|
||||
"plt.plot(X, G, 'g:', label='0.750 ** (n-2)')\n",
|
||||
"plt.plot(X, E, 'b-', lw=2, label=f'{base} ** (n-2)')\n",
|
||||
"plt.plot(X, P, 'r.', label='P_all(n)')\n",
|
||||
"plt.legend();"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Computational Complexity: O(n) Notation \n",
|
||||
"\n",
|
||||
"The brute force algorithm, with `guess_all(n)`, requires $O(n!)$ computation time because it considers every permutation of $n$ cards.\n",
|
||||
"\n",
|
||||
"In the efficient algorithm, `P_all(n)` makes $n$ calls to `P_all_given`, and each call to `P_all_given` considers $n$ down cards and makes recursive calls only when `guess_first_given(up, down1, n)` returns true. Some of those calls have already been computed and are cached. It seems complicated to do an exact analysis. \n",
|
||||
"\n",
|
||||
"However, if we assume that the majority of the work done by `P_all` is inside a call to `P_all_given`, and note that both of `P_all_given`'s arguments are restricted to $n$ possible values, then we can conclude that only $O(n^2)$ unique calls can be made. Repeated calls will fetch results from the cache in constant time. Therefore `P_all` is $O(n^2)$.\n",
|
||||
"\n",
|
||||
"Experimental evidence shows that for every value of `n` tested so far, a call to `P_all(n)` does *exactly* $n \\times (n + 1) / 2 - 1$ unique calls to `P_all_given`. We can determine this by looking at the number of elements in the function's cache.\n",
|
||||
"\n",
|
||||
"In the table below, `n` is the number of cards, `s` is the size of the cache, and we see that `s` is always equal to $c = n \\times (n + 1) / 2 - 1$. \n",
|
||||
"\n",
|
||||
"The last four columns of the table don't deal with computational complexity. They give the value of `P_all(n)`, the ratio `P_all(n + 1) / P_all(n)`, the approximation `base ** (n - 2)`, and the percentage difference between the true value and the approximation."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" n | s | s==c ||P_all(n) |P(n+1)/P(n)|base**(n-2)| Δ(P, 0.725**(n-2))\n",
|
||||
"—————|————————|——————||—————————|———————————|———————————|——————————————————\n",
|
||||
" 2 | 2 | True || 1.0e+00 | 0.8333 | 1.0e+00 | 0%\n",
|
||||
" 3 | 5 | True || 8.3e-01 | 0.8000 | 7.2e-01 | 13%\n",
|
||||
" 4 | 9 | True || 6.7e-01 | 0.7750 | 5.3e-01 | 21%\n",
|
||||
" 5 | 14 | True || 5.2e-01 | 0.7688 | 3.8e-01 | 26%\n",
|
||||
" 6 | 20 | True || 4.0e-01 | 0.7587 | 2.8e-01 | 30%\n",
|
||||
" 7 | 27 | True || 3.0e-01 | 0.7558 | 2.0e-01 | 34%\n",
|
||||
" 8 | 35 | True || 2.3e-01 | 0.7501 | 1.5e-01 | 36%\n",
|
||||
" 9 | 44 | True || 1.7e-01 | 0.7483 | 1.1e-01 | 38%\n",
|
||||
" 19 | 189 | True || 8.4e-03 | 0.7343 | 4.2e-03 | 50%\n",
|
||||
" 39 | 779 | True || 1.6e-05 | 0.7277 | 6.8e-06 | 57%\n",
|
||||
" 59 | 1,769 | True || 2.6e-08 | 0.7256 | 1.1e-08 | 58%\n",
|
||||
" 79 | 3,159 | True || 4.2e-11 | 0.7245 | 1.8e-11 | 58%\n",
|
||||
" 99 | 4,949 | True || 6.7e-14 | 0.7239 | 2.8e-14 | 57%\n",
|
||||
" 199 | 19,899 | True || 5.5e-28 | 0.7226 | 3.1e-28 | 45%\n",
|
||||
" 299 | 44,849 | True || 4.2e-42 | 0.7222 | 3.3e-42 | 20%\n",
|
||||
" 399 | 79,799 | True || 3.0e-56 | 0.7220 | 3.6e-56 | -19%\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"P_all_given.cache_clear()\n",
|
||||
"\n",
|
||||
"print(f' n | s | s==c ||P_all(n) |P(n+1)/P(n)|base**(n-2)| Δ(P, {base}**(n-2))')\n",
|
||||
"print(f'—————|————————|——————||—————————|———————————|———————————|——————————————————')\n",
|
||||
"for n in Ns:\n",
|
||||
" p = P_all(n)\n",
|
||||
" s = P_all_given.cache_info().currsize\n",
|
||||
" c = n * (n + 1) / 2 - 1\n",
|
||||
" e = base ** (n - 2)\n",
|
||||
" r = P_all(n + 1) / P_all(n)\n",
|
||||
" Δ = (p - e) / p\n",
|
||||
" print(f'{n:4d} | {s:6,d} | {s == c} ||{p:8.1e} | {r:6.4f} | {e:8.1e} | {Δ:3.0%}')"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"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.7.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
portmanteautographicnessayistsarismskydivingenuouslyesterdaysideswipingrassesseekingeditressestetsonshipshapelessnessayerstwhilesseningestedhorseshoestringsideslippingraftinglesseesawingedlyricallylsyllabicateressessilentnessaysheeningratiatinglersatzestfulnessestercesareanschlussresistentorsionallyonnaiseminarsenidesolatediumsquirtingeditoriallysergichoroustaboutstationstagersiltiercelsiusurpingotsaristsaritzastrodometerselysiantitankersparestatingeingrowingspreadsheetsarsaparillasciviouslylyceesplanadesteeminglersensibleruptionshorelessoningrainingulfsongfullyresubmissionslaughtsimmeshinglersmellingulairedalesmensingularitiestradiologiesteemedicamentsktskedgeddyingslatypicallycanthropyromaniacalcsparkpluguglyceroselysiumpireddenedgewaysidesteppingstoneselfhoodscarvestingsightersestinestimablessedlyricizingiestrangementsunamissydneyelettinglierectilerspurriergosterolsuperhumanizershippageboysenberriesquiringtailspinsterhoodwinkingingivalvelessoneditorializingieroticisternalizarinequableedingsubcausesquicentenniallynchingshopliftsarinassimilablearythmicroprogrammedicatestingspeedupsettersenessayedgargantuanthropomorphismschmalzesteddieselsynswingiestoppedagogsadlymphoidiumbrellassoedipalsiestashingleddieditorializationscreamediuseablyristsktskinglessorswaggeringnecksmoochymenealdersaturantsetsesquipedalianastomosestinasmuchnessayingsourishrillingeriespalieredefinitionsnowdropsieducationsilvanswerabilityrantsardomstrickenlynchedarknessencesarianklebonesettermitessellatingliestrogensnarlinglypticsensatediouslynessesquicentennialsoyashmachinatoriinvolutionskeanswersatiatingledificationisinglassblowerschuberthingelessonsemirurallyistsunamicablenessentiallynxescrowedgessoesophagussieductorsoscilloscopicallyrismseppukusefullyricismsticklerspermatozoonstripiestimatorsoestrummedicablynchestnutshellsubdirectorshipsidearmsfulminantineoplasticizedsucceedspatsycophancyansweringwormsubspeciescarolesbianismockedgingeredemptoryxescallopeddlerysipelasticizingyroscopicallysinsinuatediousnesserowdyismsailfishesitancycloidalmatianswerersparkersforzatossershogunstocksoftballspicesuraerifiedematadorsallyinglyceraldehydesolatenesscalyclepedometerservantshipmateshipwreckingshipshotgunnedgewisecrackingsidetrackstrikebreakersuffusingablessingsongstressesamestizostereotypicalitympanamanianswimmythologistsarevnastinessentiallopathsubbassettinglingamsterdamnabilitiesophagalenasturtiumslewedgedinburghersheydaystarshippablepharitissuedespisestrangingeringlikenessespiescapedestaleditherythromycinematographerselflesslybootstrapshootingsmidgensuedeductingsacbutsustenantingestswinishoalyssumsuperficiallycanthropiestuarythmiasmallergologyratorsionsocietiescapementsniffershopkeepersianseedpodsubprovincestuouslyerbasterseedlessensualnessavoysteringboltskindivingrowthsystemizingingivitissuescaladingsloshinglinguisticslowwitteddynamitedgilymphomassivelyrebirdseedsmangierasersemirefinediteditorializestfullycanthroperiespousalsifyarereadsorbablemishesitatestieringletstreptococcalcinationhoodwinkstandstillycheesierrantrieschewerscandaleditorshipstersyphonedisonorantsixteenthskunkinglycogenickelodeonsodomiescapeescarsonouselessnessomnambulatoriescargotserfhoodsleepinesspriestlyricistsoftheadshipkeepergnestlersloshiestrogenicitywidenerskeiningathersiticalvarylsalvagingersnapshotspursympathizestiercesiumschismaticsubcutaneouslysinewscastsabrasivespersonalswervedanticancerouslysingaporeganosingsimplifierswitchersmallestuariespyingressionosphericityfiedifiedemasculinizingersanatariumpireshippedaledginessilverinesscourginglycerinsersteepenseesawedgieskimosquitoeshoescarpingsoporificslobberylsunbathersiberiansemirespectabilityrannymphosphorickshashlikingseducibleakerschusseschewsunwardshipsychosociallymphocytestilymphsandblastshrivelleditorialistingspriggerythematozoarlocksmithservitudepletestifiestashesitanciescotingenuousnessubstratumswizzlersolubilizeditorializediblessedestroyablenniescortstockierodibleachingersequinsiespadrillesionspicedarsenicalciningathereditykestrelsesquiresidualslipslopsidedlyricizestylatencieschewinglesseneducatorshipownershipseudophilosophicallymphocyticktockspecklingeringlycosidiceyloneseizestinginglycerylsweepstakescroweevilsimonistsinuosityrannizestsequesteringleadershiptolemaicinessestoppagesizeablethersaltbushesitatorturedlymphaticallyratedifiescaladespatchesapeakednesstoogesticulationseltzersummarizestiestoppelsewherebyproductspermarylandersongbooksellerscrapbookshelffulsomelynchersojournmenthermonuclearwaterspoutswumlautshantisepticizingedifierscroungestatedlyceumstewarthogsheadsmanifestoestrusescalatorsuckeredrafteducativentriloquistseedmanifestabledentatestimonialsheilactobacillustratestinesshopliftingenuityphusescapadeservedlyriformalisticallyricstumblinglycogensheathedgierasurestartingratitudeanerythrocyteslashersullennesspunksticklebackbitersmarteriographysiotherapyreneestoniansweetingskimpedancestrychninizationisestablishmentshutteringdovestlessnobberylinelesshebangstromslilyricizeditorializersightseeingshovelheadgLine truncated
|
||||
+3
-3
@@ -257,7 +257,7 @@
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The main point of the `Panama` class is to allow the `.search()` method to search for a long palindrome. The [overall strategy](http://norvig.com/pal-alg.html#v3) is explained elsewhere. The search\n",
|
||||
"adds a lett1er at a time to both left and right side, backtracking when necessary. The state of the search is represented by the four fields `.left, .L, .R, .right`, for example:\n",
|
||||
"adds a letter at a time to both left and right side, backtracking when necessary. The state of the search is represented by the four fields `.left, .L, .R, .right`, for example:\n",
|
||||
" \n",
|
||||
" A man, a plan, a cam, a yak, a yam, a canal, Panama\n",
|
||||
" \n",
|
||||
@@ -272,7 +272,7 @@
|
||||
"\n",
|
||||
" cat(left + [L]) == cat([R] + right)[::-1]\n",
|
||||
" \n",
|
||||
"The `search` method would be more straightforward if we could write it is a recursive function. But Python does not perform well when recursing to 100 thousand levels deep. So instead I manually manage a *stack* of *commands* that tell the search what to do and to undo. A command can be a `DoCommand`, which consists of a list of letters describing all the possible actions we could take at this point. Actions include letters that could be added (only the letters that make a legitimate prefix of a known phrase in `.L` and a legitimate suffix of a known phrase in `.R`). Actions can also include the character `','`, which is used to dignal that `.L` is a complete word and should be moved ontot he `.left` list, or `';'` to signal the same for `.R/.right`. A command can also be a `UndoCommand`, which consists of a single character; one that was previously chosen to be done. So the list of characters in a `DoCommand` constitutes a choice point; we first choose one, and continue deeper from there, but we put on the command stack an `UndoCommand` to reverse the effects of the action, and put back the remaining characters to try instead, if the first character doesn't work out. Note that we pop characters off the end of a `DoCommand`, so the last character is the first one tried.\n",
|
||||
"The `search` method would be more straightforward if we could write it is a recursive function. But Python does not perform well when recursing to 100 thousand levels deep. So instead I manually manage a *stack* of *commands* that tell the search what to do and to undo. A command can be a `DoCommand`, which consists of a list of letters describing all the possible actions we could take at this point. Actions include letters that could be added (only the letters that make a legitimate prefix of a known phrase in `.L` and a legitimate suffix of a known phrase in `.R`). Actions can also include the character `','`, which is used to signal that `.L` is a complete word and should be moved onto the `.left` list, or `';'` to signal the same for `.R/.right`. A command can also be a `UndoCommand`, which consists of a single character; one that was previously chosen to be done. So the list of characters in a `DoCommand` constitutes a choice point; we first choose one, and continue deeper from there, but we put on the command stack an `UndoCommand` to reverse the effects of the action, and put back the remaining characters to try instead, if the first character doesn't work out. Note that we pop characters off the end of a `DoCommand`, so the last character is the first one tried.\n",
|
||||
"\n",
|
||||
"Let's see how it works:"
|
||||
]
|
||||
@@ -452,7 +452,7 @@
|
||||
"- I'm not sure when to end a word and when to try to continue to a longer word; experimentation here would be useful.\n",
|
||||
"- The program is deterministic, and thus always finds the same plaindrome. that's boring; can some randomness be introduced?\n",
|
||||
"- Can we make more interesting phrases? Include determiners other than \"a\" and \"an\"; include adjectives, etc.\n",
|
||||
"- The counts of prefixes and suffixes include all the phrases in the dictionary. But we are not allowed to repeat a phrase, so can we modify the code to subtract one from the counts every time a phrase is used (and add onr back in when we backtrack over the phrase)?\n",
|
||||
"- The counts of prefixes and suffixes include all the phrases in the dictionary. But we are not allowed to repeat a phrase, so can we modify the code to subtract one from the counts every time a phrase is used (and add one back in when we backtrack over the phrase)?\n",
|
||||
"\n",
|
||||
"Perhaps you can find other areas to explore.\n",
|
||||
"\n",
|
||||
|
||||
+1956
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,139 @@
|
||||
# Generate a portmantout word
|
||||
# Peter Norvig
|
||||
# See https://github.com/norvig/pytudes/blob/master/ipynb/Portmantout.ipynb
|
||||
|
||||
from collections import defaultdict, Counter
|
||||
from typing import List, Tuple, Set, Dict, Any
|
||||
|
||||
Word = str
|
||||
class Wordset(set): """A set of words."""
|
||||
Step = Tuple[int, str] # An (overlap, word) pair.
|
||||
OVERLAP, WORD = 0, 1 # Indexes of the two parts of a Step.
|
||||
Path = List[Step] # A list of steps.
|
||||
Bridge = (int, Step,...) # An excess letter count and step(s), e.g. (1, (2, 'arrow')).
|
||||
EXCESS, STEPS = 0, slice(1, None) # Indexes of the two parts of a bridge.
|
||||
|
||||
W = Wordset(open('wordlist.asc').read().split())
|
||||
|
||||
def portman(P: Path) -> Word:
|
||||
"""Compute the portmantout string S from the path P."""
|
||||
return ''.join(word[overlap:] for (overlap, word) in P)
|
||||
|
||||
def natalie(W: Wordset, start=None) -> Path:
|
||||
"""Return a portmantout path containing all words in W."""
|
||||
precompute(W)
|
||||
word = start or first(W.unused)
|
||||
used(W, word)
|
||||
P = [(0, word)]
|
||||
while W.unused:
|
||||
steps = unused_step(W, word) or bridging_steps(W, word)
|
||||
for (overlap, word) in steps:
|
||||
P.append((overlap, word))
|
||||
used(W, word)
|
||||
return P
|
||||
|
||||
def unused_step(W: Wordset, prev_word: Word) -> List[Step]:
|
||||
"""Return [(overlap, unused_word)] or []."""
|
||||
for suf in suffixes(prev_word):
|
||||
for unused_word in W.startswith.get(suf, ()):
|
||||
overlap = len(suf)
|
||||
return [(overlap, unused_word)]
|
||||
return []
|
||||
|
||||
def bridging_steps(W: Wordset, prev_word: Word) -> List[Step]:
|
||||
"""The steps from the shortest bridge that bridges
|
||||
from a suffix of prev_word to a prefix of an unused word."""
|
||||
bridge = min(W.bridges[suf][pre]
|
||||
for suf in suffixes(prev_word) if suf in W.bridges
|
||||
for pre in W.bridges[suf] if W.startswith[pre])
|
||||
return bridge[STEPS]
|
||||
|
||||
def precompute(W):
|
||||
"""Precompute and cache data structures for W. The .subwords and .bridges
|
||||
data structures are static and only need to be computed once; .unused and
|
||||
.startswith are dynamic and must be recomputed on each call to `natalie`."""
|
||||
if not hasattr(W, 'subwords') or not hasattr(W, 'bridges'):
|
||||
W.subwords = subwords(W)
|
||||
W.bridges = build_bridges(W)
|
||||
W.unused = W - W.subwords
|
||||
W.startswith = compute_startswith(W.unused)
|
||||
|
||||
def used(W, word):
|
||||
"""Remove word from `W.unused` and, for each prefix, from `W.startswith[pre]`."""
|
||||
assert word in W, f'used "{word}", which is not in the word set'
|
||||
if word in W.unused:
|
||||
W.unused.remove(word)
|
||||
for pre in prefixes(word):
|
||||
W.startswith[pre].remove(word)
|
||||
if not W.startswith[pre]:
|
||||
del W.startswith[pre]
|
||||
|
||||
def first(iterable, default=None): return next(iter(iterable), default)
|
||||
|
||||
def multimap(pairs) -> Dict[Any, set]:
|
||||
"""Given (key, val) pairs, make a dict of {key: {val,...}}."""
|
||||
result = defaultdict(set)
|
||||
for key, val in pairs:
|
||||
result[key].add(val)
|
||||
return result
|
||||
|
||||
def compute_startswith(words) -> Dict[str, Set[Word]]:
|
||||
"""A dict mapping a prefix to all the words it starts:
|
||||
{'somet': {'something', 'sometimes'},...}."""
|
||||
return multimap((pre, w) for w in words for pre in prefixes(w))
|
||||
|
||||
def subwords(W: Wordset) -> Set[str]:
|
||||
"""All the words in W that are subparts of some other word."""
|
||||
return {subword for w in W for subword in subparts(w) & W}
|
||||
|
||||
def suffixes(word) -> List[str]:
|
||||
"""All non-empty proper suffixes of word, longest first."""
|
||||
return [word[i:] for i in range(1, len(word))]
|
||||
|
||||
def prefixes(word) -> List[str]:
|
||||
"""All non-empty proper prefixes of word."""
|
||||
return [word[:i] for i in range(1, len(word))]
|
||||
|
||||
def subparts(word) -> Set[str]:
|
||||
"""All non-empty proper substrings of word"""
|
||||
return {word[i:j]
|
||||
for i in range(len(word))
|
||||
for j in range(i + 1, len(word) + (i > 0))}
|
||||
|
||||
def splits(word) -> List[Tuple[int, str, str]]:
|
||||
"""A sequence of (excess, pre, suf) tuples."""
|
||||
return [(excess, word[:i], word[i+excess:])
|
||||
for excess in range(len(word) - 1)
|
||||
for i in range(1, len(word) - excess)]
|
||||
|
||||
def try_bridge(bridges, pre, suf, excess, word, step2=None):
|
||||
"""Store a new bridge if it has less excess than the previous bridges[pre][suf]."""
|
||||
if suf not in bridges[pre] or excess < bridges[pre][suf][EXCESS]:
|
||||
bridge = (excess, (len(pre), word))
|
||||
if step2: bridge += (step2,)
|
||||
bridges[pre][suf] = bridge
|
||||
|
||||
def build_bridges(W: Wordset, maxlen=5, end='qujvz'):
|
||||
"""A table of bridges[pre][suf] == (excess, (overlap, word)), e.g.
|
||||
bridges['ar']['c'] == (0, (2, 'arc'))."""
|
||||
bridges = defaultdict(dict)
|
||||
shortwords = [w for w in W if len(w) <= maxlen + (w[-1] in end)]
|
||||
shortstartswith = compute_startswith(shortwords)
|
||||
# One-word bridges
|
||||
for word in shortwords:
|
||||
for excess, pre, suf, in splits(word):
|
||||
try_bridge(bridges, pre, suf, excess, word)
|
||||
# Two-word bridges
|
||||
for word1 in shortwords:
|
||||
for suf in suffixes(word1):
|
||||
for word2 in shortstartswith[suf]:
|
||||
excess = len(word1) + len(word2) - len(suf) - 2
|
||||
A, B = word1[0], word2[-1]
|
||||
if A != B:
|
||||
step2 = (len(suf), word2)
|
||||
try_bridge(bridges, A, B, excess, word1, step2)
|
||||
return bridges
|
||||
|
||||
if __name__ == "__main__":
|
||||
W = Wordset(open('wordlist.asc').read().split())
|
||||
print(portman(natalie(W)))
|
||||
+378
-297
@@ -8,30 +8,20 @@
|
||||
"\n",
|
||||
"# `xkcd` Name Dominoes\n",
|
||||
"\n",
|
||||
"The March 21, 2018 `xkcd` comic (#1970) was [Name Dominoes](https://xkcd.com/1970/): domino tiles laid out as if in a game, but the tiles contain names of famous people rather than numbers. \n",
|
||||
"In [dominoes](https://en.wikipedia.org/wiki/Dominoes) each tile has two halves, and a tile can be legally placed only if one half is adjacent to another tile half with the same number/name, and neither half is adjacent to a tile half with a different number/name. (Exception: the very first tile can be placed anywhere.) \n",
|
||||
"\n",
|
||||
"\n",
|
||||
"I will write a function to lay out tiles in a random, legal array. First, the key data structures:\n",
|
||||
"The March 21, 2018 `xkcd` comic (#1970) was **[Name Dominoes](https://xkcd.com/1970/)**: tiles laid out as if in a game of dominoes, but the tiles contain names of famous people. \n",
|
||||
"In regular [dominoes](https://en.wikipedia.org/wiki/Dominoes) a tile consists of two squares, each with a number of spots from 0 to 6. In xkcd name dominoes, the tiles can be one, two, or three squares in length, and each square contains a name (first, middle, or last). In both versions, a tile can be placed if it is adjacent to another tile on the board, and if no two adjacent squares contain different numbers/names.\n",
|
||||
"I will write a function, `place_tiles`, to lay out name dominoes according to these rules. \n",
|
||||
"\n",
|
||||
"- **`Tile`**: a tile is a 2-element tuple of names, like `('TIM', 'COOK')`.\n",
|
||||
"The tile `('FRANK LLOYD', 'WRIGHT')` has a space in the first name.\n",
|
||||
"- **`Name`**: a name (first name or last name) is a string.\n",
|
||||
"- **`Board(width, height)`**: a `Board` represents\n",
|
||||
"a width × height array of squares, initially `empty`, but when we put a tile on the board,\n",
|
||||
"the first name covers one location and the last name covers an adjacent location, e.g. `board[0, 0], board[0, 1] = ('TIM', 'COOK')`. Implemented as a subtype of `dict` that keeps track of `width` and `height`.\n",
|
||||
"- **`Location`**: a location is an `(x, y)` pair of coordinates for a square on the board.\n",
|
||||
"# Key Data Structures:\n",
|
||||
"\n",
|
||||
"Now I need a strategy to fill the board with tiles. I will randomly place tiles one at a time, and to make things easier I will *not* consider removing a tile from the board and backtracking. Some more concepts and functions:\n",
|
||||
"\n",
|
||||
"- **`frontier`**: I'll maintain a *frontier*, a set of locations that are adjacent to tiles on the board, and thus are candidates for placing new tiles.\n",
|
||||
"- **`dominoes(tiles)`**: makes a board and places tiles on it, one at a time, until no more can be placed. Chooses a random tile for the first tile, then repeatedly calls `try_one` to legally place an additional tile, stopping when either there is no `frontier` left (meaning no place to legally place a tile) or no `tiles` left to place.\n",
|
||||
"- **`try_one(tiles, board, frontier)`**: pop a location off the frontier, and try to find some tile that can legally put one of its halves there; when found, `put` the tile there, and remove it from `tiles`.\n",
|
||||
"- **`legal(name, loc, board)`**: a name can be placed if the location is empty, and there are no conflicts with any neighboring location.\n",
|
||||
"- **`neighbors(loc, board)`**: returns the (up to 4) neighbors of a location that are on the board.\n",
|
||||
"- **`put_tile(board, loc0, loc1, tile, frontier)`**: places a tile on the board across `loc0` and `loc1`; update the `frontier` to say that the just-covered locations are no longer in the frontier, but the empty neighbors of the tile are.\n",
|
||||
"- **`shuffle(items)`**: used to randomize lists; calls `random.shuffle` and returns the result.\n",
|
||||
"\n",
|
||||
"# The Code"
|
||||
"- **`Tile`**: a tile is a tuple of names, like `('ISSAC', 'NEWTON')`\n",
|
||||
"or `('FRANK', 'LLOYD', 'WRIGHT')` or `('DRAKE',)`.\n",
|
||||
"- **`Name`**: a name (first, middle, or last) is a string, like `'FRANK'`.\n",
|
||||
"- **`Box`**: a structure with a tile, the square that the tile's first name goes on, and the direction in which to move to place subsequent names.\n",
|
||||
"- **`Point`**: an `(x, y)` pair denoting the coordinates of a square on the board. Can also be used for **directions**: `(0, 1)` is the direction that goes 0 in the x axis and 1 in the y axis; also known as \"up\" or \"90 degrees\".\n",
|
||||
"- **`Board`**: a [width × height] grid of squares, where each square can be filled with a name. `Board` is implemented as a subclass of `dict` of the form `{(0, 0): 'ISSAC', (0, 1): 'NEWTON', ...}`, but with additional attributes to keep track of the width and height, and the boxes placed so far. If an `(x, y)` square is not in the dict, it is `empty` by default, unless the `(x, y)` coordinates are out of bounds; then it is `off` the board."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -40,102 +30,63 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%matplotlib inline\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import random\n",
|
||||
"\n",
|
||||
"empty = None # An empty square\n",
|
||||
"\n",
|
||||
"class Board(dict):\n",
|
||||
" \"A mapping from location to name.\"\n",
|
||||
" def __init__(self, width, height): self.width, self.height = width, height\n",
|
||||
" def __missing__(self, loc): return empty\n",
|
||||
"\n",
|
||||
"def dominoes(tiles, width=16, height=24):\n",
|
||||
" \"Place as many tiles on board as possible (legally and randomly).\"\n",
|
||||
" tiles = shuffle(list(tiles))\n",
|
||||
" board = Board(width, height)\n",
|
||||
" frontier = set()\n",
|
||||
" m = min(width, height) // 2\n",
|
||||
" put_tile(board, (m, m), (m, m + 1), tiles.pop(), frontier) # Place first tile\n",
|
||||
" while tiles and frontier:\n",
|
||||
" try_one(tiles, board, frontier)\n",
|
||||
" return board\n",
|
||||
" \n",
|
||||
"def try_one(tiles, board, frontier):\n",
|
||||
" \"Pop a frontier location, and try to place a tile on that location.\"\n",
|
||||
" loc0 = frontier.pop()\n",
|
||||
" for tile in shuffle(tiles):\n",
|
||||
" for (name0, name1) in [tile, tile[::-1]]:\n",
|
||||
" if legal(name0, loc0, board):\n",
|
||||
" for loc1 in shuffle(neighbors(loc0, board)):\n",
|
||||
" if legal(name1, loc1, board):\n",
|
||||
" put_tile(board, loc0, loc1, (name0, name1), frontier)\n",
|
||||
" tiles.remove(tile)\n",
|
||||
" return tile\n",
|
||||
" \n",
|
||||
"def legal(name, loc, board):\n",
|
||||
" \"Is it legal to place this name on this location on board?\"\n",
|
||||
" return (board[loc] is empty and\n",
|
||||
" all(board[nbr] is empty or board[nbr] == name\n",
|
||||
" for nbr in neighbors(loc, board)))\n",
|
||||
"\n",
|
||||
"def neighbors(loc, board):\n",
|
||||
" \"Neighbors of this location on the board.\"\n",
|
||||
" x, y = loc\n",
|
||||
" return [(x+dx, y+dy) for (dx, dy) in ((0, 1), (1, 0), (0, -1), (-1, 0))\n",
|
||||
" if 0 <= x+dx < board.width and 0 <= y+dy < board.height]\n",
|
||||
"\n",
|
||||
"def put_tile(board, loc0, loc1, tile, frontier): \n",
|
||||
" \"Place the tile across the two locations, and update frontier.\"\n",
|
||||
" board[loc0], board[loc1] = tile\n",
|
||||
" frontier -= {loc0, loc1}\n",
|
||||
" frontier |= {loc for loc in neighbors(loc0, board) + neighbors(loc1, board)\n",
|
||||
" if board[loc] is empty}\n",
|
||||
" \n",
|
||||
"def shuffle(items): random.shuffle(items); return items"
|
||||
"from collections import namedtuple"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{(0, 0): 'RY',\n",
|
||||
" (1, 0): 'KE',\n",
|
||||
" (1, 1): 'KE',\n",
|
||||
" (2, 1): 'JO',\n",
|
||||
" (2, 2): 'JO',\n",
|
||||
" (3, 2): 'GR',\n",
|
||||
" (3, 3): 'GR',\n",
|
||||
" (3, 4): 'KE',\n",
|
||||
" (4, 4): 'KE',\n",
|
||||
" (4, 5): 'JA'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"tiles8 = [('BO', 'JA'), ('JA', 'PO'), ('JA', 'RY'), ('RY', 'KE'), \n",
|
||||
" ('GR', 'KE'), ('GR', 'JO'), ('JA', 'KE'), ('KE', 'JO')]\n",
|
||||
"Tile = tuple # e.g., ('ISSAC', 'NEWTON')\n",
|
||||
"Name = str # e.g., 'ISSAC'\n",
|
||||
"Box = namedtuple('Box', 'start, dir, tile')\n",
|
||||
"\n",
|
||||
"dominoes(tiles8, 6, 6)"
|
||||
"Point = tuple\n",
|
||||
"def X(point) -> int: \"The x coordinate of a point\"; return point[0]\n",
|
||||
"def Y(point) -> int: \"The x coordinate of a point\"; return point[1]\n",
|
||||
"\n",
|
||||
"directions = {(1, 0): 0, (0, 1): 90, (-1, 0): 180, (0, -1): 270}\n",
|
||||
"\n",
|
||||
"off = '.' # A square that is off the edge of the board\n",
|
||||
"empty = '' # An empty square (does not have a tile covering it)\n",
|
||||
"\n",
|
||||
"class Board(dict):\n",
|
||||
" \"\"\"A mapping from (x, y) squares to names; for a width x height board.\"\"\"\n",
|
||||
" def __init__(self, width, height): \n",
|
||||
" \"\"\"Keep track of the width and height, and of boxes that have been placed.\"\"\"\n",
|
||||
" self.width = width \n",
|
||||
" self.height = height\n",
|
||||
" self.boxes = []\n",
|
||||
" \n",
|
||||
" def __missing__(self, square: Point) -> str: \n",
|
||||
" \"\"\"The contents of a filled square is a name; a missing square is empty or off.\"\"\"\n",
|
||||
" return empty if (0 <= X(square) < self.width and 0 <= Y(square) < self.height) else off"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Pretty Output\n",
|
||||
"# Algorithm to Fill the Board\n",
|
||||
"\n",
|
||||
"Technically, this is a legal solution, but there are two problems with it: One, it is not visual. Two, it doesn't say where each tile is: when three names come together, which of the outside names goes with the middle name? To fix those two problems I will:\n",
|
||||
"Now I need a strategy to fill the board with tiles. I will place tiles one at a time, and to make things easier I will *never* consider removing a tile from the board and backtracking. The major functions are:\n",
|
||||
"\n",
|
||||
"- Define `plot_board(board)` to use `matplotlib` to plot the board, the names, and the tiles.\n",
|
||||
"- Modify the `Board` class and the `put` function so that the board maintains a list of `boxes` that surround each two-location rectangle that a tile occupies: `[loc0, loc1]`. The constant ϵ is the distance between two adjacent tiles; we want them to not-quite touch (as in the xkcd comic)."
|
||||
"\n",
|
||||
"- **`place_tiles(tiles)`**: The top-level strategy. Makes a board and places tiles on it, one at a time, until no more can be placed. Chooses a random tile for the first tile, then repeatedly calls `try_one_square` to legally place an additional tile on a square from the frontier, stopping when either there is no `frontier` left or no `tiles` left.\n",
|
||||
"- **`frontier`**: I'll maintain a *frontier*, a set of `(x, y)` squares that are adjacent to tiles on the board, and thus are candidates for placing new tiles.\n",
|
||||
"- **`try_one_square(tiles, board, frontier)`**: pop a square off the frontier, and try to find some tile that can legally put one of its names there; when found, `put_tile` the tile there, and remove it from `tiles`.\n",
|
||||
"- **`can_put_tile(tile, board, start, dir)`**: Can we put this tile on the board at this start square, going in this direction?\n",
|
||||
"- **`can_put_name(name, board, square)`**: Can this one name (first, middle, or last) be placed on this square?\n",
|
||||
"- **`allowed(name, nbr_contents)`**: Is this name allowed to be placed next to this neighboring square's contents?\n",
|
||||
"- **`neighbors(square, board)`**: returns the four neighbors of a square.\n",
|
||||
"- **`put_tile(tile, board, start, dir, tiles, frontier)`**: places a tile on the board starting at `start` and going in direction `dir`; update the `frontier` to say that the just-covered squares are not in the frontier, but the empty neighbors of new tiles are. Remove the tile from `tiles`.\n",
|
||||
"- **`move(start, steps, dir)`**: Returm the square that results from starting at `start` and going `steps` in \n",
|
||||
"direction `dir`."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -144,65 +95,220 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%matplotlib inline\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"def place_tiles(tiles, width=16, height=35) -> Board:\n",
|
||||
" \"\"\"Place as many tiles on board as possible.\"\"\"\n",
|
||||
" tiles = list(tiles); random.shuffle(tiles)\n",
|
||||
" board = Board(width, height)\n",
|
||||
" frontier = set()\n",
|
||||
" put_tile(tiles[0], board, (width//2, height//2), (0, 1), tiles, frontier)\n",
|
||||
" while tiles and frontier:\n",
|
||||
" try_one_square(tiles, board, frontier) \n",
|
||||
" return board\n",
|
||||
"\n",
|
||||
"ϵ = 0.06 # A small amount; the space between adjacent lines\n",
|
||||
"def try_one_square(tiles, board, frontier):\n",
|
||||
" \"\"\"Pop a frontier square, and try to place a tile on that square.\"\"\"\n",
|
||||
" square = frontier.pop()\n",
|
||||
" for tile in tiles:\n",
|
||||
" # The offset is used to shift the tile along `direction`\n",
|
||||
" for offset in range(len(tile)):\n",
|
||||
" for dir in directions:\n",
|
||||
" start = move(square, -offset, dir)\n",
|
||||
" if can_put_tile(tile, board, start, dir):\n",
|
||||
" return put_tile(tile, board, start, dir, tiles, frontier)\n",
|
||||
" \n",
|
||||
"def can_put_tile(tile, board, start, dir) -> bool:\n",
|
||||
" \"\"\"Is it legal to put this tile on this square/direction on board?\"\"\"\n",
|
||||
" return all(can_put_name(name, board, move(start, i, dir))\n",
|
||||
" for i, name in enumerate(tile))\n",
|
||||
"\n",
|
||||
"class Board(dict):\n",
|
||||
" \"A mapping from location to name.\"\n",
|
||||
" def __init__(self, width=16, height=24): \n",
|
||||
" self.width, self.height, self.boxes = width, height, []\n",
|
||||
" def __missing__(self, loc): return empty\n",
|
||||
" \n",
|
||||
"def plot_board(board):\n",
|
||||
" \"Plot the box and name for every tile, plus a big box around the board.\"\n",
|
||||
" plt.figure(figsize=(board.width, board.height))\n",
|
||||
" plt.subplots_adjust(left=ϵ, right=1-ϵ, top=1-ϵ, bottom=ϵ)\n",
|
||||
" plt.axis('off') \n",
|
||||
" plt.axis('equal')\n",
|
||||
" for box in board.boxes:\n",
|
||||
" plot_box(box)\n",
|
||||
" plot_box([(-2*ϵ, -2*ϵ), (board.width - 1 + 2*ϵ, board.height - 1 + 2*ϵ)])\n",
|
||||
" for (x, y) in board:\n",
|
||||
" plt.text(x + 0.5, y + 0.5, board[x, y], \n",
|
||||
" va='center', ha='center', fontsize=8)\n",
|
||||
"def can_put_name(name, board, square) -> bool:\n",
|
||||
" return (board[square] is empty \n",
|
||||
" and all(allowed(name, board[nbr])\n",
|
||||
" for nbr in neighbors(square, board)))\n",
|
||||
"\n",
|
||||
"def plot_box(box):\n",
|
||||
" \"Plot a box, which is a [loc0, loc1] pair.\"\n",
|
||||
" Xs, Ys = {loc[0] for loc in box}, {loc[1] for loc in box}\n",
|
||||
" x0, x1 = min(Xs), max(Xs) + 1 - ϵ\n",
|
||||
" y0, y1 = min(Ys), max(Ys) + 1 - ϵ\n",
|
||||
" plt.plot([x0, x1, x1, x0, x0], \n",
|
||||
" [y0, y0, y1, y1, y0], 'k-')\n",
|
||||
"def allowed(name, nbr_contents) -> bool: return nbr_contents in (name, empty, off)\n",
|
||||
"\n",
|
||||
"def put_tile(board, loc0, loc1, tile, frontier): \n",
|
||||
" \"Place the tile across the two locations, and update frontier and boxes.\"\n",
|
||||
" board[loc0], board[loc1] = tile\n",
|
||||
" frontier -= {loc0, loc1}\n",
|
||||
" frontier |= {loc for loc in neighbors(loc0, board) + neighbors(loc1, board)\n",
|
||||
" if board[loc] is empty}\n",
|
||||
" board.boxes.append([loc0, loc1]) "
|
||||
"def neighbors(square, board) -> list:\n",
|
||||
" \"\"\"Neighbors of this square on the board.\"\"\"\n",
|
||||
" x, y = square\n",
|
||||
" return [(x+dx, y+dy) for (dx, dy) in directions]\n",
|
||||
"\n",
|
||||
"def put_tile(tile, board, start, dir, tiles, frontier): \n",
|
||||
" \"\"\"Place the tile on the board at the start square, going in the given direction. \n",
|
||||
" Update tiles and frontier.\"\"\"\n",
|
||||
" board.boxes.append(Box(start, dir, tile))\n",
|
||||
" tiles.remove(tile)\n",
|
||||
" for i, name in enumerate(tile):\n",
|
||||
" square = move(start, i, dir)\n",
|
||||
" board[square] = name\n",
|
||||
" frontier -= {square}\n",
|
||||
" frontier |= {L for L in neighbors(square, board) if board[L] is empty}\n",
|
||||
" \n",
|
||||
"def move(start: Point, steps: int, dir: Point) -> Point: \n",
|
||||
" \"\"\"Starting at `start` square, move a number of `steps` in direction `dir`.\"\"\"\n",
|
||||
" return (X(start) + steps * X(dir),\n",
|
||||
" Y(start) + steps * Y(dir))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Small Example\n",
|
||||
"\n",
|
||||
"Here I create a small example with 8 tiles, and try to fill a board. You can see the boxes that were placed, and the names that are in each square:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"tiles8 = [('JAMES', 'EARL', 'JONES'), ('QUINCY', 'JONES'), ('QUINCY', 'ADAMS'), ('JOHN', 'ADAMS'), \n",
|
||||
" ('JOHN', 'QUINCY', 'ADAMS'), ('HENRY', 'JAMES'), ('JOHN', 'HENRY'), ('EARL',)]\n",
|
||||
"\n",
|
||||
"board = place_tiles(tiles8, 7, 7)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAaAAAAGdCAYAAABU0qcqAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAEFdJREFUeJzt3X/I7nV9x/HXW06bISjnZLa0tQ0nk6g/GkjtCDqUapkQ5bJlev4Yts1ibP9sI4SonX8CSdpgk5BFM0a0qS0o4eT0aDtr0f4xGTKChWNrMhce2xDW+vHeH/d17O4sPfdt59zv+3vdjwfceJ/rluPrvu7L63l/P/cNV3V3AGCnnTU9AIC9SYAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMCIfdMDdpuqeirJ/ukdACvHu/vA9Igzobp7esOuUlXd3TW9AyBZ7+ckR3AAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEfumByxBVT2VZP/0jj3meHcfmB7x41roY+fZ+36h+5dsLR73W1XdPb1hV6mq7u461W2cWetyny/x89i8eYn7l2yvPf84ggNghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQI0pKqurKrDq/fvrKo7pjdtxWr341X1YFX9bVX9QlW9uKo+XlVHq+qBqvrF6Z3r6sTjpqp+rqruW30d/qqqXjK9bSuWvP+k/2evq6q7q+rJ1efwYFXdPL1xabwi6qyuqrOSvDzL+mbgru7+QFW9PsktSf4nyZHu/nRVvTTJZ6rql7v7u7Mz19qdSX6ju79eVQeT/HGSdw9v2o6l7u/V3luSvC/Jrd19aHjTYi3pSW9dXZHkaJJjqyf0JTjx6oznJfmvJAe7+9NJ0t3/meSBJEv5XJbooiRPdPfXk6S7v5TkZVW1lFfNXPL+S5PcluRXs/GN1xI271oCNKuSvC3J3au3t8/O2bKbqurhJB9P8pdJTn5d929k46qOM+Onk/z7Sbc9meT8gS0vxJL3vzEbV/tPr/78hk1HcNdODlsiR3CzKslVSS5ZvX/B7JwtO3EE99Ikf5b//13gK5I8tvOz9ox/TXLhSbe9LMk3B7a8EEvef0eSy6vqTUn+KckXHMG9cK6AZr02yb3dfU13vznJkap69fSoLTgRnGeSnJvk76vq15Kkqi7IRlS/PLRtL/hGkgur6ueTpKouT/If3X3ylehuteT930nyziSHk5wdR3A/FldAcyrJI0ke2nTbw9k4kvvHiUHbcOPqSePsbPyP+FCSO6rqt5J8P8nv+AWEM6aSfDfJe5L8aVX9ZJKnkvzm6KqtW/r+dPfTVXUoybEk51bVRasPfb67PzI4bXFqGd907Jyq6u6uU912Gv47NyY5p7s/djr/3nVxJu7zCaf789iJx83mzUvcv2Q79fyzWziCG1BV12fjO8B7prewHEt/3Cx9P6efK6CT7LXvQHardbnPl/h5nMkrIJ7fXnv+cQUEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTAiH3TA5aiqrx07M46Pj2ADR77O2pPPe4FaAvW9eVw4VQ89jmTHMEBMMIV0BZU1VNJ9k/v2Kbj3X1gegTAcxGgrdm/tKMI5/bAbucIDoARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCNCQqrqyqg6v3r+zqu6Y3sTuVlVXVNUDVXW0qu6vqoNV9fTqtmNVddn0RtgOL0g3q6vqrCQvj28GeB5V9ZIkH0pybXc/U1XnJLkkyaPdfXVVXZTko0neMbkTtsOT3rwrkhxNcqyqXj89hl3rmiSf7O5nkqS7n+nuR5KceKXe/Um8Ci6L4gpoViV5W5Lbk/xkkpuTfHl0EbvVhUkeTZKqeleS92bjsfKaqvpiklcluXpuHmyfAM2qJFdl4yilklwwO4dd7IkkFyVJd3+qqv4uG0dyj3b3FVX160kOJvnq4EbYFkdws16b5N7uvqa735zkSFW9enoUu9J9SW6sqnNXf37RSR//RJJDVVWBhRCgOZXkkSQPbbrt4WwcycEP6e5vJvlgks9W1d8k+ZMkf77p499PciTJW0cGwgtQ3X5uuVlVdXfXqW47Df+dG5Oc090fO51/76a//7RvZvuW+HVY4uZ1ts5fD1dAA6rq+iTvSXLP9BaAKa6ATrJTV0Bn2hI3r6Mlfh2WuHmdrfPXwxUQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBH7pgfAuquqpb3u/fHpAewNAgRnUHfX9AbYrRzBATDCFRC7XlU9lWT/9I495Hh3H5gewfoTIJZgv6OsnbPAn1mxUI7gABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAA2oqiur6vGqeqCqjlTVDVV1+6aPH62qCyY3rqvVfX949f51VXV3VT1ZVQ+u3m6e3vhcquqK1WPmaFXdX1UHq+rp1W3Hquqy6Y2wHV6Qbs5d3f2BqrohG6/2eXFVXZzkdUnu7+4nZ+etta6qg0luSfK+JLd296HhTc+rql6S5ENJru3uZ6rqnCSXJHm0u6+uqouSfDTJOyZ3wnYI0JwTr/C5P0kneX+S25IcSPKmqVF7xKVJrk7yliTn5Qdfi93smiSf7O5nkmT1z0eq6uTHESyGAM25qap+JcmLk/xSd/93VX0ryee6+9vD29bdG5Pc3t1PV9V5Sd5QVQ+uPnZ7d39ucNtzuTDJo0lSVe9K8t4kX07ymqr6YpJXZSOqsBh+BjTnru6+LMlXkrxyddvjqzfOrDuSXF5VJ640v9DdV63edmN8kuSJJBclSXd/Ksm7k5yfjSO4K5L8fpKDc/Ng+wRo3oeT3Do9Yo/5TpJ3Jjmc5Ows4wjuviQ3VtW5qz+/6KSPfyLJoU1HcrDrOYIb1t1fq6rzV7/15gx/h6yO3w4lOZbk3NUP8ZPk8939kcFpP1J3f7OqPpjks1X1vSTfzcY3L3+4+vj3q+pIkrcm+euxobAN1e05b7Oq6u6uU9222y1x83NZp89lCdzfu8s6fz0cwQEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjNg3PWApqqqnN2zT8ekBp9MC7/8lW6vHDruXAG1Bd9f0hr3M/Q/ryREcACNcAW1BVT2VZP/0jj3meHcfSBZ5/z+7HXhuArQ1+x0D7ayTfuazqPvfz6tgaxzBATBCgAAYIUAAjBAgAEYIEAAjBAiAEQIEwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIARpQVVdW1eNV9UBVHamqG6rq9k0fP1pVF0xufC6r7YdX719XVXdX1ZNV9eDq7ebpjc9n6fthnXhBujl3dfcHquqGbLza58VVdXGS1yW5v7ufnJ33vLqqDia5Jcn7ktza3YeGN23H0vfDWhCgOSde4XN/kk7y/iS3JTmQ5E1To7bo0iRXJ3lLkvPyg89lKZa+H9aCI7g5N1XVP2Tju/BPdvdjSb6VjSujb89OO6U3JjnS3U+v/vyGTUdY104O26Kl74e1IEBz7uruy5J8JckrV7c9vnrb7e5IcnlVnbhS+0J3X7V6+9zksC1a+n5YCwI078NJbp0esU3fSfLOJIeTnJ3lHWEtfT+sBT8DGtbdX6uq81e/9dbTe7aqu5+uqkNJjiU5t6ouWn3o8939kcFpW7L0/bAOqnsxz3k7oqq6u+tUt3Fmbb7Pl3b/L20vu9s6P54cwQEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMECIARAgTACAECYIQAATBCgAAYIUAAjBAgAEbsmx6wFFXV0xv2mOPTA4AzS4C2oLtregPAunEEB8AIV0BwBlXVU0n2T+/YQ45394HpEWyNAMGZtd8R7s7xs9plcQQHwAgBAmCEAAEwQoAAGCFAAIwQIABGCBAAIwQIgBECBMAIAQJghAABMEKAABghQACMECAARggQACMEiD2nqq6sqsOr9++sqjumN23VUrevdj9eVQ9W1Weq6ieq6g+q6uHV26Hpjew8AWKv6qo6K8nLk/zM9JhtWur2u7r7qiRfSnJ9kp/t7iuTXJXkuqq6dHQdO06A2MuuSHI0ybGqev30mG1a8vavJrkryW1J0t3fS/JHSd4xOYqdJ0DsVZXkbUnuXr29fXbOtix1+4mXJr8yyf8meWLTx/4tyU/t+CJG7ZseAEMqG0c/l6zev2B2zrYsdftNVXUwyWNJ/iLJhUn+efWxV+SHg8Qe4AqIveq1Se7t7mu6+81JjlTVq6dHbdFSt9/V3Vd3928nuSfJ7yVLine truncated
|
||||
"text/plain": [
|
||||
"<matplotlib.figure.Figure at 0x110568940>"
|
||||
"[Box(start=(3, 3), dir=(0, 1), tile=('QUINCY', 'ADAMS')),\n",
|
||||
" Box(start=(3, 2), dir=(1, 0), tile=('QUINCY', 'JONES')),\n",
|
||||
" Box(start=(5, 4), dir=(-1, 0), tile=('JOHN', 'ADAMS')),\n",
|
||||
" Box(start=(5, 0), dir=(0, 1), tile=('JAMES', 'EARL', 'JONES')),\n",
|
||||
" Box(start=(0, 4), dir=(1, 0), tile=('JOHN', 'QUINCY', 'ADAMS')),\n",
|
||||
" Box(start=(3, 0), dir=(1, 0), tile=('HENRY', 'JAMES')),\n",
|
||||
" Box(start=(6, 4), dir=(0, 1), tile=('JOHN', 'HENRY')),\n",
|
||||
" Box(start=(6, 1), dir=(1, 0), tile=('EARL',))]"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"board.boxes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{(3, 3): 'QUINCY',\n",
|
||||
" (3, 4): 'ADAMS',\n",
|
||||
" (3, 2): 'QUINCY',\n",
|
||||
" (4, 2): 'JONES',\n",
|
||||
" (5, 4): 'JOHN',\n",
|
||||
" (4, 4): 'ADAMS',\n",
|
||||
" (5, 0): 'JAMES',\n",
|
||||
" (5, 1): 'EARL',\n",
|
||||
" (5, 2): 'JONES',\n",
|
||||
" (0, 4): 'JOHN',\n",
|
||||
" (1, 4): 'QUINCY',\n",
|
||||
" (2, 4): 'ADAMS',\n",
|
||||
" (3, 0): 'HENRY',\n",
|
||||
" (4, 0): 'JAMES',\n",
|
||||
" (6, 4): 'JOHN',\n",
|
||||
" (6, 5): 'HENRY',\n",
|
||||
" (6, 1): 'EARL'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"board"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Plotting Pretty Output\n",
|
||||
"\n",
|
||||
"Technically, this is a solution. But it is not pretty to look at. \n",
|
||||
"\n",
|
||||
"I'll define `plot_board(board)` to plot each name in each tile and draw rectangles around them. I'll take care to make the font size appropriate for the length of each name, and I'll rotate the names according to the direction the tile is going in. The `fantasy` font is something like standard cartoon lettering.\n",
|
||||
"\n",
|
||||
"The `(x, y)` coordinates are taken to be the **center** of a square; names are centered on `(x, y)`. But the box is drawn a half-unit to each side of the center. Actually, we want it to be a biot less than a half-unit, becuase we want the outlines of the tiles to not-quite touch. In `plot_rectangle_around` we use 0.47 instead of 0.5 for this reason.\n",
|
||||
"\n",
|
||||
"One more trick: Randall has `\"Billy D. WIlliams\"` as a two-square name, not three, so I will write it as `\"Billy_D. Williams\"`, split that into two names, and only convert the `\"_\"` to a `\" \"` when plotting the text.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def plot_board(board):\n",
|
||||
" \"\"\"Plot each tile box, plus a big box around the whole board.\"\"\"\n",
|
||||
" plt.figure(figsize=(board.width, board.height))\n",
|
||||
" plt.axis('off'); plt.axis('equal')\n",
|
||||
" print(f'{len(board.boxes)} tiles placed on {len(board)} squares')\n",
|
||||
" for box in board.boxes:\n",
|
||||
" squares = [move(box.start, i, box.dir) for i in range(len(box.tile))]\n",
|
||||
" plot_rectangle_around(squares)\n",
|
||||
" for (x, y), name in zip(squares, box.tile):\n",
|
||||
" plt.text(x, y, name.replace('_', ' '), va='center', ha='center', \n",
|
||||
" fontsize=fontsizes[len(name)], fontdict={'family': 'fantasy'}, \n",
|
||||
" rotation=directions[box.dir])\n",
|
||||
"\n",
|
||||
"def plot_rectangle_around(squares, half=0.47):\n",
|
||||
" \"\"\"Plot a rectangle around this list of squares.\"\"\"\n",
|
||||
" Xs, Ys = transpose(squares)\n",
|
||||
" x0, y0 = min(Xs) - half, min(Ys) - half\n",
|
||||
" x1, y1 = max(Xs) + half, max(Ys) + half\n",
|
||||
" plt.plot([x0, x1, x1, x0, x0], \n",
|
||||
" [y0, y0, y1, y1, y0], \n",
|
||||
" 'k-')\n",
|
||||
" \n",
|
||||
"def transpose(matrix) -> list: return list(zip(*matrix))\n",
|
||||
"\n",
|
||||
"fontsizes = (0, 15, 15, 15, 13, 11, 10, 9, 7, 7, 6, 6, 6, 6, 6) # Indexed by len(name)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"8 tiles placed on 17 squares\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 504x504 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"plot_board(dominoes(tiles8, 6, 6))"
|
||||
"plot_board(board)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -212,46 +318,56 @@
|
||||
"# All the Names\n",
|
||||
"\n",
|
||||
"Now let's try all the names from the comic, courtesy of \n",
|
||||
" [explainxkcd](http://www.explainxkcd.com/wiki/index.php/1970:_Name_Dominoes), with a few typos corrected:"
|
||||
" [explainxkcd](http://www.explainxkcd.com/wiki/index.php/1970:_Name_Dominoes), with a few typos corrected. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"271"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def name_tiles(text):\n",
|
||||
" \"For each line of text, create a tile of ('First Name(s)', 'Lastname').\"\n",
|
||||
" return [name.strip().rpartition(' ')[0::2]\n",
|
||||
" for name in text.upper().split(',')]\n",
|
||||
"def define_tiles(text) -> list:\n",
|
||||
" \"Create a list of tiles from the text containing names.\"\n",
|
||||
" return [Tile(name.strip().split()) for name in text.upper().split(',')]\n",
|
||||
" \n",
|
||||
"xkcdtiles = name_tiles(\"\"\"Christian Campbell, Neve Campbell, Joe McCarthy, Eugene McCarthy, \n",
|
||||
" Gene Vincent, Gene Kelly, Kate Hudson, Rock Hudson, Gordon Brown, James Brown, Jon Brown, \n",
|
||||
"xkcdtiles = define_tiles(\"\"\"Christian Campbell, Neve Campbell, Joe McCarthy, Eugene McCarthy, \n",
|
||||
" Gene Vincent, Gene Kelly, Kate Hudson, Rock Hudson, Gordon Brown, James Brown, John Brown, \n",
|
||||
" John Howard, Columbo, Chris Columbus, Christopher Columbus, Naomi Campbell, Joseph Campbell, \n",
|
||||
" Joseph Smith, Frank Vincent, John Kelly, Katherine Johnson, The Rock, Chris Rock, Chris Isaac, \n",
|
||||
" James Newton Howard, John Wayne, Howard Stern, Howard Hunt, Chris Hughes, Naomi Watts, \n",
|
||||
" Naomi Klein, Kevin Kline, Francis Bacon, Francis Drake, Lyndon Johnson, Oscar The Grouch, \n",
|
||||
" Oscar Isaac, Isaac Hayes, Isaac Newton, Wayne Newton, Wayne Knight, Helen Hunt, Helen Hughes, \n",
|
||||
" James Watt, James Watt, Kevin Costner, Kevin Bacon, Kevin Love, Lisa Frank, Frank Drake, Drake, \n",
|
||||
" Oscar de la Renta, Oscar de la Hoya, Sean Hayes, Wallace Shawn, Wayne Howard, Wayne Brady, \n",
|
||||
" Oscar de_la Renta, Oscar de_la Hoya, Sean Hayes, Wallace Shawn, Wayne Howard, Wayne Brady, \n",
|
||||
" James Brady, Tom Brady, Helen Thomas, Tom Hanks, Hank Aaron, Aaron Carter, Stephen James, \n",
|
||||
" Will Smith, Kevin Smith, Kevin James, Garfield, James Garfield, Warren Buffett, Jimmy Buffett, \n",
|
||||
" Warren Beatty, Elizabeth Warren, Earl Warren, Elizabeth Kolbert, Stephen Colbert, \n",
|
||||
" Charles Wallace, James Monroe, Marilyn Monroe, Hank Williams, William C. Williams, Steve Harvey,\n",
|
||||
" Warren Beatty, Elizabeth Warren, Earl Warren, Elizabeth Kolbert, Stephen Colbert, George Wallace,\n",
|
||||
" Charles Wallace, James Monroe, Marilyn Monroe, Hank Williams, William_C. Williams, Steve Harvey,\n",
|
||||
" Domino Harvey, Harvey Milk, James Saint James, Etta James, Jim Jones, James Earl Jones, \n",
|
||||
" Charlie Parker, Ray Parker, Ray Charles, Charles Manson, Marilyn Manson, Robin Williams, \n",
|
||||
" Billy D. Williams, Will Wright, Fats Domino, Bill Clinton, Jimmy John, Tom Jones, Tommy John, \n",
|
||||
" Charlie Parker, Ray Parker_Jr., Ray Charles, Charles Manson, Marilyn Manson, Robin Williams, \n",
|
||||
" Billy_D. Williams, Will Wright, Fats Domino, Bill Clinton, Jimmy John, Tom Jones, Tommy John, \n",
|
||||
" Quincy Jones, James Earl Ray, Man Ray, Rachel Ray, Ray Allen, Tim Allen, Tim Cook, Tim Howard, \n",
|
||||
" Robin Wright, Wilbur Wright, Fatty Arbuckle, Fat Joe, George Clinton, John Kerry, \n",
|
||||
" Kerry Washington, John Irving, John Quincy Adams, John Adams, Amy Adams, Aimee Mann, Super Man, \n",
|
||||
" Bat Man, Ayn Rand, Lily Allen, Paul Allen, Ron Howard, Howard Hughes, Joe Kennedy, George Bush, \n",
|
||||
" George Washington, Washington Irving, Martha Washington, Ma Rainey, Jack Ma, Super Grover, \n",
|
||||
" Jack Black, Rand Paul, Paul Ryan, Paul Simon, Ron Paul, John Hughes, Langston Hughes, \n",
|
||||
" John F. Kennedy, Little Richard, Rich Little, Martha Stewart, Yo Yo Ma, Ma Bell, \n",
|
||||
" John_F. Kennedy, Little Richard, Rich Little, Martha Stewart, Yo-Yo Ma, Ma Bell, \n",
|
||||
" Grover Cleveland Alexander, Grover Cleveland, Jack White, Jack Ryan, Debby Ryan, Carly Simon, \n",
|
||||
" Carly Hughes, Charles Evans Hughes, John Williams, Little John, Stuart Little, Potter Stewart, \n",
|
||||
" Kristen Stewart, Kristen Bell, Kristen Hooks, Alexander Graham Bell, Franklin Graham, \n",
|
||||
" Kristen Stewart, Kristen Bell, Bell Hooks, Alexander Graham Bell, Franklin Graham, \n",
|
||||
" Lloyd Alexander, Meg White, Meg Ryan, Debbie Reynolds, John Reynolds, Carly Fiorina, \n",
|
||||
" Grace Lee Boggs, Wade Boggs, William Safire, Prince William, Little Prince, Harry Potter, \n",
|
||||
" James Potter, James Hook, James Dean, Aretha Franklin, Frank Lloyd Wright, Barry White, \n",
|
||||
@@ -265,138 +381,14 @@
|
||||
" Chris Hemsworth, Chris Evans, Topher Grace, Van Morrison, Sheryl Crow, Sheryl Sandberg, \n",
|
||||
" Cameron Crowe, Long John Silver, Olivia Newton John, Huey Long, John Edwards, Candy Crowley, \n",
|
||||
" Aleister Crowley, James Fenimore Cooper, James Cook, Robert Frost, Bob Evans, Evan Taylor Jones,\n",
|
||||
" James Cameron, Cam Newton, Cameron Diaz, Huey Newton, Huey Lewis, John Lewis, Jenny Lewis, \n",
|
||||
" Ryan Lewis, Burt Reynolds, Alistair Cooke, Alistair Cookie, Cokie Roberts, John Roberts, \n",
|
||||
" Robert Johnson, Robert E. Lee, Tommy Lee, Tommy Lee Jones, Etta James, John Oliver, \n",
|
||||
" Ryan Reynolds, Alastair Reynolds, George Wallace\"\"\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"270"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
" Van Jones, James Cameron, Cam Newton, Cameron Diaz, Huey Newton, Huey Lewis, John Lewis, \n",
|
||||
" Jenny Lewis, Ryan Lewis, Burt Reynolds, Alistair Cooke, Alistair Cookie, Cokie Roberts, \n",
|
||||
" John Roberts, Robert Johnson, Robert E._Lee, Tommy Lee, Tommy Lee Jones, Etta James,\n",
|
||||
" John Oliver, Ryan Reynolds, Alastair Reynolds\"\"\") \n",
|
||||
"\n",
|
||||
"len(xkcdtiles)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[('JAMES EARL', 'RAY'),\n",
|
||||
" ('BURT', 'REYNOLDS'),\n",
|
||||
" ('CHRISTOPHER', 'COLUMBUS'),\n",
|
||||
" ('KRISTEN', 'STEWART'),\n",
|
||||
" ('FAT', 'JOE'),\n",
|
||||
" ('HAROLD', 'LLOYD'),\n",
|
||||
" ('ISAAC', 'HAYES'),\n",
|
||||
" ('PAUL', 'RYAN'),\n",
|
||||
" ('GENE', 'KELLY'),\n",
|
||||
" ('MEGYN', 'KELLY')]"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"random.sample(xkcdtiles, 10)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Approximate and Partial Matches\n",
|
||||
"\n",
|
||||
"Two tile halves match if they are an exact match, like \"ADAMS\" and \"ADAMS\", or if they are an **approximate match**, like \"AMY\" and \"AIMEE\". To accomodate this, you can manually define allowable approximate matches by making the global variable `synsets` (synonym sets) be a mapping from a name to the set of approximate matches it should match, which can be done like this:\n",
|
||||
"\n",
|
||||
" synsets = synonyms(\"AMY=AIMEE, COOK=COOKE=COOKIE=COKIE, ...\")\n",
|
||||
"\n",
|
||||
"Another issue is a **partial match**: in the comic, some tiles, like \"FRANK LLOYD WRIGHT\" are 3 squares wide, and some, like \"PRINCE\" are only one square wide. For simplicity, I chose to have all my tiles be 2 squares wide, but I still want `'LLOYD'` to match the first name of `('FRANK LLOYD', 'WRIGHT')`. To accomplish this, the second argument to `synonyms` is a list of tiles; the function will go through these and add synset entries for parts of first names,\n",
|
||||
"so that both `'FRANK'` and `'LLOYD'` will match `'FRANK LLOYD'`. As for \"PRINCE\", he gets represented as the tile `('', 'PRINCE')`. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import collections\n",
|
||||
"\n",
|
||||
"def synonyms(text='', tiles=()): \n",
|
||||
" \"synonyms('AMY=AIMEE') => dict(AMY={'AMY', 'AIMEE'}, AIMEE={'AMY', 'AIMEE'})\"\n",
|
||||
" synsets = collections.defaultdict(set)\n",
|
||||
" # Process `text`\n",
|
||||
" for text1 in text.upper().split(','):\n",
|
||||
" synset = set(text1.strip().split('='))\n",
|
||||
" for s in synset:\n",
|
||||
" synsets[s] |= synset\n",
|
||||
" # Process `tiles`\n",
|
||||
" for (first, last) in tiles:\n",
|
||||
" for part in first.split():\n",
|
||||
" synsets[part].add(first)\n",
|
||||
" synsets[first].add(part)\n",
|
||||
" return synsets\n",
|
||||
"\n",
|
||||
"synsets = synonyms(\"\"\"AMY=AIMEE, COOK=COOKE=COOKIE=COKIE, ALASTAIR=ALISTAIR, \n",
|
||||
" COLUMBO=COLUMBUS, SAFIRE=SAPPHIRE=GARNET, GARNET=RUBY, CHARLIE=CHARLES, SEAN=SHAWN,\n",
|
||||
" JIMMY=JAMES, MAN=MANN, JACK=JOHN, TOM=TOMMY, WILL=WILLIAM=WILLIAMS, ROBERT=ROBERTS=BOB, \n",
|
||||
" CAM=CAMERON, OLIVER=OLIVIA, EDWARD=EDWARDS, RICH=RICHARD, CHRIS=CHRISTOPHER=TOPHER, \n",
|
||||
" FAT=FATS=FATTY, WALT=WALTER, HANK=HANKS, CROW=CROWE, COLBERT=KOLBERT\"\"\", xkcdtiles)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"collapsed": true
|
||||
},
|
||||
"source": [
|
||||
"To make this work, I update `legal` to consult the `synsets` global variable for an approximate or partial matches, and while I'm changing `legal`, I'll also disallow a match between the empty first name of \"PRINCE\" with the empty first name of \"DRAKE\"."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def legal(name, loc, board):\n",
|
||||
" \"Is it legal to place this value on this location on board?\"\n",
|
||||
" return (board[loc] is empty and \n",
|
||||
" all(board[nbr] is empty \n",
|
||||
" or board[nbr] == name != '' \n",
|
||||
" or board[nbr] in synsets[name]\n",
|
||||
" for nbr in neighbors(loc, board)))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Final Result (with Random Restart)\n",
|
||||
"\n",
|
||||
"The program sometimes gets stuck after placing relatively few tiles. I could modify the program to *back up* in this case, but it will be easier to just *give up* and restart with an empty board. I can restart multiple times, and accept the best board (the one on which the most tiles were placed):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
@@ -404,37 +396,126 @@
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<matplotlib.figure.Figure at 0x11058c668>"
|
||||
"[('EVAN', 'TAYLOR', 'JONES'),\n",
|
||||
" ('JOHN', 'LEWIS'),\n",
|
||||
" ('RYAN', 'REYNOLDS'),\n",
|
||||
" ('RAY', 'PARKER_JR.'),\n",
|
||||
" ('FATS', 'DOMINO'),\n",
|
||||
" ('ARETHA', 'FRANKLIN'),\n",
|
||||
" ('CHRIS', 'PRATT'),\n",
|
||||
" ('HENRY', 'JAMES'),\n",
|
||||
" ('FAT', 'JOE'),\n",
|
||||
" ('TOMMY', 'JOHN'),\n",
|
||||
" ('JAMES', 'NEWTON', 'HOWARD'),\n",
|
||||
" ('BARRY', 'WHITE')]"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def best(boards): return max(boards, key=lambda board: len(board.boxes))\n",
|
||||
"\n",
|
||||
"plot_board(best(dominoes(xkcdtiles) for _ in range(100)))"
|
||||
"random.sample(xkcdtiles, 12)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"*Note:* I used a 16×24 square board while the xkcd comic used 27×35, but if I tried to fit 27 squares across then each tile would be smaller, and many names would overflow the sides of the dominoes too much. Here is the original xkcd comic:\n",
|
||||
"# Approximate Matches\n",
|
||||
"\n",
|
||||
"[](https://xkcd.com/1970/)\n",
|
||||
"Two names in adjacent squares match if they are an exact match, like `'ADAMS'` and `'ADAMS'`, or if they are an **approximate match**, like `'AMY'` and `'AIMEE'`. To accomodate this, I will manually define a set of approximate matches (again courtesy of \n",
|
||||
" [explainxkcd](http://www.explainxkcd.com/wiki/index.php/1970:_Name_Dominoes)) by making the global variable `synonyms` be a set of pairs of names that should match: `{('AMY', 'AIMEE'), ('COOK', 'COOKE'),...}`. I will also redefine the function `allowed` to say that synonyms are allowed.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def define_synonyms(text='', tiles=()) -> set: \n",
|
||||
" \"\"\"Build a set of {(A, B),...} pairs, meaning that A and B are synonyms.\"\"\"\n",
|
||||
" synsets = [synset.strip().split('=') for synset in text.split(',')]\n",
|
||||
" return {(a, b) for syn in synsets for a in syn for b in syn}\n",
|
||||
"\n",
|
||||
"synonyms = define_synonyms(\"\"\"AMY=AIMEE, COOK=COOKE=COOKIE=COKIE, ALASTAIR=ALISTAIR=ALESTIER, \n",
|
||||
" COLUMBO=COLUMBUS, SAFIRE=SAPPHIRE=GARNET, GARNET=RUBY, CHARLIE=CHARLES, SEAN=SHAWN,\n",
|
||||
" JIMMY=JIM=JAMES, MAN=MANN, JACK=JOHN=JON=JOHN_F., TOM=TOMMY=THOMAS, HOOK=HOOKS, \n",
|
||||
" ROBERT=ROBERTS=BOB, CAM=CAMERON, OLIVER=OLIVIA, EDWARD=EDWARDS, RICH=RICHARD, MEG=MEGYN, \n",
|
||||
" CHRIS=CHRISTOPHER=CHRISTINE=TOPHER, FAT=FATS=FATTY, WALT=WALTER, HANK=HANKS, CROW=CROWE, \n",
|
||||
" COLBERT=KOLBERT, LEE=E._LEE, KLINE=KLEIN, JOE=JOSEPH, GENE=EUGENE, KATE=KATHERINE, \n",
|
||||
" FRANK=FRANCIS=FRANKLIN, WATT=WATTS, STEVE=STEPHEN, DEBBY=DEBBIE, STUART=STEWART, \n",
|
||||
" WILL=WILLIAM=WILLIAMS=BILLY=BILLY_D.=WILLIAM_C., RUSS=RUSSELL, EVAN=EVANS\"\"\")\n",
|
||||
"\n",
|
||||
"def allowed(name, nbr_contents) -> bool: \n",
|
||||
" \"\"\"A name is allowed next to an empty square, a square off the board, an exact match, or a synonym.\"\"\"\n",
|
||||
" return nbr_contents in (name, empty, off) or (name, nbr_contents) in synonyms"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Final Result (after Multiple Tries)\n",
|
||||
"\n",
|
||||
"The program sometimes gets stuck after placing relatively few tiles. I could modify the program to *back up* in this case, but it will be easier to just *give up* and restart with an empty board. I can restart multiple times, and accept the best board (the one on which the most names were placed):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"152 tiles placed on 312 squares\n",
|
||||
"CPU times: user 3min 56s, sys: 940 ms, total: 3min 57s\n",
|
||||
"Wall time: 3min 59s\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "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 truncated
|
||||
"text/plain": [
|
||||
"<Figure size 1152x2520 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%time plot_board(max((place_tiles(xkcdtiles) for _ in range(100)), key=len))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"*Note:* I used a 16×35 square board while the xkcd comic used 27×35, because if I tried to fit 27 squares across then each tile would be smaller, and many names would overflow the sides of the dominoes too much. Here is the [original xkcd comic](https://xkcd.com/1970/):\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# What's Next?\n",
|
||||
"\n",
|
||||
"I'm happy with the results, but here are some ideas for improvements, if you want something to work on:\n",
|
||||
"- Allow tiles that are 1 or 3 squares wide, like `('PRINCE',)` or `('FRANK', 'LLOYD', 'WRIGHT')`.\n",
|
||||
"- Print names vertically in tiles that are placed vertically, and upside down for tiles that are placed horizontally, but with the first name on the right.\n",
|
||||
"- Print shorter names with a bigger font and longer names with a smaller font.\n",
|
||||
"- Download a bunch of names from Wikipedia and fill a 200 × 300 board.\n",
|
||||
"- If you like xkcd try [regex golf](https://github.com/norvig/pytudes/blob/master/ipynb/xkcd1313.ipynb), and [part 2](https://github.com/norvig/pytudes/blob/master/ipynb/xkcd1313-part2.ipynb).\n"
|
||||
"- See if a different strategy can fit more tiles on the board. \n",
|
||||
" - Backtracking? \n",
|
||||
" - Being smarter about which frontier square to try next? \n",
|
||||
" - Being smarter about which tile to place on that frontier square?\n",
|
||||
"- Make the dominoes black with white letters? Ever-so-slightly round the corners?\n",
|
||||
"- Download a bunch of names from Wikipedia and fill a 200 × 300 board, then laser cut tham and hang them on your wall?\n",
|
||||
"- If you like xkcd try [regex golf](https://github.com/norvig/pytudes/blob/master/ipynb/xkcd1313.ipynb), and [part 2](https://github.com/norvig/pytudes/blob/master/ipynb/xkcd1313-part2.ipynb).\n",
|
||||
"- What do you want to do?\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -454,7 +535,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.5.3"
|
||||
"version": "3.7.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
# Run "python pytudes.py" to create README.md for pytudes
|
||||
|
||||
|
||||
def nb(title, url, comment=''):
|
||||
"""Make a markdown table entry for a jupyter/ipython notebook."""
|
||||
urlb = f'/blob/master/ipynb/{url}'
|
||||
co = f'[co](https://colab.research.google.com/github/norvig/pytudes{urlb})'
|
||||
dn = f'[dn](https://beta.deepnote.org/launch?template=python_3.6&url=https%3A%2F%2Fgithub.com%2Fnorvig%2Fpytudes%2Fblob%2Fmaster%2Fipynb%2F{url}) '
|
||||
my = f'[my](https://mybinder.org/v2/gh/norvig/pytudes/master?filepath=ipynb%2F{url})'
|
||||
nb = f'[nb](https://nbviewer.jupyter.org/github/norvig/pytudes{urlb})'
|
||||
ti = f'<b><a href="ipynb/{url}" title="{comment}">{title}</a></b>'
|
||||
return f'| {co} {dn} {my} {nb} | {ti} |'
|
||||
|
||||
|
||||
def py(url, description, doc=''):
|
||||
"""Make a markdown table entry for a .py file."""
|
||||
if doc: doc = f'[documentation]({doc})'
|
||||
return f'|[{url}](/blob/master/py/{url})|*{description}*|{doc}|'
|
||||
|
||||
|
||||
body = f'''
|
||||
<div align="right" style="text-align: right"><i>Peter Norvig<br><a href="https://github.com/norvig/pytudes/blob/master/LICENSE">MIT License</a><br>2015-2020</i></div>
|
||||
|
||||
# pytudes
|
||||
|
||||
"An *étude* (a French word meaning *study*) is an instrumental musical composition, usually short, of considerable difficulty, and designed to provide practice material for perfecting a particular musical skill." — [Wikipedia](https://en.wikipedia.org/wiki/%C3%89tude)
|
||||
|
||||
This project contains **pytudes**—Python programs, usually short, for perfecting particular programming skills.
|
||||
Some programs are in Jupyter (`.ipynb`) notebooks, some in `.py` files. For each notebook you can:
|
||||
- Click on [co](https://colab.research.google.com) to **run** the file on Colab
|
||||
- Click on [dn](https://deepnote.com) to **run** the notebook on DeepNote
|
||||
- Click on [my](https://mybinder.org) to **run** the notebook on MyBinder
|
||||
- Click on [nb](https://nbviewer.jupyter.org/) to **view** the notebook on NBViewer
|
||||
- Click on the title to **view** the notebook on github.
|
||||
- Hover over the title to **view** a description.
|
||||
|
||||
|
||||
# Index of Jupyter (IPython) Notebooks
|
||||
|
||||
|
||||
|Run|Programming Examples|
|
||||
|---|--|
|
||||
{nb('Advent of Code 2018', 'Advent-2018.ipynb', 'Puzzle site with a coding puzzle each day for Advent 2018 ')}
|
||||
{nb('Advent of Code 2017', 'Advent%202017.ipynb', 'Puzzle site with a coding puzzle each day for Advent 2017')}
|
||||
{nb('Advent of Code 2016', 'Advent%20of%20Code.ipynb', 'Puzzle site with a coding puzzle each day for Advent 2016*')}
|
||||
{nb("Beal's Conjecture Revisited", 'Beal.ipynb', "A search for counterexamples to Beal's Conjecture")}
|
||||
{nb('Bike Speed Versus Grade', 'Bike%20Speed%20versus%20Grade.ipynb', 'How fast can I bike as the route gets steeper?')}
|
||||
{nb("Can't Stop", 'Cant-Stop.ipynb', 'Optimal play in a dice board game')}
|
||||
{nb('Chaos with Triangles', 'Sierpinski.ipynb', 'A surprising appearance of the Sierpinski triangle in a random walk between vertexes')}
|
||||
{nb("Conway's Game of Life", 'Life.ipynb', 'The cellular automata zero-player game')}
|
||||
{nb('Dice Baseball', 'Dice%20Baseball.ipynb', 'Simulating baseball games')}
|
||||
{nb('Generating Mazes', 'Maze.ipynb', 'Make a maze by generating a random tree superimposed on a grid')}
|
||||
{nb('Photo Focal Lengths', 'PhotoFocalLengths.ipynb', 'Generate charts of what focal lengths were used on a photo trip.')}
|
||||
{nb('Pickleball Tournament', 'Pickleball.ipynb', 'Scheduling a doubles tournament fairly and efficiently')}
|
||||
{nb('Project Euler Utilities', 'Project%20Euler%20Utils.ipynb', 'My utility functions for the Project Euler problems, including `Primes` and `Factors`')}
|
||||
{nb('Properly Ordered Card Hands', 'Orderable%20Cards.ipynb', 'Can you get your hand of cards into a nice order with just one move?')}
|
||||
{nb('Tracking Trump: Electoral Votes', 'Electoral%20Votes.ipynb', 'How many electoral votes would Trump get if he wins the state where he has positive net approval?')}
|
||||
{nb('Weighing Twelve Balls', 'TwelveBalls.ipynb', 'A puzzle where you are given some billiard balls and a balance scale, and asked to find the one ball that is heavier or lighter, in a limited number of weighings')}
|
||||
{nb('WWW: Who Will Win (NBA Title)?', 'WWW.ipynb', 'Computing the probability of winning the NBA title, for my home town Warriors, or any other team')}
|
||||
|
||||
|
||||
|Run | Logic and Number Puzzles|
|
||||
|--|---|
|
||||
{nb('Boggle / Inverse Boggle', 'Boggle.ipynb', 'Find all the words on a Boggle board; then find a board with a lot of words')}
|
||||
{nb('Chemical Element Spelling', 'ElementSpelling.ipynb', 'Spelling words using the chemical element symbols, like CoIn')}
|
||||
{nb('Cryptarithmetic', 'Cryptarithmetic.ipynb', 'Substitute digits for letters and make NUM + BER = PLAY')}
|
||||
{nb('Four 4s, Five 5s, and Countdown to 2016', 'Countdown.ipynb', 'Solving the equation 10 _ 9 _ 8 _ 7 _ 6 _ 5 _ 4 _ 3 _ 2 _ 1 = 2016. From an Alex Bellos puzzle')}
|
||||
{nb('The Devil and the Coin Flip Game', 'Coin%20Flip.ipynb', 'How to beat the Devil at his own game')}
|
||||
{nb('Flipping Cards: A Guessing Game', 'flipping.ipynb', 'Can you go through a deck of cards, guessing higher or lower correctly for each card?')}
|
||||
{nb('Gesture Typing', 'Gesture%20Typing.ipynb', 'What word has the longest path on a gesture-typing smartphone keyboard?')}
|
||||
{nb('Ghost', 'Ghost.ipynb', 'The word game Ghost (add letters, try to avoid making a word)')}
|
||||
{nb('How Many Soldiers Do You Need to Beat the Night King?', 'NightKing.ipynb', 'Investigasting a battle between the army of the dead and the army of the living')}
|
||||
{nb("Let's Code About Bike Locks", 'Fred%20Buns.ipynb', 'A tale of a bicycle combination lock that uses letters instead of digits. Inspired by Bike Snob NYC')}
|
||||
{nb('Pairing Socks', 'Socks.ipynb', 'What is the probability that you will be able to pair up socks as you randomly pull them out of the dryer?')}
|
||||
{nb('Portmantout Words', 'Portmantout.ipynb', 'Find a long word that squishes together a bunch of words')}
|
||||
{nb('The Puzzle of the Misanthropic Neighbors', 'Mean%20Misanthrope%20Density.ipynb', 'How crowded will this neighborhood be, if nobody wants to live next door to anyone else?')}
|
||||
{nb('Refactoring a Crossword Game Program', 'Scrabble.ipynb', 'Refactoring the Scrabble / Word with Friends game from Udacity 212')}
|
||||
{nb('Riddler: Battle Royale', 'Riddler%20Battle%20Royale.ipynb', 'A puzzle involving allocating your troops and going up against an opponent')}
|
||||
{nb('Riddler Lottery', 'RiddlerLottery.ipynb', 'Can you find what lottery number tickets these five friends picked?')}
|
||||
{nb('Riddler: Tour de 538', 'TourDe538.ipynb', 'Solve a puzzle involving the best pace for a bicycle race.')}
|
||||
{nb('Sicherman Dice', 'Sicherman%20Dice.ipynb', 'Find a pair of dice that is like a regular pair of dice, only different')}
|
||||
{nb("Sol Golomb's Rectangle Puzzle", 'Golomb-Puzzle.ipynb', 'A Puzzle involving placing rectangles of different sizes inside a square')}
|
||||
{nb('Spelling Bee', 'SpellingBee.ipynb', 'Find the highest-scoring board for the NY Times Spelling Bee puzzle')}
|
||||
{nb('Translating English Sentences into Propositional Logic Statements', 'PropositionalLogic.ipynb', 'Automatically convert informal English sentences into formal Propositional Logic')}
|
||||
{nb('How to Do Things with Words: NLP in Python', 'How%20to%20Do%20Things%20with%20Words.ipynb', 'Spelling Correction, Secret Codes, Word Segmentation, and more')}
|
||||
{nb('When Cheryl Met Eve: A Birthday Story', 'Cheryl-and-Eve.ipynb', "Inventing new puzzles in the Style of Cheryl's Birthday")}
|
||||
{nb("When is Cheryl's Birthday?", 'Cheryl.ipynb', "Solving the *Cheryl's Birthday* logic puzzle")}
|
||||
{nb("World's Longest Palindrome", 'pal3.ipynb', 'Searching for a long Panama-style palindrome, this time letter-by-letter')}
|
||||
{nb('xkcd 1313: Regex Golf (Part 2: Infinite Problems)', 'xkcd1313-part2.ipynb', 'Regex Golf: better, faster, funner. With Stefan Pochmann')}
|
||||
{nb('xkcd 1313: Regex Golf', 'xkcd1313.ipynb', 'Find the smallest regular expression; inspired by Randall Monroe')}
|
||||
{nb('xkcd 1970: Name Dominoes', 'xkcd-Name-Dominoes.ipynb', 'Lay out dominoes legally; the dominoes have people names, not numbers')}
|
||||
|
||||
|
||||
|Run|Math Concepts|
|
||||
|--|--|
|
||||
{nb('A Concrete Introduction to Probability', 'Probability.ipynb', 'Code and examples of the basic principles of Probability Theory')}
|
||||
{nb('Probability, Paradox, and the Reasonable Person Principle', 'ProbabilityParadox.ipynb', 'Some classic paradoxes in Probability Theory, and how to think about disagreements')}
|
||||
{nb('Estimating Probabilities with Simulations', 'ProbabilitySimulation.ipynb', 'When the sample space is too complex, simulations can estimate probabilities')}
|
||||
{nb('Economics Simulation', 'Economics.ipynb', 'A simulation of a simple economic game')}
|
||||
{nb("Euler's Sum of Powers Conjecture", "Euler's%20Conjecture.ipynb", 'Solving a 200-year-old puzzle by finding integers that satisfy a<sup>5</sup> + b<sup>5</sup> + c<sup>5</sup> + d<sup>5</sup> = e<sup>5</sup>')}
|
||||
{nb('How to Count Things', 'How%20To%20Count%20Things.ipynb', 'Combinatorial math: how to count how many things there are, when there are a lot of them')}
|
||||
{nb('Symbolic Algebra, Simplification, and Differentiation', 'Differentiation.ipynb', 'A computer algebra system that manipulates expressions, including symbolic differentiation')}
|
||||
|
||||
|
||||
|Run|Computer Science Algorithms and Concepts|
|
||||
|--|--|
|
||||
{nb('Bad Grade, Good Experience', 'Snobol.ipynb', 'As a student, did you ever get a bad grade on a programming assignment? (Snobol, Concordance)')}
|
||||
{nb('BASIC Interpreter', 'BASIC.ipynb', 'How to write an interpreter for the BASIC programming language')}
|
||||
{nb('The Convex Hull Problem', 'Convex%20Hull.ipynb', 'A classic Computer Science Algorithm')}
|
||||
{nb('The Stable Matching Problem', 'StableMatching.ipynb', 'What is the best way to pair up two grpups with each other, obeying preferences?')}
|
||||
{nb('The Traveling Salesperson Problem', 'TSP.ipynb', 'Another of the classics')}
|
||||
|
||||
|
||||
# Index of Python Files
|
||||
|
||||
|
||||
| File | Description | Documentation |
|
||||
|:--------|:-------------------|----|
|
||||
{py('beal.py', "Search for counterexamples to Beal's Conjecture", 'http://norvig.com/beal.html')}
|
||||
{py('docex.py', 'A framework for running unit tests, similar to `doctest`')}
|
||||
{py('ibol.py', 'An Exercise in Species Barcoding', 'http://norvig.com/ibol.html')}
|
||||
{py('lettercount.py', 'Convert Google Ngram Counts to Letter Counts', 'http://norvig.com/mayzner.html')}
|
||||
{py('lis.py', 'Lisp Interpreter written in Python', 'http://norvig.com/lispy.html')}
|
||||
{py('lispy.py', 'Even Better Lisp Interpreter written in Python', 'http://norvig.com/lispy2.html')}
|
||||
{py('lispytest.py', 'Tests for Lisp Interpreters')}
|
||||
{py('pal.py', 'Find long palindromes', 'http://norvig.com/palindrome.html')}
|
||||
{py('pal2.py', 'Find longer palindromes', 'http://norvig.com/palindrome.html')}
|
||||
{py('pal3.py', 'Find even longer palindromes', 'http://norvig.com/palindrome.html')}
|
||||
{py('pytudes.py', 'Pre-process text to generate this README.md file.')}
|
||||
{py('py2html.py', 'Pretty-printer to format Python files as html')}
|
||||
{py('SET.py', 'Analyze the card game SET', 'http://norvig.com/SET.html')}
|
||||
{py('spell.py', 'Spelling corrector', 'http://norvig.com/spell-correct.html')}
|
||||
{py('sudoku.py', 'Program to solve sudoku puzzles', 'http://norvig.com/sudoku.html')}
|
||||
{py('testaccum.py', 'Tests for my failed Python `accumulation display` proposal', 'http://norvig.com/pyacc.html')}
|
||||
{py('yaptu.py', 'Yet Another Python Templating Utility')}
|
||||
# Etudes for Programmers
|
||||
I got the idea for the *"etudes"* part of the name from
|
||||
this [1978 book](https://books.google.com/books/about/Etudes_for_programmers.html?id=u89WAAAAMAAJ)
|
||||
by [Charles Wetherell](http://demin.ws/blog/english/2012/08/25/interview-with-charles-wetherell)
|
||||
that was very influential to me when I was first learning to program. I still have my copy.
|
||||
|
||||

|
||||
'''
|
||||
|
||||
output = 'README.md'
|
||||
with open(output, 'w') as out:
|
||||
print(f'Wrote {output}; {out.write(body)} characters')
|
||||
@@ -7,7 +7,7 @@
|
||||
- Deepnote
|
||||
- [Epiphany](https://epiphany.pub/) "a new blogging experience"
|
||||
- [Iodide](https://github.com/iodide-project/iodide) Run notebook in the browser
|
||||
- {Pyodide](https://github.com/iodide-project/pyodide) Run Python in the browser
|
||||
- [Pyodide](https://github.com/iodide-project/pyodide) Run Python in the browser
|
||||
- [Klipse](https://github.com/viebel/klipse) Run interactive code in tech blogs
|
||||
- Matlab / R
|
||||
- Mathematica
|
||||
|
||||
Reference in new issue
Block a user