Add files via upload

This commit is contained in:
Peter Norvig authored and GitHub committed 2020-07-01 17:14:27 -07:00
1 parent a84f7d5d79
commit 29bbcff8ce
3 files changed
+840 -183

No files matched your search

+227 -169
View File
@@ -4,12 +4,13 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"<div align=\"right\"><i>Peter Norvig, Oct 2017<br>Revised Jan 2020</i></div>\n",
"<div align=\"right\" style=\"text-align: right\"><i>Peter Norvig, Oct 2017<br>Revised Jan 2020</i></div>\n",
"\n",
"# Bicycling: Speed v. Grade, VAM, Eddington, Hill-index\n",
"# Bicycling: Speed, Grade, VAM, Hill-index, Eddington\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 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 `#`)."
"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\n",
"of `Ride` structures. "
]
},
{
@@ -18,48 +19,57 @@
"metadata": {},
"outputs": [],
"source": [
"%matplotlib inline\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import re\n",
"from collections import namedtuple\n",
"\n",
"Ride = namedtuple('Ride', 'miles, hours, feet, date, title')\n",
"\n",
"def parse_ride(line: str) -> Ride:\n",
" \"\"\"Parse a line from a Strava log file into a `Ride`. Line format is tab separated:\n",
" Ride\tThu, 8/9/2018\tBRNW\t4:58:07\t68.41 mi\t3,862 ft\"\"\"\n",
" _, date, title, time, mi, ft = line.strip().split('\\t')\n",
" return Ride(number(mi), hours(*time.split(':')[::-1]), number(ft), date, title)\n",
"\n",
"def number(string) -> float: return float(re.sub(r'[^0-9. ]', '', string))\n",
"\n",
"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 line.strip() and not line.startswith('#') and 'MTB' not in line and '\t0 ft' not in line]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"From the raw data I will derive three important vectors of numbers: \n",
"- `mls`: length of ride in miles\n",
"- `mph`: speed in miles per hour\n",
"- `fpm`: grade in feet climbed per mile\n",
"\n",
"I'll show a simple scatter plot visualization:"
"from collections import namedtuple"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"Ride = namedtuple('Ride', 'miles, hours, feet, date, title')\n",
"\n",
"def parse_rides(lines: str) -> [Ride]:\n",
" \"\"\"Parse lines from a Strava log file into a list of `Ride`. Tab separated fields:\n",
" Ride\tThu, 8/9/2018\tBRNW\t4:58:07\t68.41 mi\t3,862 ft\"\"\"\n",
" return [Ride(number(mi), parse_hours(time), number(ft), date, title)\n",
" for line in lines if line.startswith('Ride')\n",
" for _, date, title, time, mi, ft in [line.strip().split('\\t')]]\n",
"\n",
"def number(string) -> float: return float(re.sub(r'[^0-9. ]', '', string))\n",
"\n",
"def parse_hours(time: str) -> float: \n",
" if time.count(':') < 2: time = '0:' + time\n",
" sec, min, hour = time.split(':')[::-1]\n",
" return int(sec)/3600 + int(min)/60 + int(hour)\n",
"\n",
"rides = parse_rides(open('bikerides25.tsv'))#{parse_ride(line) for line in open('bikerides25.tsv') if line.startswith('Ride')}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"From the raw data I will derive three important arrays of numbers: \n",
"- `miles`: array of lengths of each ride in miles\n",
"- `hours`: array of durations of each ride in hours\n",
"- `feet`: array of total climbing of each ride in feet \n",
"\n",
"I'll show a simple scatter plot visualization:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"image/png": "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 truncated
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
@@ -71,15 +81,13 @@
}
],
"source": [
"def vectors3(rides) -> tuple:\n",
" \"Return vectors of (miles, miles/hour, feet/mile)\"\n",
" return ([r.miles for r in rides],\n",
" [r.miles / r.hours for r in rides],\n",
" [r.feet / r.miles for r in rides])\n",
"def column(attr, rides): return np.array([getattr(r, attr) for r in rides])\n",
"\n",
"mls, mph, fpm = vectors3(rides)\n",
"miles = column('miles', rides)\n",
"hours = column('hours', rides)\n",
"feet = column('feet', rides)\n",
"\n",
"plt.scatter(fpm, mph);"
"plt.scatter(feet/miles, miles/hours);"
]
},
{
@@ -88,12 +96,12 @@
"source": [
"# Making it Prettier\n",
"\n",
"As expected, there is a lot of variance, but overall speeds get slower as the grade gets steeper. Just eyeballing the scatter plot, it looks like some kind of downward sloping curve would be a better fit than a straight line, so I'll fit quadratic (degree two) and cubic (degree 3) polynomials to the data (for no good theoretical reason; just because those are the two simplest nonlinear functions I could think of). I'll also make the plot prettier and bigger:"
"As expected, there is a lot of variance, but overall speeds get slower as the grade gets steeper. Just eyeballing the scatter plot, it looks like some kind of downward sloping curve would be a better fit than a straight line, so I'll fit quadratic (degree two) and cubic (degree 3) polynomials to the data (for no good theoretical reason; just because those are two simple nonlinear functions and numpy has a solver for them). I'll also make the plot prettier and bigger:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
@@ -119,12 +127,12 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAykAAAKSCAYAAADWEJRoAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nOy9f3yT9bn//6KUFimUYltLoRC6IzJw4s50uqFleuTgr0qrrk7H/LG5bGx6tuOm88ciQ41D2Y56vno2WN2U73kwp91cZ6dHz5nTHUGdFmG4qVilBPorpKVJSWISkvTzR8+dNXfuO7mT3Pc771xcz8eDh5Dcyeu+rtf7jvd1v39N8Xq942AYhmEYhmEYhpGEkkKfAMMwDMMwDMMwzGS4SGEYhmEYhmEYRiq4SGEYhmEYhmEYRiq4SGEYhmEYhmEYRiq4SGEYhmEYhmEYRiq4SGEYhmEYhmEYRiq4SGEYhmGKmqqqKlx88cWGj3/llVdQVVWFb3zjGxaeFcMwDJMPXKQwDMMcw2zfvh3HH388qqqqsGHDBs1jqqqqdP+sWrUq5XilCND7o6fDMAzDMAqlhT4BhmEYpjD4fD6sW7cOM2bMgN/vT3tsZWWlZs/D/PnzdT/ziU98QrOH47Of/Wz2J5uGN954A8cdd5yp38kwDMMUFi5SGIZhjlFuueUWBAIBfOc738Hdd9+d9tjZs2fj9ttvz+r7TznllKw/kwsnnXSS5RoMwzCMWHi4F8MwzDFIZ2cnnnrqKfzoRz9CXV1doU9Hl23btqGqqgobN27En//8Z1x22WWw2WyoqqqC1+sFoD8n5dChQ7jxxhuxePFizJ07F2effTa2bduWVi8UCuHhhx/G5z73OcyfPx/z5s3DOeecg1/84hcYHx9POb6rqwtr1qzBkiVLcMIJJ2DJkiU4//zz8W//9m/mJIBhGOYYhXtSGIZhjjEGBwdx0003oaWlBZ///Ocz3rgDQCQSwa9+9Sv09/dj5syZWL58OT7zmc9gypQpup8ZGhrCL37xC4yOjqKmpgaf+cxnsGTJkpzO+Y033sADDzyAFStW4JprrsHg4CCmTp2qe/zhw4exevVq7N+/H2eeeSZWrFiBoaEh3Hzzzfjc5z6n+ZkjR46gtbUVO3fuxPLly/HFL34RAPDiiy/iO9/5Dt5880389Kc/TRz/85//HN/97ndxwgkn4Pzzz0dtbS1GRkawd+9ePPbYY/jud7+bU6wMwzAMFykMwzDHFOPj47jhhhswbdo0PPDAA4Y/53a7sW7duqTXPv7xj2PLli049dRTNT/z0ksv4aWXXkp67YILLsB//Md/oLq6Oqvzfumll/DQQw/huuuuM3T8XXfdhf3798Nut+NHP/pR4vV169ZpTvYHgDvuuAM7d+7Ehg0b8K//+q+J18PhMK6++mo88cQTuOSSS3DRRRcBALZu3YqysjK88sorKb1RIyMjWcXHMAzDJMPDvRiGYY4htmzZgj/+8Y948MEHDRcKN9xwA1544QV8+OGH6Ovrw0svvYSWlha89957aG1tRV9fX9LxNTU12LBhA1599VX09fXhww8/xK9//WssX74czz//PL7whS8gHo9ndd6f+MQnDBcoR48eRUdHByoqKvD9738/6b1TTz0VV1xxRcpnRkdH8cQTT2D58uVJBQoAlJeXY/369QCAJ598MvF6SUkJSktLUVZWlvJ92RZhDMMwTDLck8IwDHOMsHfvXmzYsAFXXnllVvuK3HvvvUn//sd//Eds3boV11xzDZ555hk8/PDDuP/++xPvL126FEuXLk38e+bMmVi1ahXOOOMMNDU1obu7G88++ywuueQSw+dw+umnGz72/fffRzAYxBlnnIGqqqqU988666yUIW47d+5ENBpFSUkJNm7cmPKZaDQKAOjp6Um8dsUVV+COO+7AmWeeiUsvvRQrVqzAmWeeiblz5xo+V4ZhGEYbLlIYhmGOEb72ta+huro6qaDIh6985St45pln8Prrrxs6vrKyEm1tbfjxj3+M119/Pasi5YQTTjB87NjYGACgtrbW8HcdPnwYALB7927s3r1b97snL9X8zW9+E7W1tfj5z3+ORx99FFu2bAEAfPrTn8b69evR1NRk+JwZhmGYZLhIYRiGOUb4y1/+AgCw2Wya7z/00EN46KGHcNFFF+GXv/xlxu+rqakBAASDQcPnkMtnAKSdoK+msrISAODxeDTfP3TokO5nvva1r2HTpk2Gtdra2tDW1oaxsTG8+eabeP7557F161a0tbVh+/btOPHEEw1/F8MwDPN3uEhhGIY5Rrj66qs1X//www/x6quv4uSTT8anPvUpLF++3ND37dy5EwCwaNEiw+eQy2ey5aSTTsKMGTPwt7/9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 truncated
"text/plain": [
"<Figure size 864x720 with 1 Axes>"
]
@@ -134,61 +142,61 @@
}
],
"source": [
"show(fpm, mph)"
"show(feet/miles, miles/hours)"
]
},
{
"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 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."
"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 when the grade is steeper than 50 ft/mile, the 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 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."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Estimating Duration\n",
"# Estimating Time of a Ride\n",
"\n",
"This data can help me predict how long will it take to ride a route with a given distance and elevation. For example, to get to Pescadero from La Honda, I could take the flatter [coast route](https://www.google.com/maps/dir/La+Honda,+California/Pescadero,+California/@37.2905834,-122.3896683,12z/data=!4m19!4m18!1m10!1m1!1s0x808faed4dc6265bd:0x51a109d3306a7219!2m2!1d-122.274227!2d37.3190255!3m4!1m2!1d-122.4039496!2d37.3116594!3s0x808f062b7d7585e7:0x942480c22f110b74!1m5!1m1!1s0x808f00b4b613c4c1:0x43c609077878b77!2m2!1d-122.3830152!2d37.2551636!3e1) (15.7 miles, 344 ft climb), or the shorter [creek route](https://www.google.com/maps/dir/La+Honda,+California/Pescadero,+California/@37.2905834,-122.3896683,12z/data=!4m19!4m18!1m10!1m1!1s0x808faed4dc6265bd:0x51a109d3306a7219!2m2!1d-122.274227!2d37.3190255!3m4!1m2!1d-122.3658887!2d37.2538867!3s0x808f00acf265bd43:0xb7e2a0c9ee355c3a!1m5!1m1!1s0x808f00b4b613c4c1:0x43c609077878b77!2m2!1d-122.3830152!2d37.2551636!3e1) (13.5 miles, 735 ft climb); which is faster?"
]
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"def duration(dist, climb, F=poly_fit(fpm, mph, 2)) -> float:\n",
"def time(dist, climb, F=poly_fit(feet/miles, miles/hours, 2)) -> float:\n",
" \"Given a distance in miles and total climb in feet, estimate time in minutes.\"\n",
" return 60 * dist / F(climb / dist)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'Coast: 69.1 min, Creek: 62.5 min.'"
"'Coast: 69.2 min, Creek: 62.5 min.'"
]
},
"execution_count": 6,
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"f'Coast: {duration(15.7, 344):.1f} min, Creek: {duration(13.5, 735):.1f} min.'"
"f'Coast: {time(15.7, 344):.1f} min, Creek: {time(13.5, 735):.1f} min.'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This suggests the shorter creek route would be about 6 or 7 minutes faster. Good to know, but other factors (like the scenery) are probably more important in making the choice."
"This predicts the shorter creek route would be 6 or 7 minutes faster. Good to know, but other factors (like the scenery) are probably more important in making the choice."
]
},
{
@@ -202,7 +210,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
@@ -212,12 +220,14 @@
" for segment in lines:\n",
" title, mi, ft, *times = segment.split(',')\n",
" for time in times:\n",
" time = hours(*time.split(':')[::-1])\n",
" time = parse_hours(time)\n",
" yield Ride(float(mi), time, int(ft), 'date?', title)\n",
" \n",
"segments = set(parse_segments(open('bikesegments.csv')))\n",
"\n",
"mls2, mph2, fpm2 = vectors3(segments) "
"miles2 = column('miles', segments)\n",
"hours2 = column('hours', segments)\n",
"feet2 = column('feet', segments) "
]
},
{
@@ -229,12 +239,12 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"image/png": "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 truncated
"text/plain": [
"<Figure size 864x720 with 1 Axes>"
]
@@ -244,30 +254,30 @@
}
],
"source": [
"pct2 = [f * 100 / 5280 for f in fpm2]\n",
"pct2 = feet2 / miles2 * 100 / 5280\n",
"\n",
"show(pct2, mph2, xlabel='Segment Grade (percent)') "
"show(pct2, miles2 / hours2, xlabel='Segment Grade (percent)') "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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",
"So I can cruise at 15 to 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). 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):"
"The average 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):"
]
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAzoAAAKSCAYAAAAaia6DAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nOydfXiT9bnHv6WF8k6xQEtHqUUqCsODOmE6cQg430DrUQYbOnVbUdT5chQZs7KhnYpsbrvUOaw725zOeYrag+jUbQKTKSLiy3wZdlpToEnahCYhrWlo0vMHJxmFtCY09y/9Ps/9uS6ua6bpJ8/vkzsZP5I8yfL5fJ1QFEVRFEVRFEWxEP0yfQCKoiiKoiiKoijpRjc6iqIoiqIoiqJYDt3oKIqiKIqiKIpiOXSjoyiKoiiKoiiK5dCNjqIoiqIoiqIolkM3OoqiKIqiKIqiWA7d6CiKoii2Z+rUqZg6dWrS13c4HMjLy8P5558veFSKoihKb9CNjqIoipIS//u//4tly5bh3HPPRXFxMfLy8vDtb3+72+vv3bsXy5cvx9lnn43jjjsOY8aMwcSJEzF79mw89NBDaG1tTep2r732WuTl5SEvLw8fffRRupajKIqiWJScTB+AoiiKwsWaNWvw3nvvYejQoSgqKsK+fft6vL7L5cJjjz2GE088EWeffTby8/Ph9/uxefNmrFixAr/73e/w4osvYsSIEd06NmzYgMcffxxDhw5FMBhM95Kwfv36tDsVRVGUzKIbHUVRFCUl7rrrLnzhC1/AhAkTsGXLFsyfP7/H6x977LFwOBzIyen6fzmdnZ1YsmQJampq8Mgjj+Dmm29O+PtNTU244YYbcPHFF8PlcuHvf/972tYSo7S0NO1ORVEUJbPoW9cURVGUlDjjjDNwzDHHICsrK6nr5+TkHLbJAYCsrCyUl5cDAD7++ONuf/+6665D//798ZOf/OSIjveVV15BXl4eli5din/+85+49NJLMWHCBOTl5eHdd98F0P1ndPbt24cf/OAHmDx5MgoKCnDKKafg/vvvR2dnZ7e3F41G8eijj+Lss8/G+PHjUVBQgFNPPRX33XcfwuFwwuNbuHAhpkyZEn9b36xZs3Dbbbf1eDuKoihKz+grOoqiKErGeOGFFwCg2xMB/Pd//zdeeuklPPnkkxg5cmSvbqu+vh5f+9rXMGnSJCxatAh+vx+DBw/u9vrt7e248MILsWPHDkyePBkLFixAIBDAT3/6025fVero6MCll16KF154ARMnTsTFF1+M3Nxc/P3vf8cdd9yBzZs346mnnopv/F566SUsXLgQw4YNw7nnnosvfOEL8Pl8+Pjjj7F27VqsWrUq4SZRURRF+Xz02VNRFEUxQltbG37xi18AAFpaWvDqq6/ivffew6xZsxKezODjjz9GZWUlLr30Upx99tm9vv2tW7fiv/7rv7By5cqkrv/AAw9gx44dOO+88/DYY4+hX78Db4K46aabMGvWrIS/87Of/QwvvPACKioqcM899yA7OxvAgVd5brrpJvzud7/DI488gquvvhoA8Oijj6KzsxPPPvss/uM//qOLa+/evbrJURRF6QX61jVFURTFCJ999hlWr16N1atX4+GHH8Z7772Hb37zm3jssceQm5vb5bodHR1YsmQJjjrqKNx1111puf0xY8Zg+fLlSV//8ccfR1ZWFlatWhXf5ADA+PHjcdVVVx12/Wg0il/96lcYPXo07r777vgmBwD69euHO+64A1lZWXjyySe7XA4g4StLRx11VNLHqiiKohyO/lORoiiKYoT8/Hz4fD50dnbC6XRi48aNuPPOOzFr1iysW7cORx99dPy69957L3bs2IHa2loMHz48Lbf/xS9+8bANVXfs27cPn3zyCQoLC1FWVnbYz7/yla8cdtm//vUveL1elJaWYs2aNQm9gwYNQl1dXfy/v/71r2P9+vWYM2cOLrroIsycOROnnHIKSkpKklyVoiiK0h260VEURVGMkpWVhaKiIixevBiTJk3C3LlzcfPNN+Opp54CALzzzju477778N3vfhdf/epX03a7Y8aMSfq6gUAAADB69OikXXv37gVw4LNAq1evTup25s2bh6eeegr3338/nnjiCfzud78DAEyePBnLly/HhRdemPQxK4qiKF3Rt64piqIoGeNLX/oS8vLy8Pe//z1+hrH33nsPHR0dqK6ujn9BaOxP7CQA06dPR15eHjZs2JD0bSV7ljgA8VeRmpubE/68qamp298555xz4PP5evxzMHPmzEFtbS0cDgc2bNiAG2+8Ebt27cIVV1yBLVu2JH3MiqIoSlf0FR1FURQlY+zbtw+BQACDBg2Kb0QmTpyIyy67LOH1X3rpJbjdblx44YUYPnw4xo8fL3Jcw4YNw4QJE1BfX49//etfmDhxYpefJzrr2rHHHosRI0bgzTffRDgcxoABA1K6zUGDBuH000/H6aefjqOPPho33ngjnn/+eZx++um9WouiKIpd0Y2OoiiKIspbb72FiRMnYtiwYV0uD4fDWLZsGaLRaJezqs2YMQMzZsxI6Dr//PPhdrtx22234dhjjxU97sWLF+POO+/EypUru5x1raGhAWvXrj3s+jk5Obj66quxevVq3HLLLbjnnnsOO8mA1+vFnj17cMIJJwAANm3ahOnTpx92PbfbDQAYOHCgxNIURVFsgW50FEVRlJTYsGEDnnvuOQD/fgvX9u3bsXTp0vh1Hnroofj/fvjhh7F+/XqcfvrpKC4uxtChQ+MnI2hqasLEiRPx4x//2OwikuC6667Dc889h+effx4zZ87E3LlzEQgE8Mwzz+DUU0/Fn/70p8N+Z9myZfjggw/w6KOP4qWXXsIZZ5yBL3zhC/B4PKivr8fWrVvx3e9+N77RqaysRENDA77yla9g/PjxGDhwIN5//3389a9/xVFHHYXLL7/c9LIVRVEsg250FEVRlJT4xz/+gSeeeKLLZQ0NDWhoaIj/98EbnUWLFgEA3nzzTWzduhWtra0YNmwYjjvuOFx//fX49re/3eMXd2aK3Nxc1NbW4p577sEzzzyDX/3qVxg/fjxuvvlmzJ8/P+FGJycnB48++iieeuopPP744/jzn/+MYDCIo446CsXFxbjpppviPQDg5ptvxnPPPYe33noLr7zyCgCgqKgIS5cuxTXXXINx48YZW6+iKIrVyPL5fJ2ZPghFURRFURRFUZR0omddUxRFURRFURTFcuhGR1EURVEURVEUy6EbHUVRFEVRFEVRLIdudBRFURRFURRFsRy60VEURVEURVEUxXLoRkdRFEVRFEVRFMuhGx1FURRFURRFUSyHbnSEcbvdtP66ujoxN3MX6WNn7c58n0o2B7R7d7DOurRfn2PMu6X9+hxj3g3wzrq0307ddaMjTH5+PrVfCuYurM0B7i7a3bzbhF8K5i6szQHuLtrdWm5pmOfRTt11oyNMNBql9kvB3IW1OcDdRbubd5vwS8HchbU5wN1Fu1vLLQ3zPNqpe8Y2OtXV1TjttNNQXFyM4uJinHXWWXjxxRfjP1+6dCny8vK6/Jk7d24XR3t7O5YtW4YJEyagqKgIixYtwp49e7pcZ9euXVi4cCGKioowYcIE3HrrrQiHw0bWCAB+v5/aLwVzF9bmAHcX7W7ebcIvBXMX1uYAdxftbi23NMzzaKfuOULH8bkUFRVh1apVOOaYYxCNRvHEE09g8eLF2LRpE774xS8CAGbNmoW1a9fGf2fAgAFdHCtWrMDzzz+PX//61xg5ciRuu+02LFy4EJs3b0Z2djYikQgWLlyIkSNH4vnnn0dLSwuWLl2Kzs5OrFmzxsg6R48eTe2XgrkLa3OAu4t2N+824ZeCuQtrc4C7i3a3llsa5nm0U/eMvaJz/vnn46yzzsKECRMwceJE3H777Rg6dCjeeOON+HVyc3NRUFAQ/zNy5Mj4z/x+P37/+9/jjjvuwJlnnolp06Zh7dq1eP/997Fp0yYAwMsvv4wPP/wQa9euxbRp03DmmWdi1apVePTRRxEIBIysk3nHLwlzF9bmAHcX7W7ebcIvBXMX1uYAdxftbi23NMzzaKfufeIzOpFIBE899RRaW1sxffr0+OWvvfYaJk6ciJNPPhnXX389mpub4z97++23sX//fsyePTt+2bhx4zBp0iS8/vrrAIBt27Zh0qRJGDduXPw6c+bMQXt7O95++20DKwOys7Op/VIwd2FtDnB30e7m3Sb8UjB3YW0OcHfR7tZyS8M8j3bqnrG3rgHA+++/j6997WsIhUIYMmQIHnvsMUyZMgUAMHfuXMyfPx8lJSVoaGhAVVUVLrjgAmzatAm5ubloampCdnb2YWdfGD16NJqamgAATU1Nh73ElZ+fj+zs7Ph1pBk6dCi1XwrmLqzNAe4u2t2824RfCuYurM0B7i7a3VpuaZjn0U7dM7rRKSsrwyuvvAK/34/169dj6dKl2LBhAyZPnoyLL744fr0pU6Zg2rRpmDp1Kl588UVccMEF3To7OzuRlZUV/++D//fBdHd5DOlz3rOgHTKDdjePNs8M2j0zaHfzaPPMoN0zg8nuZWVl3f4soxudAQMGYMKECQCAE088ETt27MAvf/lLPPDAA4ddd+zYsSgqKsInn3wCABgzZgwikQi8Xi9GjRoVv57H48Fpp50Wv07sbWwxvF4vIpHI536YqadoqRCJRERfIpT019XVpa3DoTB3kT521u7M96lkc0C7dwfrrEv79TnGvFvar88x5t0A76xL++3UvU98RidGNBrt9tTPXq8XTqcTBQUFAIBp06ahf//+2LhxY/w6e/bswc6dOzFjxgwAwPTp07Fz584up5zeuHEjcnNzMW3aNMGV/JuOjg5qvxTMXVibA9xdtLt5twm/FMxdWJsD3F20u7Xc0jDPo526Z2yj86Mf/QivvvoqHA4H3n//faxatQpbtmzBggULEAwGUVlLine truncated
"text/plain": [
"<Figure size 864x720 with 1 Axes>"
]
@@ -277,9 +287,7 @@
}
],
"source": [
"vam2 = [r.feet / r.hours for r in segments]\n",
"\n",
"show(pct2, vam2, xlabel='Grade (percent)', ylabel='VAM (vertical feet per hour)')"
"show(pct2, feet2 / hours2, xlabel='Grade (percent)', ylabel='VAM (vertical feet per hour)')"
]
},
{
@@ -294,12 +302,12 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"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
"image/png": "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 truncated
"text/plain": [
"<Figure size 864x720 with 1 Axes>"
]
@@ -309,7 +317,7 @@
}
],
"source": [
"show(mph2, vam2, xlabel='Speed(mph)', ylabel='VAM (vertical feet per hour)')"
"show(miles2 / hours2, feet2 / hours2, xlabel='Speed(mph)', ylabel='VAM (vertical feet per hour)')"
]
},
{
@@ -322,17 +330,17 @@
"\n",
"> *Your **hill-index** is the maximum number**&nbsp;h** for which you can regularly climb an**&nbsp;h** percent grade at **&nbsp;h** miles per hour.*\n",
"\n",
"I think **my hill-index is 6**, but let's draw a plot to confirm:"
"I think **my hill-index is 6**. (I know that [Old La Honda](https://graphics.stanford.edu/~lucasp/grade/oldlahonda.html) is a 7% grade, and I only [achieved 7.0 mph](https://www.strava.com/segments/9343401?filter=my_results&gender=all) on it once out of many tries). But let's draw a plot to confirm:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 12,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAykAAAKSCAYAAADWEJRoAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nOydfVhc5Zn/vzMMEN4CCWIgMRnRYDTdaqt9WTWkrloTlRh8QW3VbWvFt6ZVq67dlupWqUa7W7uXtmqx7WqN1lIVjbqpbrdqEvVXbdSoXS1ROiSBmQCZF4bJzDAvvz9waIDhMHM49/3MM96f6/K6Wpj58Dyf8zDyOHPOsfl8viQEQRAEQRAEQRByBLvqAQiCIAiCIAiCIOyPbFIEQRAEQRAEQcgpZJMiCIIgCIIgCEJOIZsUQRAEQRAEQRByCtmkCIIgCIIgCIKQU8gmRRAEQRAEQRCEnEI2KYIgCILWfPKTn8QnP/nJjB/vcrlQVVWF008/nXBUgiAIwmyQTYogCMLHiCeffBLXX389Tj31VCxevBhVVVW4+OKLp3383r17ccMNN2DVqlU4/PDDceCBB2Lp0qU48cQTcc8992BkZCSjn/uNb3wDVVVVqKqqwl//+lerpiMIgiDkKQ7VAxAEQRD4+NGPfoR33nkH5eXlWLhwIYaHhw0f73a78dBDD+HTn/40Vq1aherqavj9frz44ov413/9VzzwwAP4/e9/j8rKymkdTz/9NDZs2IDy8nIEg0Grp4SnnnrKcqcgCIKgFtmkCIIgfIy49dZbsWjRIhxyyCHYsmUL1qxZY/j4ww47DC6XCw7HxH9dJJNJXHrppejs7MT999+Pa6+9Nu3z9+zZg6uuugpnn3023G43tm7datlcUtTX11vuFARBENQiH/cSBEH4GLFy5UoceuihsNlsGT3e4XBM2aAAgM1mQ3NzMwDggw8+mPb569atQ2FhIf793//d1Hg3b96MqqoqXHHFFXjvvfdw4YUX4pBDDkFVVRW2b98OYPpzUoaHh/Hd734Xy5cvx4IFC/DZz34Wd911F5LJ5LQ/L5FI4MEHH8SqVauwZMkSLFiwAMceeyx+/OMfIxqNph3feeedh0984hPjH4U74YQT8L3vfc/w5wiCIAjGyDspgiAIgik2bdoEANOetP7LX/4Szz33HB599FHMmzdvVj+rp6cHp5xyCpYtW4bzzz8ffr8fpaWl0z4+Eolg7dq12LZtG5YvX46WlhYEAgH8x3/8x7Tv5sRiMVx44YXYtGkTli5dirPPPhvFxcXYunUrbr75Zrz44ot47LHHxjdtzz33HM477zxUVFTg1FNPxaJFi+Dz+fDBBx/gvvvuww9+8IO0GzxBEARhZuTVUxAEQZiRUCiE//zP/wQAeL1evPzyy3jnnXdwwgknpD3x/oMPPkBbWxsuvPBCrFq1atY//9VXX8W3v/1t3HjjjRk9/u6778a2bdtw2mmn4aGHHoLdPvbBgWuuuQYnnHBC2ufceeed2LRpE1pbW7F+/XoUFBQAGHt35ZprrsEDDzyA+++/H5dffjkA4MEHH0QymcTGjRtx1FFHTXDt3btXNiiCIAizQD7uJQiCIMzIvn37cPvtt+P222/Hz3/+c7zzzjv48pe/jIceegjFxcUTHhuLxXDppZdi/vz5uPXWWy35+QceeCBuuOGGjB+/YcMG2Gw2/OAHPxjfoADAkiVLcNlll015fCKRwL333ouamhrcdttt4xsUALDb7bj55pths9nw6KOPTvg6gLTv6MyfPz/jsQqCIAhTkf/MIwiCIMxIdXU1fD4fkskk+vv78cc//hG33HILTjjhBPzud7/DwQcfPP7YO+64A9u2bUNXVxfmzp1ryc//h3/4hymboekYHh7Ghx9+iNraWjQ0NEz5/vHHHz/lazt27MDQ0BDq6+vxox/9KK23pKQE3d3d4///3HPPxVNPPYWTTjoJZ555JhobG/HZz34WTqczw1kJgiAI0yGbFEEQBCFjbDYbFi5ciAsuuADLli3DySefjGuvvRaPPfYYAOCtt97Cj3/8Y1xyySX4whe+YNnPPfDAAzN+bCAQAADU1NRk7Nq7dy+AsXNfbr/99ox+TlNTEx577DHcddddeOSRR/DAAw8AAJYvX44bbrgBa9euzXjMgiAIwkTk416CIAiCKT7zmc+gqqoKW7duHb+S1TvvvINYLIaOjo7xmzem/kmdsP65z30OVVVVePrppzP+WZlejQzA+Ls3AwMDab+/Z8+eaZ+zevVq+Hw+w3/256STTkJXVxdcLheefvppXH311di5cye++tWvYsuWLRmPWRAEQZiIvJMiCIIgmGJ4eBiBQAAlJSXjm4ilS5fioosuSvv45557Dh6PB2vXrsXcuXOxZMkSknFVVFTgkEMOQU9PD3bs2IGlS5dO+H66q3sddthhqKysxJ///GdEo1EUFRVl9TNLSkqwYsUKrFixAgcffDCuvvpqPPvss1ixYsWs5iIIgvBxRTYpgiAIwrS88cYbWLp0KSoqKiZ8PRqN4vrrr0cikZhw9a7Pf/7z+PznP5/Wdfrpp8Pj8eB73/seDjvsMNJxX3DBBbjllltw4403Tri6V29vL+67774pj3c4HLj88stx++2347rrrsP69eunnBA/NDSE3bt348gjjwQAvPDCC/jc5z435XEejwcAMGfOHIqpCYIgfCyQTYogCMLHiKeffhrPPPMMgL9/7On111/HFVdcMf6Ye+65Z/x///znP8dTTz2FFStWYPHixSgvLx8/cX7Pnj1YunQpfvjDH/JOIgPWrVuHZ555Bs8++ywaGxtx8sknIxAI4IknnsCxxx6L//7v/57ynOuvvx5/+ctf8OCDD+K5557DypUrsWjRIgwODqKnpwevvvoqLrnkkvFNSltbG3p7e3H88cdjyZIlmDNnDt5991384Q9/wPz58/GVr3yFe9qCIAh5g2xSBEEQPka8/fbbeOSRRyZ8rbe3F729veP/f/9Nyvnnnw8A+POf/4xXX30VIyMjqKiowOGHH45vfetbuPjiiw1vqqiK4uJidHV1Yf369XjiiSdw7733YsmSJbj22muxZs2atJsUh8OBBx98EI899hg2bNiA559/HsFgEPPnz8fixYtxzTXXjPcAgGuvvRbPPPMM3njjDWzevBkAsHDhQlxxxRW48sorcdBBB7HNVxAEId+w+Xy+pOpBCIIgCIIgCIIgpJCrewmCIAiCIAiCkFPIJkUQBEEQBEEQhJxCNimCIAiCIAiCIOQUskkRBEEQBEEQBCGnkE2KIAiCIAiCIAg5hWxSBEEQBEEQBEHIKWSTIgiCIAiCIAhCTiGbFAM8Ho+WbgDo7u4mc+vchdJP2RzQt7usdTV+Wev8bmq/dOd3A/Iao8Ita12NP9fWumxSDKiurtbSTY3OXaR7frmp0Xk9Snd+N4efEl27S3M1funO7+bwU2Fm3LJJMSCRSGjppkbnLtI9v9zU6LwepTu/m8NPia7dpbkav3Tnd3P4qTAz7rzcpHg8HsRiMUSjUQwMDAAA/H4/gsEgAMDtdiMejyMSiWBwcBAA4PP5MDIyAgDo7+9HIpHA0NAQhoaGAABerxehUAgA0NfXBwAIhULwer0AgKGhIYTDYSQSCfT39wMARkZG4PP5AACDg4OIRCKIx+Nwu93j4/H7/QCAgYEBRKNRxGKx8bfEAoEAAoGApXMKh8OzntOePXvSzgnArOe0Z88e0jl5PJ6sjpMVc7LqOPn9fkvWXro57d27l2xOfX19lq29dHNKQXGc9u7dS/r7NDg4SLb2/H4/2WtECivWXro5pY4Jxe+Tx+Ox9LV88pz6+/u1eS2fPCev10v2uuf1esnmlPo+1Wt5CorjNDQ0ZOlr+eQ5DQwMkK09v99v+d9GqTmloPp3bup/U/w+ud1u0r8j+vr6yF4jUs/P9DhlMyev1ztlTjNh8/l8yRkfJWhHd3c3GhoaVA/jY4U0V4N050eaq0G6q0G68yPN1ZBr3fPynRSryGSXl4tuanTuIt3zy02NzutRuvO7OfyU6NpdmqvxS3d+N4efCjPjlk2KAQUFBVq6qdG5i3TPLzc1Oq9H6c7v5vBTomt3aa7GL9353Rx+KsyMWzYpBpSXl2vppkbnLtI9v9zU6LwepTu/m8NPia7dpbkav3Tnd3P4qTAzbtmkGJA6OUg3NzU6d5Hu+eWmRuf1KN353Rx+SnTtLs3V+KU7v5vDT4WZccsmxYCamhot3dTo3EW655ebGp3Xo3Tnd3P4KdG1uzRX45fu/G4OPxVmxi2bFANisZiWbmp07iLd88tNjc7rUbrzuzn8lOjaXZqr8Ut3fjeHnwoz45ZNigHDw8NauqnRuYt0zy83NTqvR+nO7+bwU6Jrd2muxi/d+d0cfirMjFs2KQYccMABWrqp0bmLdM8vNzU6r0fpzu/m8FOia3dprsYv3fndHH4qzIxbNikGpO60qZubGp27SPf8clOj83qU7vxuDj8lunaX5mr80p3fzeGnwsy4ZZNiQGFhoZZuanTuIt3zy02NzutRuvO7OfyU6NpdmqvxS3d+N4efCjPjlk2KAWVlZVq6qdG5i3TPLzc1Oq9H6c7v5vBTomt3aa7GL9353Rx+KsyMWzYpBvT392vppkbnLtI9v9zU6LwepTu/m8NPia7dpbkav3Tnd3P4qTAzbtmkGLBgwQIt3dTo3EW655ebGp3Xo3Tnd3P4KdG1uzRX45fu/G4OPxVmxi2bFAOi0aiWbmp07iLd88tNjc7rUbrzuzn8lOjaXZqr8Ut3fjeHnwoz45ZNigEjIyNauqnRuYt0zy83NTqvR+nO7+bwU6Jrd2muxi/d+d0cfirMjDsvNykejwexWAzRaBQDAwMAAL/fj2AwCABwu92Ix+OIRCIYHBwEMHZptFTA/v5+JBIJlJWVYWhoCADg9XoRCoUAAH19fQCAUCgEr9cLABgLine truncated
"text/plain": [
"<Figure size 864x720 with 1 Axes>"
]
@@ -343,7 +351,7 @@
],
"source": [
"points = range(2, 8)\n",
"show(pct2, mph2, xlabel='Grade (percent)') \n",
"show(pct2, miles2 / hours2, xlabel='Grade (percent)') \n",
"plt.plot(points, points, 'gD:', lw=2);"
]
},
@@ -373,16 +381,16 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 13,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"56"
"57"
]
},
"execution_count": 12,
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
@@ -390,70 +398,19 @@
"source": [
"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",
" distances = sorted(distances, reverse=True)\n",
" return max(e for e, d in enumerate(distances, 1) if d >= e)\n",
"\n",
"def days_at_least(e, distances) -> int: return sum(d >= e for d in distances)\n",
"\n",
"Eddington(mls)"
"Eddington(miles)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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",
"My **Eddington Number is 57**: I've done at least 57 miles on at least 57 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 **84**, and his available roads, bicycles, and navigation aids were not nearly as nice as mine, so good for him.\n",
"\n",
"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&mdash;what I'll call the *gap*&mdash;for increasingly ambitious Eddington numbers:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"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": [
"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": [
"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",
"An easier way to get a bigger Eddington number is to use kilometers rather than miles:"
"How many more rides would I need to improve my number? Obviously, for anyone to improve from, say, 57 to 60 they need at least 3 more rides. But any rides that were, say, 58 or 59 miles count for 57 but wouldn't count for 60; thus more than 3 rides may be needed. Here is a table of the number of rides I would need&mdash;what I'll call the *gap*&mdash;for increasingly ambitious Eddington numbers:"
]
},
{
@@ -462,25 +419,75 @@
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"80"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
"name": "stdout",
"output_type": "stream",
"text": [
"Dist Days Gap\n",
"---- ---- ---\n",
" 57 58 -1\n",
" 58 55 3\n",
" 59 50 9\n",
" 60 49 11\n",
" 61 43 18\n",
" 62 41 21\n",
" 63 39 24\n",
" 64 37 27\n",
" 65 35 30\n",
" 66 33 33\n",
" 67 28 39\n",
" 68 26 42\n",
" 69 21 48\n",
" 70 20 50\n"
]
}
],
"source": [
"kms = [m * 5280 * 12 * 2.54 / 100 / 1000 for m in mls]\n",
"Eddington(kms)"
"def Egaps(distances, targets):\n",
" \"\"\"Print the number of days doing various target distances, and the Eddington gap.\"\"\"\n",
" print('Dist Days Gap')\n",
" print('---- ---- ---')\n",
" for target in targets:\n",
" days = sum(d >= target for d in distances)\n",
" print(f'{target:4} {days:4} {target - days:3}')\n",
" \n",
"Egaps(miles, range(57, 71))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I'd like to get my Eddington number to meet or beat my age. If I do two 65+ mile rides per month, I'll make it before I turn 65.\n",
"\n",
"An easy way to get bigger Eddington numbers is to use kilometers rather than miles:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"81"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"kms = miles * 1.609344\n",
"\n",
"Eddington(kms)"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
@@ -488,40 +495,91 @@
"text": [
"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"
" 81 82 -1\n",
" 82 74 8\n",
" 83 73 10\n",
" 84 68 16\n",
" 85 67 18\n",
" 86 66 20\n",
" 87 64 23\n",
" 88 63 25\n",
" 89 62 27\n",
" 90 61 29\n",
" 91 59 32\n",
" 92 58 34\n",
" 93 55 38\n",
" 94 53 41\n",
" 95 50 45\n",
" 96 49 47\n",
" 97 46 51\n",
" 98 43 55\n",
" 99 41 58\n",
" 100 41 59\n"
]
}
],
"source": [
"Egaps(kms, range(80, 101))"
"Egaps(kms, range(81, 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."
"My **metric Eddington Number is 81**, and I should be able to get to 90 in about a year, and 100 in two or three years, assuming around two 100+ km rides per month.\n",
"\n",
"Here is my Eddington number progress over the years, in miles and kms:"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Year E_ml E_km\n",
"---- ----- -----\n",
"2013 23 27\n",
"2014 34 45\n",
"2015 41 60\n",
"2016 46 65\n",
"2017 50 72\n",
"2018 53 75\n",
"2019 54 78\n",
"2020 57 81\n"
]
}
],
"source": [
"def year(ride) -> int: return int(ride.date[-4:])\n",
"\n",
"def Eprogress(years, rides=rides):\n",
" \"Print a table of Eddington numbers by year\"\n",
" print('Year E_ml E_km')\n",
" print('---- ----- -----')\n",
" for y in years:\n",
" miles = np.array([r.miles for r in rides if year(r) <= y])\n",
" print(f'{y} {Eddington(miles):5} {Eddington(miles * 1.609344):5}')\n",
" \n",
"Eprogress(range(2013, 2021))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"*Note:* the definition of Eddington Number seems to be mathematically precise, but there is an ambiguity: what does *day* mean? Does it mean daytime (sunrise to sunset)? Or calendar day (midnight to midnight)? Or any 24 hour period? I originally assumed it meant calendar date, but I wanted to make an exception for times when I rode with my gear to a camping site, spent the night there, and rode back home the next morning, what bikepackers call a [sub 24 hour overnight (S24O)](https://oneofsevenproject.com/s24o-bikepacking-guide/) ride. And then COVID struck, the camping sites closed, and I further relaxed the definition to allow an S24O ride where I sleep in my own home. I realize Eddington had a lot more hardships than we have (e.g. World War I and World War II), but I hope he would approve of this slight accomodation."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -540,9 +598,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.2"
"version": "3.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}
+18 -14
View File
@@ -1,8 +1,14 @@
#Ride Date Title mi ft
# 2020 Eddington rides only
# 2020 Eddington rides only; most recent first
Ride Sun, 6/28/2020 Saratoga 5:31:44 71.01 mi 1,063 ft
Ride Sun, 6/21/2020 Sawyer Camp Trail 6:35:20 79.78 mi 1,738 ft
Ride Sun, 6/13/2020 Canada Rd 5:39:40 70.73 mi 1,841 ft
Ride Fri, 2/28/2020 Sawyer Camp Trail 6:24:49 84.43 mi 3,448 ft
Ride Wed, 2/12/2020 Bay Trail 5:25:09 66.02 mi 572 ft
Ride Fri, 2/7/2020 Bay Trail / Cargill 6:07:23 74.04 mi 1,814 ft
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
# 2019 Mostly Eddington; some 25+ mile rides
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
@@ -147,12 +153,10 @@ 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
@@ -204,7 +208,6 @@ 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
@@ -259,7 +262,7 @@ 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 Sat, 11/7/2015 Crystal Springs Part 1 2:15:00 32.00 mi 1000 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
@@ -293,7 +296,6 @@ 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
@@ -311,7 +313,6 @@ 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
@@ -364,7 +365,7 @@ 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 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
@@ -449,8 +450,11 @@ 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
# 2013: Rides in the 20 to 25 mile range
Ride Sat, 6/1/2013 Woodside Loop 1:41:20 24.45 mi 1,125 ft
Ride Sun, 6/16/2013 Woodside Loop 1:38:19 24.52 mi 1,137 ft
Ride Sun, 6/23/2013 Climb 2:16:29 24.30 mi 2,001 ft
Ride Sat, 7/13/2013 Doug's Event 1:51:55 21.35 mi 1,677 ft
Ride Sun, 8/4/2013 Kris's first trike ride 1:51:21 20.96 mi 988 ft
Ride Sun, 11/24/2013 Alpine Rd 1:42:36 21.02 mi 1,289 ft
Ride Fri, 11/29/2013 Woodside Loop 1:33:36 22.75 mi 1,011 ft
1 #Ride Date Title mi ft
2 # 2020 Eddington rides only 2020 Eddington rides only; most recent first
3 Ride Sun, 6/28/2020 Saratoga 5:31:44 71.01 mi
4 Ride Sun, 6/21/2020 Sawyer Camp Trail 6:35:20 79.78 mi
5 Ride Sun, 6/13/2020 Canada Rd 5:39:40 70.73 mi
6 Ride Fri, 2/28/2020 Sawyer Camp Trail 6:24:49 84.43 mi
7 Ride Wed, 2/12/2020 Bay Trail 5:25:09 66.02 mi
8 Ride Fri, 2/7/2020 Bay Trail / Cargill 6:07:23 74.04 mi
9 Ride Sun, 1/26/2020 Los Gatos / Bay 5:18:15 67.98 mi
10 Ride Fri, 1/31/2020 Bay Trail / Guadalupe / Geocaching 5:21:39 69.39 mi
11 # 2019 Eddington rides only 2019 Mostly Eddington; some 25+ mile rides
12 Ride Sun, 6/2/2019 The Sequoia 6:40:43 77.51 mi
13 Ride Sat, 6/8/2019 Morning Ride 2:53:08 34.42 mi
14 Ride Sat, 6/15/2019 Morning Ride 2:29:38 29.98 mi
153 Ride Sat, 12/17/2016 Morning Ride 3:11:31 40.10 mi
154 Ride Fri, 1/12/2018 Morning Ride 2:58:30 40.09 mi
155 Ride Sun, 12/18/2016 Morning Ride 3:04:01 40.03 mi
Ride Sat, 6/11/2016 Ring of Fire (Partial; Garmin troubles) 3:00:00 40.00 mi
156 Ride Sun, 1/25/2015 Canada Rd 2:45:51 39.99 mi
157 Ride Sat, 6/21/2014 Sierra to the Sea Day 7 3:36:53 39.77 mi
158 Ride Sat, 9/23/2017 Canada / Crestview 3:25:13 39.77 mi
159 Ride Fri, 12/27/2013 OLH to Page Mill 3:24:14 39.73 mi
Ride Sat, 4/30/2016 Ridge to Bridge MTB 5:40:57 39.67 mi
160 Ride Fri, 10/30/2015 OLH - West Alpine 3:28:46 39.51 mi
161 Ride Sat, 9/5/2015 Ring of Fire 3:31:55 39.18 mi
162 Ride Wed, 8/28/2013 Train Track Crash 2:50:55 39.16 mi
208 Ride Fri, 3/30/2018 Morning Ride 2:38:07 36.11 mi
209 Ride Sun, 11/5/2017 Lunch Ride 3:24:01 35.91 mi
210 Ride Sun, 2/8/2015 Commute 2:28:46 35.85 mi
Ride Tue, 6/26/2018 Return from Chabot Lake with Juliet MTB 3:21:30 35.81 mi
211 Ride Sat, 3/30/2019 Morning Ride 3:23:55 35.78 mi
212 Ride Sun, 11/22/2015 Pigeon Point 3:14:41 35.73 mi
213 Ride Mon, 9/19/2016 Sunnyvale 2:37:04 35.68 mi
262 Ride Sun, 4/8/2018 Lunch Ride 2:21:54 32.18 mi
263 Ride Sun, 5/3/2015 Canada Rd 2:12:24 32.02 mi
264 Ride Wed, 6/17/2015 06/17/2015 Mountain View, California 2:13:42 32.01 mi
265 Ride Sat, 11/7/2015 Crystal Springs Part 1 2:15:00 32.00 mi
266 Ride Sun, 8/23/2015 Afternoon Ride 2:24:29 31.90 mi
267 Ride Sun, 4/12/2015 Palo Alto Cycling 2:01:49 31.76 mi
268 Ride Sun, 11/13/2016 Afternoon Ride 2:23:46 31.71 mi
296 Ride Mon, 1/30/2017 Morning Ride 2:19:47 30.49 mi
297 Ride Sun, 8/27/2017 Lunch Ride 2:16:36 30.47 mi
298 Ride Fri, 10/12/2018 Lunch Ride 2:16:25 30.44 mi
Ride Mon, 6/25/2018 Chabot Lake Overnight Bike packing with Juliet MTB 3:02:49 30.41 mi
299 Ride Sat, 2/28/2015 Palo Alto Cycling 2:40:24 30.37 mi
300 Ride Sat, 6/24/2017 Afternoon Ride 2:06:05 30.31 mi
301 Ride Tue, 6/2/2015 06/02/2015 Mountain View, California 2:09:58 30.30 mi
313 Ride Tue, 11/22/2016 Morning Ride 2:18:57 30.04 mi
314 Ride Wed, 3/4/2015 Commute 2:15:56 30.02 mi
315 Ride Sun, 3/25/2018 Dumbarton / Coyote Hills / Middle of the Bay 2:31:51 30.01 mi
Ride Tue, 5/31/2016 Google Bike around campus 2:30:00 30.00 mi
316 Ride Tue, 2/14/2017 Morning Ride 2:22:16 29.92 mi
317 Ride Sun, 5/31/2015 05/31/2015 Palo Alto, California 2:35:24 29.85 mi
318 Ride Tue, 11/1/2016 Morning Ride 2:15:55 29.78 mi
365 Ride Sat, 1/31/2015 Alpine Rd 2:29:26 26.98 mi
366 Ride Sun, 2/28/2016 Woodside Loop 1:43:34 26.93 mi
367 Ride Sun, 4/1/2018 Lunch Ride 2:30:35 26.92 mi
368 Workout Ride Wed, 5/3/2017 Palo Alto Road Cycling 2:06:05 26.86 mi
369 Ride Fri, 9/9/2016 Morning Ride 1:56:44 26.85 mi
370 Ride Sun, 9/23/2018 Morning Ride 1:57:48 26.83 mi
371 Ride Sat, 10/4/2014 Woodside Loop 1:52:09 26.71 mi
450 Ride Sun, 1/19/2014 01/19/2014 Palo Alto, CA 1:36:55 25.01 mi
451 Ride Mon, 9/5/2016 Afternoon Ride 1:42:56 25.00 mi
452 Ride Sat, 11/8/2014 Atherton - Woodside 1:40:51 25.00 mi
453 Ride # 2013: Rides in the 20 to 25 mile range
454 Ride Sun, 6/2/2019 Sat, 6/1/2013 The Sequoia Woodside Loop 6:40:43 1:41:20 77.51 mi 24.45 mi
455 Ride Sat, 6/8/2019 Sun, 6/16/2013 Morning Ride Woodside Loop 2:53:08 1:38:19 34.42 mi 24.52 mi
456 Ride Sat, 6/15/2019 Sun, 6/23/2013 Morning Ride Climb 2:29:38 2:16:29 29.98 mi 24.30 mi
457 Ride Fri, 7/5/2019 Sat, 7/13/2013 Morning Ride Doug's Event 4:30:55 1:51:55 64.05 mi 21.35 mi
458 Ride Sun, 8/4/2013 Kris's first trike ride 1:51:21 20.96 mi
459 Ride Sun, 11/24/2013 Alpine Rd 1:42:36 21.02 mi
460 Ride Fri, 11/29/2013 Woodside Loop 1:33:36 22.75 mi
+595
View File
@@ -0,0 +1,595 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div align=\"right\" style=\"text-align: right\"><i>Peter Norvig<br>2012, 2020</i></div>\n",
"\n",
"# The Unfinished Game ... of Risk\n",
"\n",
"![](components_.jpg)\n",
"\n",
"[Keith Devlin](https://web.stanford.edu/~kdevlin/)'s [book](https://www.amazon.com/Unfinished-Game-Pascal-Fermat-Seventeenth-Century/dp/0465018963) [*The Unfinished Game*](https://wordplay.blogs.nytimes.com/2015/12/14/devlin-unfinished-game/) describes how Fermat and Pascal discovered the rules of probability that guide gambling in games. The question they confront is: what if a gambling game is interrupted, but one player is in the lead by a certain score. How much of the pot should the leader get?\n",
"\n",
"My friends and I faced a similar question when a game of [Risk](https://www.ultraboardgames.com/risk/game-rules.php) ran on too long (as they often do) and we were unable to finish. Player **A** had just cashed in cards and added troops to a single large force in Brazil that was poised to make a sweeping attack on player **B**, whose territories were situated in Africa and Asia in such a way that **A** could attack North Africa first and then go from one territory to the next without ever having to branch off. We wrote down the number of **A**'s armies in Brazil, **72**, and the number of armies in **B**'s successive territories: **22, 8, 2, 2, 2, 7, 1, 1, 3, 1, 2, 3, 5, 1.** What is the probability that **A** can capture all these territories?\n",
"\n",
"______\n",
"\n",
"# Monte Carlo Simulation\n",
"\n",
"We can answer the question with a [**Monte Carlo simulation**](https://en.wikipedia.org/wiki/Monte_Carlo_method) that follows the rules of Risk and uses random numbers to roll the dice. First some preliminaries:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from typing import List, Tuple\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import random\n",
"\n",
"attackers = 72\n",
"territories = (22, 8, 2, 2, 2, 7, 1, 1, 3, 1, 2, 3, 5, 1)\n",
"\n",
"die = (1, 2, 3, 4, 5, 6)\n",
"Die = int # a type alias"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"How many territories and how many total defenders?"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(14, 60)"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(territories), sum(territories)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"It looks fairly even: the attackers have 12 more armies, 72 to 60, but they will have to leave an army in each of the 14 territories along the way. \n",
"\n",
"In Risk a **battle** consists of a one-time roll of some dice and the resulting loss of armies. The attacker will roll three dice if possible (but can roll a maximum of the number of armies in the attacking territory minus one) and the defender will roll two dice if possible (or only one if they have only one army remaining). We compare the highest die on each side, with the defender losing an army if the attacker's die is higher, and the attacker losing an army if tied or lower. Then if both sides rolled at least two dice, we do the same comparison with the second highest die on each side. The function `deaths` returns a tuple of (number of attacking armies that die, number of defending armies that die). "
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"def deaths(attack_dice: List[Die], defend_dice: List[Die]) -> Tuple[int, int]:\n",
" \"\"\"The number of (attackers, defenders) who die due to this roll.\"\"\"\n",
" dead = [0, 0]\n",
" for a, d in zip(sorted(attack_dice, reverse=True), \n",
" sorted(defend_dice, reverse=True)):\n",
" dead[a > d] += 1\n",
" return tuple(dead)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def test1() -> bool:\n",
" # Four examples from www.ultraboardgames.com/risk/game-rules.php\n",
" assert deaths([6, 1, 1], [3]) == (0, 1) # 6 beats 3, so defender loses 1\n",
" assert deaths([6, 2, 1], [3, 2]) == (1, 1) # 6 beats 3, but 2 ties 2, so 1 loss each\n",
" assert deaths([3, 3], [4, 3]) == (2, 0) # 3 loses to 4, and attacker 3 loses to 3\n",
" assert deaths([6], [5, 4]) == (0, 1) # 6 beats 5, so defender loses 1\n",
" return True\n",
" \n",
"test1()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"An **invasion** consists of a series of battles until either the defenders are all defeated (in which case the attacker can move armies into the captured territory, but must leave one army behind), or the attackers have fewer than two armies left, and can no longer attack. \n",
"\n",
"A **campaign** consists of a sequence of invasions, designed to capture all the defender's territories.\n",
"In the function `campaign`, the variable `attackers` tracks the number of attackers invading the current territory. (The total number of armies for the attacking side will in general be more than `attackers`, because there will be one army left behind in each captured territory.) If `verbose` is true, we'll `say` what is happening along the way. In the end, return the number of `attackers` in the final territory minus the total number of defenders still alive. This will be positive if all the defenders have been defeated, and nonpositive if not."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"def campaign(attackers: int, territories: List[int], verbose=True) -> int:\n",
" \"\"\"Given a number of armies for attacker and a list of territory army sizes for defender,\n",
" randomly play to the end, and return remaining attackers minus remaining defenders.\"\"\"\n",
" for t, defenders in enumerate(territories, 1):\n",
" if verbose: say(attackers, 'attack', defenders, t)\n",
" while attackers > 1 and defenders > 0:\n",
" A, D = deaths(roll(min(3, attackers - 1)), \n",
" roll(min(2, defenders)))\n",
" attackers -= A\n",
" defenders -= D\n",
" if attackers == 1:\n",
" if verbose: say(attackers, \"can't beat\", defenders, t)\n",
" break\n",
" if verbose: say(attackers, 'defeat', defenders, t)\n",
" attackers -= 1 # Capture a new territory, leave one behind\n",
" return attackers - (defenders + sum(territories[t:]))\n",
"\n",
"def say(attackers, action, defenders, t):\n",
" \"\"\"Say what is happening.\"\"\"\n",
" print(f'{attackers:3} armies {action} {defenders:2} defenders in territory {t}')\n",
" \n",
"def roll(n) -> List[Die]: \n",
" \"\"\"A random roll of `n` dice, sorted largest first.\"\"\"\n",
" return sorted((random.choice(die) for _ in range(n)), reverse=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# The Answer\n",
"\n",
"Let's answer the question of who wins the unfinished game:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" 72 armies attack 22 defenders in territory 1\n",
" 57 armies defeat 0 defenders in territory 1\n",
" 56 armies attack 8 defenders in territory 2\n",
" 53 armies defeat 0 defenders in territory 2\n",
" 52 armies attack 2 defenders in territory 3\n",
" 52 armies defeat 0 defenders in territory 3\n",
" 51 armies attack 2 defenders in territory 4\n",
" 47 armies defeat 0 defenders in territory 4\n",
" 46 armies attack 2 defenders in territory 5\n",
" 46 armies defeat 0 defenders in territory 5\n",
" 45 armies attack 7 defenders in territory 6\n",
" 42 armies defeat 0 defenders in territory 6\n",
" 41 armies attack 1 defenders in territory 7\n",
" 41 armies defeat 0 defenders in territory 7\n",
" 40 armies attack 1 defenders in territory 8\n",
" 40 armies defeat 0 defenders in territory 8\n",
" 39 armies attack 3 defenders in territory 9\n",
" 34 armies defeat 0 defenders in territory 9\n",
" 33 armies attack 1 defenders in territory 10\n",
" 33 armies defeat 0 defenders in territory 10\n",
" 32 armies attack 2 defenders in territory 11\n",
" 32 armies defeat 0 defenders in territory 11\n",
" 31 armies attack 3 defenders in territory 12\n",
" 27 armies defeat 0 defenders in territory 12\n",
" 26 armies attack 5 defenders in territory 13\n",
" 26 armies defeat 0 defenders in territory 13\n",
" 25 armies attack 1 defenders in territory 14\n",
" 23 armies defeat 0 defenders in territory 14\n"
]
},
{
"data": {
"text/plain": [
"22"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"campaign(attackers, territories)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The attackers won a resounding victory, capturing all the territories and moving 22 remaining armies into the final territory.\n",
"\n",
"But that was just one simulation; other simulations could have different results. Let's summarize, say, 100,000 simulations:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 33.8 s, sys: 80.5 ms, total: 33.9 s\n",
"Wall time: 34 s\n"
]
}
],
"source": [
"%time scores = [campaign(attackers, territories, False) for _ in range(100000)]"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAYsAAAEWCAYAAACXGLsWAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nO3debwd8/3H8debhNgTpEQWlwpKW8EtWtVGBWksobVEtY2l0lp+aqm9tVOq9iUabSTW2ImdINFSSxARQqUoIYiK2Gn4/P74fo9MTs4y995z7pw79/N8POZxZr6zfc45c+Z7Zr7f+X5lZjjnnHOVLJJ1AM455xqfZxbOOeeq8szCOedcVZ5ZOOecq8ozC+ecc1V5ZuGcc64qzyxyRlKTJJPUJeM4PpS0epYxuMZTj+MiHu9rpFx2X0lvxThWqGUceeeZRR1JmihpjqTFi9LHSDq5KO0VSYPaN8L6MbOlzeylWm5T0vKSrpH0ThyulLRsYv5Jkp6RNE/S8UXrrifp2bjewYn0rpIeldS3FfEMjCeqw9v0xjqRehwXaUnqCpwFbBXj+G9LMpq4jV6Sxkt6I67bVDR/cUmjJb0v6U1Jh1TY1jBJL0iaK+ltSWOTx3Oj8cyiTuJBtBlgwPaZBtMKChrt+DgZ6AGsDnwdWAk4PjF/BnA4cHuJdf8I/A5YD/i9pJVj+iHADWb2WiviGQ68G1/rQtKi9dp2WzTo8VHNSkA34Nk2bONL4C7gp2XmHw/0B1YFNgcOlzS4zLIPAZua2XKEY7oL4RhvTGbmQx0G4FjCwXAWcFsifQTwP+Bz4EPgVuBywkH4SUw7PC57HfAmMBd4EFg3sZ0lgDOB/8T5/4hpTYQMqktc7qfAK8A34/QmwMPAe8DTwMDENicCp8S4PwHWKHpPewK3JqZnANcmpl8DBsRxK6wPjAEuJJzEPwAeBb4e5wk4G3g7vo+phVhLfKZ3AvslpvcH7i6x3BXA8UVp04HF4/gjwEZAP+AxoGsrvt8l43sZFr/L5sS8u4ADipZ/GvhJHF8buJeQ0bwA7JJYbgwwErgD+AgYBGwDPAW8Hz/j4vf2y3gc/Bf4Q/y+B8V5iwBHAv+O868Fli/znnoAtwGzgTlxvE+l4yOmnRyPqcLxvAJwZYz3caApsY3kcbE48GfgVeAt4GJgiThvxbj/9+Ln9HdgkTJxV90msGb8PC3GeT/hN2Ux/UNg1xZ8/13iuk1F6a8TrlwK0ycB41Jsb2ngMuCOrM5ZVWPMOoC8DoQT6X7AhoTMYaXEvDHAyUXLf/UDT6TtBSwTfwDnAFMS8y6MP9TewKLA9+JyTfEg7kI4uc9I/JB6xxPGkHgS2TJO94zzJ8Yf2bpx/a5F8awef7yLAL0IJ6jXE/PmFH7QLJxZvEs4QXeJJ5Jxcd7WwBNAd0LG8Q2gV5nPdFvCSbRHHO4HDiqxXKnM4jpgO6APIQNeAbiZRGbZwu/3F8Cs+NnfCpyXmPdL4KHE9Drxc1scWIpwwt8zfhYbAO8Q/wjEz2ousGn8nLsBA4FvxelvE06COyS2/SHwfWAxwonyf8zPLA4iZI594v7/Alxd5j2tQPhzsSThuLsOuDkxf6HjI6bNIFzpLQc8B/yLkMl1IZwAL01sI3lcnAOMB5aP+7sV+GOc90fCib5rHDYDVCbutNtsIvFHqnjdRNp7wPerfP8LZRaEY9JY8Le+E/BMhe18P37fhUxrq0r7zXLIPIA8DvEA+B+wYpx+Hjg4MX8MKTKLovnd4wG1XDxpfAKsV2K5wg/id/GHm/xneARwedHydwPD4/hE4MQq7+01wgluGDCK8M98bcLJb3xiueLM4q+JeUOA5+P4j+LJZRPK/HNMrLcKMIFwFfYl4d/5YiWWK5VZrErIaJ4EdiPcGryccHVxCzAJ2LkF3/EE4Jw4vhvh33jXOL1M/OGvGqdPAUbH8V2Bvxdt6y/AcYnP6rIq+z4HODuOH0vi5E840X/O/MxiOrBFYn6veGx2SfEeBwBzEtMLHR8x7ZjE9JnAnYnp7VjwT44RrkgUP6OvJ+Z9F3g5jp8Yv5c1UsSZdptNpMgsUn7/pTKLvjGtWyJtS+CVFNvrTbiFtWZLY2mvoaPdc+wohgP3mNk7cfoqWnhfW9Kikk6T9G9J7xMyEwiX5ysS/nH+u8ImDgMuNLOZibRVgZ0lvVcYCBlbr8Qy1e7dTyL80/1BHJ8I/DAOkyqs92Zi/GPCZTdmdj9wAeFK6S1JoyoU8l1HyFiWAZYlvP8rqsRL3M9/zGyImW1AOAmdSMhQ/wxcQ8g8zpK0fLVtxcLwzQlXSMTtdSPcLsLMPiDcchsW5w9LLLsqsHHRd7A7UChDgaLvQNLGkh6QNFvSXOA3hGMAQgb61fJm9jHharFgVeCmxL6mA18Q7t8Xv68lJf1F0n/iMfcg0L2o3KTU8fFWYvyTEtNLl1inJyFjeyIR210xHeAMwhXLPZJeknRkiW20dJv19mF8TR6/yxJuV1ZkZq8TYh1Xh7hqwjOLGpO0BLAL8MNYG+JN4GBgPUnrxcWsxKrFaT8DhhIu55cj/CuC8O/pHeBTwqV/OVsRCnKTBXGvEa4suieGpczstApxFCtkFpvF8UmkyyzKMrPzzGxDwu2NNQkZXSnrAX8xs4/M7EPCbYohrdjlsYQrnbcIt3cmm9lcYCbhH2o1vyD8dm6N3+9LhMzil4llrgZ2k/Rdwj3zB2L6a8Ckou9gaTPbN7Fu8XdwFeHWSl8LhaEXE44DCLfC+hQWjMdfskroa8CPi/bXLZ6cih0KrAVsbGbLEv4QkNhXqdha6x1CRrJuIq7lzKzwJ+IDMzvUzFYnXJ0cImmLtmyz3sxsDuH7WC+RvB7pC9S7UPk3nSnPLGpvB8I/t3UIl/EDCPfh/878k8lbhHv8ScVpywCfEf4lLgmcWphhZl8Cown/hFeJVyHfLaqi+ywwGLhQUqE21hXAdpK2jut0i9U/+5DeJMK/6iXiVcvf435WIBTCtoik78R/zl0JtxA+JXx+pTwO/ErSEvGkOIJQcFzYVldJ3QjHdZf4/haoTSRpHUJmNzImvQz8SNJKhFosr6YI+5fACcz/fgcQ7vVvk6i7fwfhX/2JwDXxO4NQaLumpF/EeLvGz+AbFfa3DPCumX0qaSPCH4mC6wnf6fckLRbjSp7cLwZOkbRqfP89JQ2tsJ9PgPfiFdZxKT6LVomfxyXA2ZK+FmPrLWnrOL6tpDUkiVBQ/gXlj4tU2yyj1G+xoniMFX5ri8fpgssIf9J6SFob2Idwa7HUdnaX1C/WLFuVcLvyvpbE0q6yvg+Wt4FwKXlmifRdCLdiuhBOSlMIBWk3x/lDCSeq9wi3R5Ym3N74gFCQ/EsWLAdYgnDv+nXm15YqVRuqmfCD+HGc3phwwn+XcJ/9dqBfnDcR+FWK9ziLBQstJ5O4Tx3TisssTk7MGwjMjONbEGpAfUj4Z3glsHSZ/a5GKLD8b4z/LqB/Yv6YuN/ksEfRNh4g/HMuTK9HKNt5Bzgkkf4ssHuJGDYhZGg9S8x7lkQtKOBvMYbvFC23VvzcZ8f3cj/za5Et8FnFtJ3iMfABIbO5ALgiMX+PeOwUakO9DmwW5y1CqB78Qlz/38CpZT7fVeIx8CHhdt+vi46lhY6P4jRCzagxielBwIwyx0U3wp+glwgZwnTgwDjvYMKt148IV3x/qHA8pt1mEwuXWfyGcDy/R6yVFt//ZlX2t8CQmLc44Y/c+4TfXfKY6he3Xfi9nRLfW+E9jgJWaM/zVUsGxaCdczkgaWnCia+/mb2cdTwuP/w2lHMdnKTtYuH0UoQC+2eYXyHCuZrwzMK5jm8o8EYc+gPDzG8ZuBrz21DOOeeq8isL55xzVWXajHW9rLjiitbU1JR1GM4516E88cQT75hZyYcYc5lZNDU1MXny5KzDcM65DkXSf8rN89tQzjnnqvLMwjnnXFWeWTjnnKvKMwvnnHNVeWbhnHOuKs8snHPOVVX3zCI2hf2UpNvi9GqSHpX0oqRrYrPKSFo8Ts+I85sS2zgqpr9Qpblh55xzddAeVxa/JTQTXHA6oUvI/oQ+m/eO6XsTunBcAzg7Llfof2AYoWOcwcBFxX0UOOecq6+6ZhaxU51tgL/GaRH6XL4+LjKW0FkQhMbQxsbx64Et4vJDgXFm9llscnkGsFE943bOObegel9ZnAMcDhR6CVsBeM/M5sXpmYSOyomvrwHE+XPj8l+ll1jnK5JGSJosafLs2bNr/T5cR/Pww2FwztVE3TILSdsCb5vZE8nkEotalXmV1pmfYDbKzJrNrLlnz/bqn901rO99LwzOuZqoZ9tQmwLbSxpC6OpwWcKVRndJXeLVQx9CG/wQrhj6AjMldQGWI3SdWUgvSK7jXGnTpoXXb34z2zicy4m6XVmY2VFm1sfMmggF1Peb2e6EPpB3iosNJ/QzDTA+ThPn3x87cBkPDIu1pVYjdO7yWL3idjlxwAFhcM7VRBatzh4BjJN0MvAUoVN74uvlkmYQriiGAZjZs5KuBZ4D5gH7m9kX7R+261DOOCPrCJzLlVz2lNfc3GzeRLlzzrWMpCfMrLnUvFz2Z+EcU6aE1wEDso2jDZqOvH2B6VdO2yajSJzzzMLl1UEHhdeJEzMNw7m88MzC5dM552QdgXO54pmFy6cOfPupnOLbUrDwrSm/deXqxTMLl0+PPx5ev/OdbOOowE/sriPxzMLl02GHhVcvs3CuJjyzcPl0wQVZR9BipW4zOdcoPLNw+eTNfDhXU55ZuHwqtDib88YE/WrEtRfPLFw+HX10ePUyC+dqwjMLl09/+UvWETiXK55ZuHxaa62sI3AuV9qjD27n2t+kSWFwztWEX1m4fDruuPDaQGUWXhjtOrKKmYWkPoR+JTYDVgE+AaYBtwN3mtmXFVZ3LjujR2cdgXO5UjazkHQp0Bu4DTgdeJvQPeqawGDgGElHmtmD7RGLine truncated
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"def summary(scores):\n",
" \"\"\"Summarize the scores from simulations.\"\"\"\n",
" P = np.average([s > 0 for s in scores]) \n",
" N, avg = len(scores), np.average(scores)\n",
" title = f'Attacker wins {P:.0%}. Average armies left: {avg:.1f}'\n",
" plt.ylabel(f'Frequency (out of {N:,d} runs)'); \n",
" plt.xlabel('Armies left (positive for attacker)')\n",
" plt.hist(scores, bins=max(scores) - min(scores) + 1)\n",
" plt.plot([0, 0], [0, N/25], 'r:'); plt.title(title)\n",
" \n",
"summary(scores)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We see that the attackers win 81% of the time, and the scores look roughly like a bell-shaped curve, but with a non-normal pattern on the left side. What's causing the non-normal pattern? Note that the number of defenders in the final four territories are `(2, 3, 5, 1)`, and the width of the four spikes on the left are `(1, 2, 4, 1)`, one less than the number of defenders (except for the last spike). I think there are spikes rather than a smooth curve because it is doubly-difficult to capture a territory: you need to have two armies left, not just one, so that you can leave one behind. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Exact Probabilities\n",
"\n",
"By repeatedly running a simulation, we can get approximate probabilities. But what if we wanted **exact probabilities** (at least exact under the assumption that the dice are exactly fair and that floating point roundoff is not an issue)? I'll start by defining the function `outcomes`, so that, for example, `outcomes(3, 2)` returns a Counter of all possible outcomes of rolling 3 attacker dice versus 2 defender dice; each outcome is a pair of `(attacker_deaths, defender_deaths)` and has a count of how often it occurred."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"from functools import lru_cache\n",
"from collections import Counter\n",
"import itertools\n",
"\n",
"@lru_cache()\n",
"def outcomes(num_A: int, num_D: int) -> List[Tuple[int, int]]:\n",
" \"\"\"All (equiprobable) outcomes of (attacker_deaths, defender_deaths)\n",
" from rolling num_A dice versus num_D dice.\"\"\"\n",
" return Counter(deaths(dice[:num_A], dice[num_A:])\n",
" for dice in itertools.product(die, repeat=num_A + num_D))"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Counter({(2, 0): 2275, (1, 1): 2611, (0, 2): 2890})"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"outcomes(3, 2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The result for `outcomes(3, 2)` says that the attackers lose two armies in 2,275 out of the $6^5 = 7,776$ different outcomes; the defenders lose two in 2,890 outcomes; and they each lose one in the remaining 2,611 outcomes. I found a [web page](http://datagenetics.com/blog/november22011/) that lists the results for all six possible battles (attackers roll 1, 2, or 3 dice; defenders roll 1 or 2); let's verify that our `outcomes` agree with theirs:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def test2() -> bool:\n",
" # See http://datagenetics.com/blog/november22011/\n",
" assert outcomes(1, 1) == {(1, 0): 21, (0, 1): 15}\n",
" assert outcomes(2, 1) == {(1, 0): 91, (0, 1): 125}\n",
" assert outcomes(3, 1) == {(1, 0): 441, (0, 1): 855}\n",
" assert outcomes(1, 2) == {(1, 0): 161, (0, 1): 55}\n",
" assert outcomes(2, 2) == {(2, 0): 581, (1, 1): 420, (0, 2): 295}\n",
" assert outcomes(3, 2) == {(2, 0): 2275, (1, 1): 2611, (0, 2): 2890}\n",
" return True\n",
" \n",
"test2()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we know exact outcomes for a single roll of the dice; what about an invasion where $A$ attackers keep attacking until they defeat $D$ defenders (or all but one attacker dies trying)? The function `winP(A, D)` gives the probability of the attackers winning. It works recursively. The two base cases are that the probability is zero if the attackers don't have at least two armies and the probability is one if the defenders have no armies. \n",
"\n",
"In the recursive case we observe the possible outcomes for the first battle, and then compute the win probability for the invasion as the average, over every possible outcome of the dice rolls, of the win probability for the number of remaining attackers and defenders after the battle, weighted by the number of times that the outcome occurs. (We use `np.average` because it accepts an optional `weights` argument.)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"@lru_cache(None)\n",
"def winP(attackers: int, defenders: int) -> float:\n",
" \"\"\"The probability that `attackers` can invade and defeat all the `defenders`.\"\"\"\n",
" if attackers <= 1:\n",
" return 0\n",
" elif defenders == 0:\n",
" return 1\n",
" else:\n",
" battle = outcomes(min(3, attackers - 1), min(2, defenders))\n",
" return np.average([winP(attackers - A, defenders - D) for (A, D) in battle],\n",
" weights=list(battle.values()))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's try a simple example: `winP(2, 1)` should be `15/36`, the same as the probability of the attacker prevailing in `outcomes(2, 1)`:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"winP(2, 1) == 15/36"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"What's the probability that 12 attackers successfully invade 10 defenders?"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0.6538260547697077"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"winP(12, 10)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's make a chart, with the number of defenders varying from 1 to 60, and the number of attackers separated into eight cases (depicted as eight lines), where in each case there are Δ more attackers than defenders, for Δ = -5, -2, 0, 1, 2, 5, and 10:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
"def chart(Ds=range(1, 61), deltas=(-5, -2, -1, 0, 1, 2, 5, 10)):\n",
" plt.figure(figsize=(9, 6)); plt.grid()\n",
" plt.title('Each line: attackers with Δ more armies than defenders')\n",
" plt.xlabel('Number of Defenders'); plt.ylabel('Win Probability for Attackers')\n",
" for delta in reversed(deltas):\n",
" Ps = [winP(max(0, D + delta), D) for D in Ds]\n",
" plt.plot(Ds, Ps, '.-', label=f'Δ={delta}')\n",
" plt.legend()"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 648x432 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"chart()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that the purple line (fourth from bottom), where the number of attackers exactly equals the number of defenders, gives a low win probability for a small attacking force, but reaches 50% for 12-on-12, and 73% for 60-on-60. The red line, where the attackers have one more army than the defenders, dips from one to two defenders but is over 50% for a 6-on-5 attack. Similarly, the green line, where the attackers have a surplus of two armies, dips sharply from 75% to 66% as the number of defenders goes from 1 to 2, dips slightly more for 3 and 4 defenders, and then starts to rise. So overall, an attacker does not need a big advantage in armies as long as there are many armies on both sides. Even when the attacker is at a disadvantage in numbers (as in the bottom grey line where the attacker has five fewer armies), the attacker can still have an advantage in win probability; `winP(55, 60)` is about 57%."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Simulation versus Exact Computation\n",
"\n",
"Let's see how the exact computation compares with the simulation:"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(0.7194339370737594, 0.7209)"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"A, D = 32, 30\n",
"\n",
"exact = winP(A, D)\n",
"simul = np.average([campaign(A, [D], False) > 0 for _ in range(10000)])\n",
"\n",
"exact, simul"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We see that they give similar results, differing by one or two parts per thousand. So when would you prefer a simulation, and when an exact computation?\n",
"\n",
"**Advantages of a simulation:**\n",
"- Usually simpler to code; can use an `if` statement to distinguish two branches; don't have to follow every branch.\n",
"- Can be more efficient to compute; don't have to spend a lot of computation time on extremely rare events.\n",
"- Can handle event sequences of potentially unbounded length.\n",
"\n",
"**Advantages of an exact calculation:**\n",
"- Don't need to rerun multiple times; don't need statistical inference to analyze the range of outcomes.\n",
"- If a very rare event can be extremely bad (or good), it is important to know exactly how likely the rare event is. \n",
"\n",
"I chose to use a **simulation** for `campaign` because:\n",
"- I thought it would be easier to code.\n",
"- I would be happy with only four bits of accuracy: enough to determine the win percentage within 6%. (I ended up running enough simulations to get within 1%.) \n",
"- I didn't care about the 1-in-a-billion chance of, say, the attackers losing 17 battles in a row; I just want to know the overall odds of the attackers losing the campaign. (Note that if I was doing a simulation of a nuclear reactor, I would certainly be very concerned with a 1-in-a-billion chance of a meltdown, and would need to code ways of exploring that possibility more carefully.)\n",
"- A simulation is more flexible. Consider a small change to the rules of Risk: if the dice show five 1s in a battle, both sides add one army. We can easily implement that in a simulation in two or three lines of code, and the effect on run time will be negligible, because it only happens one in 7,776 times. But in an exact calculation this new rule would change everything: it would become an infinite game, and we would have to make a wholesale rearrangement of the code to deal with that. \n",
"\n",
"I chose to use **exact calculation** for `winP` because:\n",
"- I knew the computational demands would be very small either way.\n",
"- I saw there was something going on with odd/even number of armies, and I wanted to distinguish slight variations that are real from slight variations that are random, so getting exact numbers was useful.\n",
"\n",
"\n",
"(*Note:* I call it an \"exact\" calculation, but it is actually limited to the precision of 64-bit floating point numbers. If you truly need an exact answer for a discrete combinatorics problem, do division with the `fraction.Fraction` class, not with `float`. And of course it is an exact computation in the model; if the model does not reflect the world, the results will be wrong.)"
]
}
],
"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": 4
}