{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Modeling and Simulation in Python\n", "\n", "Chapter 2\n", "\n", "Copyright 2017 Allen Downey\n", "\n", "License: [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# Configure Jupyter so figures appear in the notebook\n", "%matplotlib inline\n", "\n", "# Configure Jupyter to display the assigned value after an assignment\n", "%config InteractiveShell.ast_node_interactivity='last_expr_or_assign'\n", "\n", "# import functions from the modsim library\n", "from modsim import *\n", "\n", "# set the random number generator\n", "np.random.seed(7)\n", "\n", "# If this cell runs successfully, it produces no output." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Modeling a bikeshare system" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll start with a `State` object that represents the number of bikes at each station.\n", "\n", "When you display a `State` object, it lists the state variables and their values:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "bikeshare = State(olin=10, wellesley=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can access the state variables using dot notation." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "bikeshare.olin" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "scrolled": true }, "outputs": [], "source": [ "bikeshare.wellesley" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise:** What happens if you spell the name of a state variable wrong? Edit the previous cell, change the spelling of `wellesley`, and run the cell again.\n", "\n", "The error message uses the word \"attribute\", which is another name for what we are calling a state variable. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise:** Add a third attribute called `babson` with initial value 0, and display the state of `bikeshare` again." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Updating\n", "\n", "We can use the update operators `+=` and `-=` to change state variables." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "bikeshare.olin -= 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we display `bikeshare`, we should see the change." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "bikeshare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Of course, if we subtract a bike from `olin`, we should add it to `wellesley`." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "bikeshare.wellesley += 1\n", "bikeshare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Functions\n", "\n", "We can take the code we've written so far and encapsulate it in a function." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def bike_to_wellesley():\n", " bikeshare.olin -= 1\n", " bikeshare.wellesley += 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When you define a function, it doesn't run the statements inside the function, yet. When you call the function, it runs the statements inside." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "bike_to_wellesley()\n", "bikeshare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "One common error is to omit the parentheses, which has the effect of looking up the function, but not calling it." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "bike_to_wellesley" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The output indicates that `bike_to_wellesley` is a function defined in a \"namespace\" called `__main__`, but you don't have to understand what that means." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise:** Define a function called `bike_to_olin` that moves a bike from Wellesley to Olin. Call the new function and display `bikeshare` to confirm that it works." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "# Solution goes here" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "# Solution goes here" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Conditionals" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`modsim.py` provides `flip`, which takes a probability and returns either `True` or `False`, which are special values defined by Python.\n", "\n", "The Python function `help` looks up a function and displays its documentation." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "help(flip)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the following example, the probability is 0.7 or 70%. If you run this cell several times, you should get `True` about 70% of the time and `False` about 30%." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "flip(0.7)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the following example, we use `flip` as part of an if statement. If the result from `flip` is `True`, we print `heads`; otherwise we do nothing." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "if flip(0.7):\n", " print('heads')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With an else clause, we can print heads or tails depending on whether `flip` returns `True` or `False`." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "if flip(0.7):\n", " print('heads')\n", "else:\n", " print('tails')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step\n", "\n", "Now let's get back to the bikeshare state. Again let's start with a new `State` object." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "bikeshare = State(olin=10, wellesley=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Suppose that in any given minute, there is a 50% chance that a student picks up a bike at Olin and rides to Wellesley. We can simulate that like this." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "if flip(0.5):\n", " bike_to_wellesley()\n", " print('Moving a bike to Wellesley')\n", "\n", "bikeshare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And maybe at the same time, there is also a 40% chance that a student at Wellesley rides to Olin." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "if flip(0.4):\n", " bike_to_olin()\n", " print('Moving a bike to Olin')\n", "\n", "bikeshare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can wrap that code in a function called `step` that simulates one time step. In any given minute, a student might ride from Olin to Wellesley, from Wellesley to Olin, or both, or neither, depending on the results of `flip`." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "def step():\n", " if flip(0.5):\n", " bike_to_wellesley()\n", " print('Moving a bike to Wellesley')\n", " \n", " if flip(0.4):\n", " bike_to_olin()\n", " print('Moving a bike to Olin')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since this function takes no parameters, we call it like this:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "step()\n", "bikeshare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parameters\n", "\n", "As defined in the previous section, `step` is not as useful as it could be, because the probabilities `0.5` and `0.4` are \"hard coded\".\n", "\n", "It would be better to generalize this function so it takes the probabilities `p1` and `p2` as parameters:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "def step(p1, p2):\n", " if flip(p1):\n", " bike_to_wellesley()\n", " print('Moving a bike to Wellesley')\n", " \n", " if flip(p2):\n", " bike_to_olin()\n", " print('Moving a bike to Olin')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can call it like this:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "step(0.5, 0.4)\n", "bikeshare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise:** At the beginning of `step`, add a print statement that displays the values of `p1` and `p2`. Call it again with values `0.3`, and `0.2`, and confirm that the values of the parameters are what you expect. " ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "# Solution goes here" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## For loop" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Before we go on, I'll redefine `step` without the print statements." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "def step(p1, p2):\n", " if flip(p1):\n", " bike_to_wellesley()\n", " \n", " if flip(p2):\n", " bike_to_olin()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And let's start again with a new `State` object:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "bikeshare = State(olin=10, wellesley=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can use a `for` loop to move 4 bikes from Olin to Wellesley." ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [], "source": [ "for i in range(4):\n", " bike_to_wellesley()\n", " \n", "bikeshare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Or we can simulate 4 random time steps." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "for i in range(4):\n", " step(0.3, 0.2)\n", " \n", "bikeshare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If each step corresponds to a minute, we can simulate an entire hour like this." ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "for i in range(60):\n", " step(0.3, 0.2)\n", "\n", "bikeshare" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After 60 minutes, you might see that the number of bike at Olin is negative. We'll fix that problem in the next notebook.\n", "\n", "But first, we want to plot the results." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## TimeSeries\n", "\n", "`modsim.py` provides an object called a `TimeSeries` that can contain a sequence of values changing over time.\n", "\n", "We can create a new, empty `TimeSeries` like this:" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "results = TimeSeries()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And we can add a value to the `TimeSeries` like this:" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "results[0] = bikeshare.olin\n", "results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `0` in brackets is an `index` that indicates that this value is associated with time step 0.\n", "\n", "Now we'll use a for loop to save the results of the simulation. I'll start one more time with a new `State` object." ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "bikeshare = State(olin=10, wellesley=2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here's a for loop that runs 10 steps and stores the results." ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [], "source": [ "for i in range(10):\n", " step(0.3, 0.2)\n", " results[i] = bikeshare.olin" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can display the results." ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A `TimeSeries` is a specialized version of a Pandas `Series`, so we can use any of the functions provided by `Series`, including several that compute summary statistics:" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [], "source": [ "results.mean()" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [], "source": [ "results.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can read the documentation of `Series` [here](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.html)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Plotting\n", "\n", "We can also plot the results like this." ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [], "source": [ "plot(results, label='Olin')\n", "\n", "decorate(title='Olin-Wellesley Bikeshare',\n", " xlabel='Time step (min)', \n", " ylabel='Number of bikes')\n", "\n", "savefig('figs/chap01-fig01.pdf')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`decorate`, which is defined in the `modsim` library, adds a title and labels the axes." ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [], "source": [ "help(decorate)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`savefig()` saves a figure in a file." ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [], "source": [ "help(savefig)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The suffix of the filename indicates the format you want. This example saves the current figure in a PDF file named `chap01-fig01.pdf`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Exercise:** Wrap the code from this section in a function named `run_simulation` that takes three parameters, named `p1`, `p2`, and `num_steps`.\n", "\n", "It should:\n", "\n", "1. Create a `TimeSeries` object to hold the results.\n", "2. Use a for loop to run `step` the number of times specified by `num_steps`, passing along the specified values of `p1` and `p2`.\n", "3. After each step, it should save the number of bikes at Olin in the `TimeSeries`.\n", "4. After the for loop, it should plot the results and\n", "5. Decorate the axes.\n", "\n", "To test your function:\n", "\n", "1. Create a `State` object with the initial state of the system.\n", "2. Call `run_simulation` with appropriate parameters.\n", "3. Save the resulting figure.\n", "\n", "Optional:\n", "\n", "1. Extend your solution so it creates two `TimeSeries` objects, keeps track of the number of bikes at Olin *and* at Wellesley, and plots both series at the end." ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [], "source": [ "# Solution goes here" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [], "source": [ "# Solution goes here" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Opening the hood\n", "\n", "The functions in `modsim.py` are built on top of several widely-used Python libraries, especially NumPy, SciPy, and Pandas. These libraries are powerful but can be hard to use. The intent of `modsim.py` is to give you the power of these libraries while making it easy to get started.\n", "\n", "In the future, you might want to use these libraries directly, rather than using `modsim.py`. So we will pause occasionally to open the hood and let you see how `modsim.py` works.\n", "\n", "You don't need to know anything in these sections, so if you are already feeling overwhelmed, you might want to skip them. But if you are curious, read on." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Pandas\n", "\n", "This chapter introduces two objects, `State` and `TimeSeries`. Both are based on the `Series` object defined by Pandas, which is a library primarily used for data science.\n", "\n", "You can read the documentation of the `Series` object [here](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.html)\n", "\n", "The primary differences between `TimeSeries` and `Series` are:\n", "\n", "1. I made it easier to create a new, empty `Series` while avoiding a [confusing inconsistency](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.html).\n", "\n", "2. I provide a function so the `Series` looks good when displayed in Jupyter.\n", "\n", "3. I provide a function called `set` that we'll use later.\n", "\n", "`State` has all of those capabilities; in addition, it provides an easier way to initialize state variables, and it provides functions called `T` and `dt`, which will help us avoid a confusing error later." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Pyplot\n", "\n", "The `plot` function in `modsim.py` is based on the `plot` function in Pyplot, which is part of Matplotlib. You can read the documentation of `plot` [here](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html).\n", "\n", "`decorate` provides a convenient way to call the `pyplot` functions `title`, `xlabel`, and `ylabel`, and `legend`. It also avoids an annoying warning message if you try to make a legend when you don't have any labelled lines." ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [], "source": [ "help(decorate)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### NumPy\n", "\n", "The `flip` function in `modsim.py` uses NumPy's `random` function to generate a random number between 0 and 1.\n", "\n", "You can get the source code for `flip` by running the following cell." ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [], "source": [ "%psource flip" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "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.6.6" } }, "nbformat": 4, "nbformat_minor": 2 }