├── .nbinteract.json ├── images ├── coronavirus_curve.mp4 ├── 2020-03-20_ben_sparks.jpg ├── numberphile_background.jpg ├── 2020-03-20_ben_sparks_2.jpg └── coronavirus_curve_no_background.mp4 ├── notebooks ├── coronavirus_curve.mp4 ├── numberphile_background.jpg ├── coronavirus_curve_no_background.mp4 └── The_Coronavirus_Curve.ipynb ├── .gitignore ├── requirements.in ├── README.md └── requirements.txt /.nbinteract.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec": "john-sandall/numberphile/master" 3 | } -------------------------------------------------------------------------------- /images/coronavirus_curve.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-sandall/numberphile/master/images/coronavirus_curve.mp4 -------------------------------------------------------------------------------- /images/2020-03-20_ben_sparks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-sandall/numberphile/master/images/2020-03-20_ben_sparks.jpg -------------------------------------------------------------------------------- /images/numberphile_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-sandall/numberphile/master/images/numberphile_background.jpg -------------------------------------------------------------------------------- /notebooks/coronavirus_curve.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-sandall/numberphile/master/notebooks/coronavirus_curve.mp4 -------------------------------------------------------------------------------- /images/2020-03-20_ben_sparks_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-sandall/numberphile/master/images/2020-03-20_ben_sparks_2.jpg -------------------------------------------------------------------------------- /notebooks/numberphile_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-sandall/numberphile/master/notebooks/numberphile_background.jpg -------------------------------------------------------------------------------- /images/coronavirus_curve_no_background.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-sandall/numberphile/master/images/coronavirus_curve_no_background.mp4 -------------------------------------------------------------------------------- /notebooks/coronavirus_curve_no_background.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-sandall/numberphile/master/notebooks/coronavirus_curve_no_background.mp4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://www.gitignore.io/ ### 2 | 3 | # Caches 4 | **/.ipynb_checkpoints/* 5 | **/__pycache__/* 6 | *.pyc 7 | 8 | # Other 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | jupyter-contrib-nbextensions==0.5.1 2 | jupyterlab==2.0.1 3 | nb-black==1.0.7 4 | nbinteract==0.2.5 5 | pandas==1.0.3 6 | Pillow==7.0.0 7 | pip-tools==4.5.1 8 | scipy==1.4.1 9 | seaborn==0.10.0 10 | treon==0.1.3 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Numberphile 2 | 3 | [![ben sparks](./images/2020-03-20_ben_sparks.jpg)](https://www.youtube.com/watch?v=k6nLfCbAzgo) 4 | 5 |
Watch The Coronavirus Curve - Numberphile on YouTube
6 | 7 | **Video Description** 8 | > _Ben Sparks explains (and codes) the so-called SIR Model being used to predict the spread of cornavirus (COVID-19)._ 9 | > 10 | > **LINKS** 11 | > - National Health Service (UK) advice on Coronavirus: https://www.nhs.uk/conditions/coronavirus-covid-19/ 12 | > - Ben Sparks: https://www.bensparks.co.uk 13 | > - Use the Geogebra file Ben created for this video: https://www.geogebra.org/m/nbjfjtpv 14 | > - Another good file courtesy of Juan Carlos Ponce Campuzano: https://www.geogebra.org/m/utbemrca 15 | > - Washington Post simulator: https://www.washingtonpost.com/graphics/2020/world/corona-simulator/ 16 | > - Extended presentation by Nick Jewell for MSRI: https://youtu.be/MZ957qhzcjI 17 | > - More videos with Ben Sparks: http://bit.ly/Sparks_Playlist 18 | > 19 | > **SOME OTHER YOUTUBERS ON THIS TOPIC...** 20 | > - 3blue1brown on the exponential growth of epidemics: https://youtu.be/Kas0tIxDvrg 21 | > - Tom Crawford on the SIR Model: https://youtu.be/NKMHhm2Zbkw 22 | > - Kurzgesagt on COVID-19: https://youtu.be/BtN-goy9VOY 23 | > 24 | > **NUMBERPHILE** 25 | > - Website: http://www.numberphile.com/ 26 | > - Numberphile on Facebook: http://www.facebook.com/numberphile 27 | > - Numberphile tweets: https://twitter.com/numberphile 28 | > Subscribe: http://bit.ly/Numberphile_Sub 29 | > 30 | > Videos by [Brady Haran](https://www.bradyharanblog.com/) ([@BradyHaran](https://twitter.com/BradyHaran)) 31 | 32 | 33 | # Tips & Tricks 34 | ``` 35 | # Activate environment 36 | workon numberphile 37 | 38 | # Update packages from requirements.txt 39 | pip-sync 40 | 41 | # Install new package & update requirements.txt 42 | pip install new-package-name 43 | pip freeze # to check version number 44 | 45 | # copy paste package & version to requirements.in 46 | pip-compile requirements.in 47 | pip-sync 48 | ``` 49 | 50 | # Setup 51 | ``` 52 | # Install virtualenv 53 | pip install virtualenv 54 | 55 | 56 | # Install virtualenvwrapper (http://virtualenvwrapper.readthedocs.org/en/latest/index.html) 57 | pip install virtualenvwrapper 58 | # Tell shell to source virtualenvwrapper.sh and where to put the virtualenvs by adding following to .zshrc 59 | zshconfig 60 | # # "Tell shell to source virtualenvwrapper.sh and where to put the virtualenvs" 61 | # export WORKON_HOME=$HOME/.virtualenvs 62 | # export PROJECT_HOME=$HOME/code 63 | # source /usr/local/bin/virtualenvwrapper.sh 64 | source ~/.zshrc 65 | source /usr/local/bin/virtualenvwrapper.sh 66 | # Now let's make a virtualenv 67 | mkvirtualenv venv 68 | workon venv 69 | # Commands `workon venv`, `deactivate`, `lsvirtualenv` and `rmvirtualenv` are useful 70 | # WARNING: When you brew install formulae that provide Python bindings, you should not be in an active virtual environment. 71 | # (https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/Homebrew-and-Python.md) 72 | deactivate 73 | 74 | 75 | # Create virtualenv & install packasges 76 | mkvirtualenv numberphile 77 | pip install pip-tools 78 | pip-sync 79 | python -m ipykernel install --user --name numberphile --display-name "Python (numberphile)" 80 | 81 | # Install Jupyter extensions JS/CSS & enable required extensions 82 | jupyter contrib nbextension install 83 | jupyter nbextension enable toc2/main 84 | 85 | # Initialise nbinteract & create .html 86 | nbinteract init 87 | nbinteract notebooks/The_Coronavirus_Curve.ipynb 88 | ``` 89 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | appdirs==1.4.3 # via black 8 | appnope==0.1.0 # via ipykernel, ipython 9 | attrs==19.3.0 # via black, jsonschema 10 | backcall==0.1.0 # via ipython 11 | black==19.10b0 # via nb-black 12 | bleach==3.1.4 # via nbconvert 13 | bqplot==0.11.0 # via nbinteract 14 | click==7.1.1 # via black, pip-tools 15 | cycler==0.10.0 # via matplotlib 16 | decorator==4.4.2 # via ipython, traitlets 17 | defusedxml==0.6.0 # via nbconvert 18 | docopt==0.6.2 # via nbinteract, treon 19 | entrypoints==0.3 # via nbconvert 20 | importlib-metadata==1.6.0 # via jsonschema 21 | ipykernel==5.2.0 # via ipywidgets, jupyter, jupyter-console, notebook, qtconsole 22 | ipython-genutils==0.2.0 # via jupyter-contrib-nbextensions, nbformat, notebook, qtconsole, traitlets 23 | ipython==7.13.0 # via ipykernel, ipywidgets, jupyter-console, jupyter-latex-envs, nb-black, nbinteract 24 | ipywidgets==7.5.1 # via bqplot, jupyter, nbinteract 25 | jedi==0.16.0 # via ipython 26 | jinja2==2.11.1 # via jupyterlab, jupyterlab-server, nbconvert, nbinteract, notebook 27 | json5==0.9.4 # via jupyterlab-server 28 | jsonschema==3.2.0 # via jupyterlab-server, nbformat 29 | jupyter-client==6.1.2 # via ipykernel, jupyter-console, notebook, qtconsole, treon 30 | jupyter-console==6.1.0 # via jupyter 31 | jupyter-contrib-core==0.3.3 # via jupyter-contrib-nbextensions, jupyter-nbextensions-configurator 32 | jupyter-contrib-nbextensions==0.5.1 # via -r requirements.in 33 | jupyter-core==4.6.3 # via jupyter-client, jupyter-contrib-core, jupyter-contrib-nbextensions, jupyter-latex-envs, jupyter-nbextensions-configurator, nbconvert, nbformat, notebook, qtconsole 34 | jupyter-highlight-selected-word==0.2.0 # via jupyter-contrib-nbextensions 35 | jupyter-latex-envs==1.4.6 # via jupyter-contrib-nbextensions 36 | jupyter-nbextensions-configurator==0.4.1 # via jupyter-contrib-nbextensions 37 | jupyter==1.0.0 # via treon 38 | jupyterlab-server==1.0.7 # via jupyterlab 39 | jupyterlab==2.0.1 # via -r requirements.in 40 | kiwisolver==1.1.0 # via matplotlib 41 | lxml==4.5.0 # via jupyter-contrib-nbextensions 42 | markupsafe==1.1.1 # via jinja2 43 | matplotlib==3.2.1 # via seaborn 44 | mistune==0.8.4 # via nbconvert 45 | nb-black==1.0.7 # via -r requirements.in 46 | nbconvert==5.6.1 # via jupyter, jupyter-contrib-nbextensions, jupyter-latex-envs, nbinteract, notebook, treon 47 | nbformat==4.4.0 # via ipywidgets, nbconvert, nbinteract, notebook 48 | nbinteract==0.2.5 # via -r requirements.in 49 | notebook==6.0.3 # via jupyter, jupyter-contrib-core, jupyter-contrib-nbextensions, jupyter-latex-envs, jupyter-nbextensions-configurator, jupyterlab, jupyterlab-server, widgetsnbextension 50 | numpy==1.18.2 # via bqplot, matplotlib, nbinteract, pandas, scipy, seaborn 51 | pandas==1.0.3 # via -r requirements.in, bqplot, seaborn 52 | pandocfilters==1.4.2 # via nbconvert 53 | parso==0.6.2 # via jedi 54 | pathspec==0.7.0 # via black 55 | pexpect==4.8.0 # via ipython 56 | pickleshare==0.7.5 # via ipython 57 | pillow==7.0.0 # via -r requirements.in 58 | pip-tools==4.5.1 # via -r requirements.in 59 | prometheus-client==0.7.1 # via notebook 60 | prompt-toolkit==3.0.5 # via ipython, jupyter-console 61 | ptyprocess==0.6.0 # via pexpect, terminado 62 | pygments==2.6.1 # via ipython, jupyter-console, nbconvert, qtconsole 63 | pyparsing==2.4.6 # via matplotlib 64 | pyrsistent==0.16.0 # via jsonschema 65 | python-dateutil==2.8.1 # via jupyter-client, matplotlib, pandas 66 | pytz==2019.3 # via pandas 67 | pyyaml==5.3.1 # via jupyter-contrib-nbextensions, jupyter-nbextensions-configurator 68 | pyzmq==19.0.0 # via jupyter-client, notebook, qtconsole 69 | qtconsole==4.7.2 # via jupyter 70 | qtpy==1.9.0 # via qtconsole 71 | regex==2020.2.20 # via black 72 | scipy==1.4.1 # via -r requirements.in, seaborn 73 | seaborn==0.10.0 # via -r requirements.in 74 | send2trash==1.5.0 # via notebook 75 | six==1.14.0 # via bleach, cycler, jsonschema, pip-tools, pyrsistent, python-dateutil, traitlets 76 | terminado==0.8.3 # via notebook 77 | testpath==0.4.4 # via nbconvert 78 | toml==0.10.0 # via black 79 | toolz==0.10.0 # via nbinteract 80 | tornado==6.0.4 # via ipykernel, jupyter-client, jupyter-contrib-core, jupyter-contrib-nbextensions, jupyter-nbextensions-configurator, jupyterlab, notebook, terminado 81 | traitlets==4.3.3 # via bqplot, ipykernel, ipython, ipywidgets, jupyter-client, jupyter-contrib-core, jupyter-contrib-nbextensions, jupyter-core, jupyter-latex-envs, jupyter-nbextensions-configurator, nbconvert, nbformat, nbinteract, notebook, qtconsole, traittypes 82 | traittypes==0.2.1 # via bqplot 83 | treon==0.1.3 # via -r requirements.in 84 | typed-ast==1.4.1 # via black 85 | wcwidth==0.1.9 # via prompt-toolkit 86 | webencodings==0.5.1 # via bleach 87 | widgetsnbextension==3.5.1 # via ipywidgets 88 | zipp==3.1.0 # via importlib-metadata 89 | 90 | # The following packages are considered to be unsafe in a requirements file: 91 | # setuptools 92 | -------------------------------------------------------------------------------- /notebooks/The_Coronavirus_Curve.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "toc": true 7 | }, 8 | "source": [ 9 | "

Table of Contents

\n", 10 | "
" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "# The Coronavirus Curve - Numberphile" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "[![ben sparks](../images/2020-03-20_ben_sparks.jpg)](https://www.youtube.com/watch?v=k6nLfCbAzgo)" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "
Watch The Coronavirus Curve - Numberphile on YouTube
" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "**Video Description**\n", 39 | "> _Ben Sparks explains (and codes) the so-called SIR Model being used to predict the spread of cornavirus (COVID-19)._\n", 40 | "> \n", 41 | "> **LINKS**\n", 42 | "> - National Health Service (UK) advice on Coronavirus: https://www.nhs.uk/conditions/coronavirus-covid-19/\n", 43 | "> - Ben Sparks: https://www.bensparks.co.uk\n", 44 | "> - Use the Geogebra file Ben created for this video: https://www.geogebra.org/m/nbjfjtpv\n", 45 | "> - Another good file courtesy of Juan Carlos Ponce Campuzano: https://www.geogebra.org/m/utbemrca \n", 46 | "> - Washington Post simulator: https://www.washingtonpost.com/graphics/2020/world/corona-simulator/\n", 47 | "> - Extended presentation by Nick Jewell for MSRI: https://youtu.be/MZ957qhzcjI\n", 48 | "> - More videos with Ben Sparks: http://bit.ly/Sparks_Playlist\n", 49 | "> \n", 50 | "> **SOME OTHER YOUTUBERS ON THIS TOPIC...**\n", 51 | "> - 3blue1brown on the exponential growth of epidemics: https://youtu.be/Kas0tIxDvrg\n", 52 | "> - Tom Crawford on the SIR Model: https://youtu.be/NKMHhm2Zbkw\n", 53 | "> - Kurzgesagt on COVID-19: https://youtu.be/BtN-goy9VOY\n", 54 | "> \n", 55 | "> **NUMBERPHILE**\n", 56 | "> - Website: http://www.numberphile.com/\n", 57 | "> - Numberphile on Facebook: http://www.facebook.com/numberphile\n", 58 | "> - Numberphile tweets: https://twitter.com/numberphile\n", 59 | "> Subscribe: http://bit.ly/Numberphile_Sub\n", 60 | "> \n", 61 | "> Videos by [Brady Haran](https://www.bradyharanblog.com/) ([@BradyHaran](https://twitter.com/BradyHaran))" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "# This is where the magic happens. ✨\n", 71 | "%load_ext nb_black\n", 72 | "\n", 73 | "import matplotlib.image as mpimg\n", 74 | "import numpy as np\n", 75 | "import pandas as pd\n", 76 | "import seaborn as sns\n", 77 | "from IPython.display import Video\n", 78 | "from ipywidgets import interact, interact_manual\n", 79 | "from matplotlib import pyplot as plt\n", 80 | "from matplotlib.animation import FuncAnimation\n", 81 | "from scipy.integrate import solve_ivp" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "%matplotlib inline" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "## The SIR Model (of disease spread)\n", 98 | "The three variables we'll use:\n", 99 | "- **S = Susceptible** (people who are possibly able to get the disease)\n", 100 | "- **I = Infected** (people who have got the disease)\n", 101 | "- **R = Recovered** (people who are not infected any more, may be recovered, may be dead)\n", 102 | "\n", 103 | "**Goal:** build up some simple naïve assumptions of how diseases spread & follow the mathematical consequences to make a prediction." 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "## Set up some initial conditions\n", 113 | "\n", 114 | "# Population of size 1, i.e. 100% (N is between 0 and 1)\n", 115 | "N = 1\n", 116 | "\n", 117 | "# Assume some Infected people (1% are Infected)\n", 118 | "Istart = 0.01\n", 119 | "\n", 120 | "# Assume some people are Susceptible\n", 121 | "Sstart = N - Istart\n", 122 | "\n", 123 | "# Nobody yet has Recovered\n", 124 | "Rstart = 0\n", 125 | "\n", 126 | "print(f\"Starting conditions: N = {N}, S = {Sstart}, I = {Istart}, R = {Rstart}\")" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "## For now, fix these \"rate\" variables\n", 136 | "\n", 137 | "# transm = Transmission/infection rate, how quickly the disease gets transmitted.\n", 138 | "transm = 3.2\n", 139 | "\n", 140 | "# recov = Recovery rate, how quickly people recover, this should be\n", 141 | "# smaller as it takes people longer to recover from a disease.\n", 142 | "recov = 0.23\n", 143 | "\n", 144 | "# maxT = How long we're going to let the model run for.\n", 145 | "maxT = 1" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "### The Rate Equations\n", 153 | "We'll set up some \"rate equations\" (a.k.a. \"differential equations\") that tell us about each variable's rate of change. If you would like to learn more about differential equations, [3Blue1Brown](https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw) has an [_excellent_ series on YouTube here](https://www.youtube.com/watch?v=p_di4Zn4wz4&list=PLZHQObOWTQDNPOjrT6KVlfJuKtYTftqH6) or you could try this [introductory course on Brilliant](https://brilliant.org/courses/differential-equations/).\n", 154 | "\n", 155 | "Differential equations describe the mathematics of change and appear in every branch of science and beyond. They can be used to describe and model anything that changes, from rockets to bodies to stock markets.\n", 156 | "\n", 157 | "The three equations that form the SIR Model are described (using mathematical notation) as follows:\n", 158 | "\n", 159 | "> $\\frac{dS}{dT} = - TransmissionRate * S * I$\n", 160 | "> \n", 161 | "> $\\frac{dI}{dT} = TransmissionRate * S * I - RecoveryRate * I$\n", 162 | "> \n", 163 | "> $\\frac{dR}{dt} = RecoveryRate * I$\n", 164 | "\n", 165 | "Do these make sense?\n", 166 | "- The rate of change of **Susceptibles**, $\\frac{dS}{dT}$, is negative because the rate will go down as more Susceptible people get Infected.\n", 167 | "- The rate of change of **Infected**, $\\frac{dI}{dT}$, is the number who will become Infected next (those who are Susceptible) less those who Recover (the more people become Infected, the more people can Recover).\n", 168 | "- The rate of change of **Recovery**, $\\frac{dR}{dt}$, is decided by how many people are Infected." 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": null, 174 | "metadata": {}, 175 | "outputs": [], 176 | "source": [ 177 | "## Let's write these in Python.\n", 178 | "\n", 179 | "\n", 180 | "def dS_dT(S, I, transm):\n", 181 | " \"\"\"The rate of change of Susceptibles over time.\n", 182 | " \n", 183 | " Args:\n", 184 | " S (float): Total who are Susceptible.\n", 185 | " I (float): Total who are Infected.\n", 186 | " transm (float): transmission rate.\n", 187 | " \n", 188 | " Returns:\n", 189 | " float: rate of change of Suscpetibles.\n", 190 | " \n", 191 | " Examples:\n", 192 | " \n", 193 | " >> dS_dT(S=0.99, I=0.01, transm=3.2)\n", 194 | " -0.03168\n", 195 | " \"\"\"\n", 196 | " # Negative because rate will go down as more Susceptible people get Infected.\n", 197 | " return -transm * S * I\n", 198 | "\n", 199 | "\n", 200 | "def dI_dT(S, I, transm, recov):\n", 201 | " \"\"\"The rate of change of Infected people over time.\n", 202 | " \n", 203 | " Args:\n", 204 | " S (float): Total who are Susceptible.\n", 205 | " I (float): Total who are Infected.\n", 206 | " transm (float): transmission rate.\n", 207 | " recov (float): recovery rate.\n", 208 | " \n", 209 | " Returns:\n", 210 | " float: rate of change of Infected.\n", 211 | " \n", 212 | " Examples:\n", 213 | " \n", 214 | " >> dI_dT(S=0.99, I=0.01, transm=3.2, recov=0.23)\n", 215 | " 0.02938\n", 216 | " \"\"\"\n", 217 | " return (\n", 218 | " transm * S * I # If people were Susceptible, they'll become Infected next.\n", 219 | " - recov * I # The more people become Infected, the more people can Recover.\n", 220 | " )\n", 221 | "\n", 222 | "\n", 223 | "def dR_dT(I, recov):\n", 224 | " \"\"\"The rate of change of Recovered people over time.\n", 225 | " \n", 226 | " Args:\n", 227 | " I (float): Total who are Infected.\n", 228 | " recov (float): recovery rate.\n", 229 | " \n", 230 | " Returns:\n", 231 | " float: rate of change of Recovered.\n", 232 | " \n", 233 | " Examples:\n", 234 | " \n", 235 | " >> dR_dT(I=0.01, recov=0.23)\n", 236 | " 0.0023\n", 237 | " \"\"\"\n", 238 | " return recov * I # Anyone who's Infected can Recover." 239 | ] 240 | }, 241 | { 242 | "cell_type": "markdown", 243 | "metadata": {}, 244 | "source": [ 245 | "### Solve the system of differential equations!\n", 246 | "First we create a single function to hold all three rate equations, because Python's [solver function](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html) wants to be given a single input function, not three." 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": null, 252 | "metadata": {}, 253 | "outputs": [], 254 | "source": [ 255 | "def SIR(t, y):\n", 256 | " \"\"\"\n", 257 | " This function specifies a system of differential equations to be solved,\n", 258 | " and their parameters. We will pass this to the solve_ivp [1]_ function\n", 259 | " from the scipy library.\n", 260 | " \n", 261 | " Args:\n", 262 | " t (float): time step.\n", 263 | " y (list): parameters, in this case a list containing [S, I, R, transm, recov].\n", 264 | " \n", 265 | " Returns:\n", 266 | " list: Calculated values [S, I, R, transm, recov]\n", 267 | " \n", 268 | " Examples:\n", 269 | " \n", 270 | " >>> SIR(t=0, y=[0.99, 0.01, 0.0, 3.2, 0.23])\n", 271 | " [-0.03168, 0.02938, 0.0023]\n", 272 | " \n", 273 | " .. [1] https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html\n", 274 | " \"\"\"\n", 275 | " S, I, R = y\n", 276 | " return [\n", 277 | " dS_dT(S, I, transm),\n", 278 | " dI_dT(S, I, transm, recov),\n", 279 | " dR_dT(I, recov),\n", 280 | " ]\n", 281 | "\n", 282 | "\n", 283 | "# Let's take it for a spin\n", 284 | "SIR(t=0, y=[0.99, 0.01, 0.0])" 285 | ] 286 | }, 287 | { 288 | "cell_type": "markdown", 289 | "metadata": {}, 290 | "source": [ 291 | "Now we can solve the system of differential equations! You can learn more about [scipy's `solve_ivp()` function here](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html)." 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "execution_count": null, 297 | "metadata": {}, 298 | "outputs": [], 299 | "source": [ 300 | "# Solve the system of differential equations!\n", 301 | "solution = solve_ivp(\n", 302 | " fun=SIR, # input function\n", 303 | " t_span=[0, maxT], # start at time 0 and continue until we get to maxT\n", 304 | " t_eval=np.arange(0, maxT, 0.1), # points at which to store the computed solutions\n", 305 | " y0=[Sstart, Istart, Rstart], # initial conditions\n", 306 | ")\n", 307 | "solution" 308 | ] 309 | }, 310 | { 311 | "cell_type": "markdown", 312 | "metadata": {}, 313 | "source": [ 314 | "Let's create a pandas DataFrame with the calculated SIR values (solution.y) in the cells and the time steps (solution.t) as the index." 315 | ] 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": null, 320 | "metadata": {}, 321 | "outputs": [], 322 | "source": [ 323 | "df = pd.DataFrame(\n", 324 | " solution.y.T, columns=[\"Susceptible\", \"Infected\", \"Recovered\"], index=solution.t,\n", 325 | ")\n", 326 | "df" 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": null, 332 | "metadata": {}, 333 | "outputs": [], 334 | "source": [ 335 | "# Visualise the result!\n", 336 | "plot = df.plot(color=[\"blue\", \"red\", \"green\"], lw=2)" 337 | ] 338 | }, 339 | { 340 | "cell_type": "markdown", 341 | "metadata": {}, 342 | "source": [ 343 | "### Visualisation, Numberphile-style\n", 344 | "Let make a helper function which also adds a larger x-axis, and also let's add the official Numberphile brown paper as a background." 345 | ] 346 | }, 347 | { 348 | "cell_type": "code", 349 | "execution_count": null, 350 | "metadata": {}, 351 | "outputs": [], 352 | "source": [ 353 | "background = mpimg.imread(\"../images/numberphile_background.jpg\")\n", 354 | "\n", 355 | "\n", 356 | "def plot_curves(solution, xlim=[0, 10], title=None, add_background=True):\n", 357 | " \"\"\"Helper function that takes a solution and optionally visualises it\n", 358 | " using official Numberphile brown paper.\n", 359 | " \n", 360 | " Args:\n", 361 | " solution (scipy.integrate._ivp.ivp.OdeResult): Output of solve_ivp() function.\n", 362 | " xlim (list): x-axis limits in format [min, max].\n", 363 | " title (str): Optional graph title.\n", 364 | " add_background (bool): Add Numberphile brown paper background?\n", 365 | " \n", 366 | " Returns:\n", 367 | " matplotlib graph of SIR model curves.\n", 368 | " \n", 369 | " Examples:\n", 370 | " \n", 371 | " >>> solution = solve_ivp(SIR, t_span=[0, maxT], t_eval=np.arange(0, maxT, 0.1),\n", 372 | " y0=[Sstart, Istart, Rstart])\n", 373 | " >>> plot_curves(solution, title=\"The SIR Model of disease spread\")\n", 374 | " \"\"\"\n", 375 | " # Set up plot\n", 376 | " fig, ax = plt.subplots(figsize=(14, 6))\n", 377 | " plt.title(title, fontsize=15)\n", 378 | " plt.xlabel(\"Time\", fontsize=12)\n", 379 | " plt.ylabel(\"Percentage of population\", fontsize=12)\n", 380 | " # Create DataFrame\n", 381 | " df = pd.DataFrame(\n", 382 | " solution.y.T,\n", 383 | " columns=[\"Susceptible\", \"Infected\", \"Recovered\"],\n", 384 | " index=solution.t,\n", 385 | " )\n", 386 | " # Make the plot\n", 387 | " plot = df.plot(color=[\"blue\", \"red\", \"green\"], lw=2, ax=ax)\n", 388 | " plot.set_xlim(xlim[0], xlim[1])\n", 389 | " # Add background?\n", 390 | " if add_background:\n", 391 | " plot.imshow(\n", 392 | " background,\n", 393 | " aspect=plot.get_aspect(),\n", 394 | " extent=plot.get_xlim() + plot.get_ylim(),\n", 395 | " zorder=1,\n", 396 | " )\n", 397 | "\n", 398 | "\n", 399 | "plot_curves(solution, title=\"The SIR Model of disease spread\")" 400 | ] 401 | }, 402 | { 403 | "cell_type": "markdown", 404 | "metadata": {}, 405 | "source": [ 406 | "Let's also create another helper function that plugs into the ipywidgets `interact`, so we can play around with the parameters." 407 | ] 408 | }, 409 | { 410 | "cell_type": "code", 411 | "execution_count": null, 412 | "metadata": {}, 413 | "outputs": [], 414 | "source": [ 415 | "def solve_and_plot(\n", 416 | " Istart=0.01,\n", 417 | " Rstart=0,\n", 418 | " transm=3.2,\n", 419 | " recov=0.23,\n", 420 | " maxT=20,\n", 421 | " title=None,\n", 422 | " add_background=True,\n", 423 | "):\n", 424 | " \"\"\"Helper function so we can play around with the parameters using the interact ipywidget.\n", 425 | " \n", 426 | " Args:\n", 427 | " Istart (float): Starting value for Infected (as percent of population).\n", 428 | " Rstart (float): Starting value for Recovered (as percent of population).\n", 429 | " transm (float): transmission rate.\n", 430 | " recov (float): recovery rate.\n", 431 | " maxT (int): maximum time step.\n", 432 | " title (str): Optional graph title.\n", 433 | " add_background (bool): Optionally add Numberphile background.\n", 434 | " \n", 435 | " Returns:\n", 436 | " matplotlib graph of SIR model curves.\n", 437 | " \n", 438 | " Examples:\n", 439 | " \n", 440 | " >>> solve_and_plot(maxT=20, title=\"Set maxT = 20\")\n", 441 | " \"\"\"\n", 442 | "\n", 443 | " N = 1\n", 444 | " Sstart = N - Istart\n", 445 | "\n", 446 | " def SIR(t, y):\n", 447 | " \"\"\"We need to redefine this inside solve_and_plot() otherwise it\n", 448 | " won't pick up any changes to transm or recov.\n", 449 | " \"\"\"\n", 450 | " S, I, R = y\n", 451 | " return [\n", 452 | " dS_dT(S, I, transm),\n", 453 | " dI_dT(S, I, transm, recov),\n", 454 | " dR_dT(I, recov),\n", 455 | " ]\n", 456 | "\n", 457 | " solution = solve_ivp(\n", 458 | " fun=SIR,\n", 459 | " t_span=[0, maxT],\n", 460 | " t_eval=np.arange(0, maxT, 0.1),\n", 461 | " y0=[Sstart, Istart, Rstart],\n", 462 | " )\n", 463 | " plot_curves(solution, xlim=[0, maxT], title=title, add_background=add_background)\n", 464 | "\n", 465 | "\n", 466 | "# Let's set maxT to 20 to see how things pan out\n", 467 | "solve_and_plot(maxT=20, title=\"Set maxT = 20\")" 468 | ] 469 | }, 470 | { 471 | "cell_type": "markdown", 472 | "metadata": {}, 473 | "source": [ 474 | "### Adding interactivity\n", 475 | "We can make this interactive using the interact function from ipywidgets!\n", 476 | "\n", 477 | "> Note: we use `interact_manual` in order to add a button as the graph can take a couple seconds to update." 478 | ] 479 | }, 480 | { 481 | "cell_type": "code", 482 | "execution_count": null, 483 | "metadata": { 484 | "scrolled": false 485 | }, 486 | "outputs": [], 487 | "source": [ 488 | "interact_manual(\n", 489 | " solve_and_plot,\n", 490 | " Istart=(0, 1, 0.01),\n", 491 | " Rstart=(0, 1, 0.01),\n", 492 | " transm=(0, 10, 0.01),\n", 493 | " recov=(0, 1, 0.01),\n", 494 | " maxT=(0, 20, 1),\n", 495 | " title=\"\",\n", 496 | ")" 497 | ] 498 | }, 499 | { 500 | "cell_type": "markdown", 501 | "metadata": {}, 502 | "source": [ 503 | "---\n", 504 | "\n", 505 | "[![](../images/2020-03-20_ben_sparks_2.jpg)](https://www.youtube.com/watch?v=k6nLfCbAzgo&t=850)\n", 506 | "
(click the image to be taken to the equivalent part of the Numberphile video)
\n", 507 | "\n", 508 | "> \"In this model, almost 80% of the population got it at once. If you project that to our NHS, that's a problem.\"\n", 509 | "---" 510 | ] 511 | }, 512 | { 513 | "cell_type": "markdown", 514 | "metadata": {}, 515 | "source": [ 516 | "## Flatten The Curve\n", 517 | "If we disable the background then re-drawing the graph becomes fast enough to make changes to the parameters in real-time! Have a play, see if you can squash the sombrero!" 518 | ] 519 | }, 520 | { 521 | "cell_type": "code", 522 | "execution_count": null, 523 | "metadata": {}, 524 | "outputs": [], 525 | "source": [ 526 | "interact(\n", 527 | " solve_and_plot,\n", 528 | " Istart=(0, 1, 0.01),\n", 529 | " Rstart=(0, 1, 0.01),\n", 530 | " transm=(0, 10, 0.01),\n", 531 | " recov=(0, 1, 0.01),\n", 532 | " maxT=(0, 50, 1),\n", 533 | " title=\"\",\n", 534 | " add_background=False,\n", 535 | ")" 536 | ] 537 | }, 538 | { 539 | "cell_type": "markdown", 540 | "metadata": {}, 541 | "source": [ 542 | "### Social distancing\n", 543 | "Let's try decreasing the transmission rate (e.g. implement social distancing). Notice how the red curve flattens as the transmission rate is reduced." 544 | ] 545 | }, 546 | { 547 | "cell_type": "code", 548 | "execution_count": null, 549 | "metadata": {}, 550 | "outputs": [], 551 | "source": [ 552 | "# Transmission rate = 1.4\n", 553 | "solve_and_plot(\n", 554 | " transm=1.4,\n", 555 | " recov=0.23,\n", 556 | " maxT=15,\n", 557 | " add_background=False,\n", 558 | " title=\"Transmission rate = 1.4\",\n", 559 | ")\n", 560 | "\n", 561 | "# Transmission rate = 0.8\n", 562 | "solve_and_plot(\n", 563 | " transm=0.8,\n", 564 | " recov=0.23,\n", 565 | " maxT=15,\n", 566 | " add_background=False,\n", 567 | " title=\"Transmission rate = 0.8\",\n", 568 | ")" 569 | ] 570 | }, 571 | { 572 | "cell_type": "markdown", 573 | "metadata": {}, 574 | "source": [ 575 | "If you reduce the transmission rate enough, it turns out that not _everyone_ ends up being infected. There's still 10% - 20% of people who remain susceptible in the long run but they never get the disease.\n", 576 | "\n", 577 | "This is what we're hoping for with COVID-19, as there are some people who will not survive the disease. If you can stop the 10% most vulnerable from getting it, this would be great." 578 | ] 579 | }, 580 | { 581 | "cell_type": "code", 582 | "execution_count": null, 583 | "metadata": {}, 584 | "outputs": [], 585 | "source": [ 586 | "solve_and_plot(\n", 587 | " transm=0.6,\n", 588 | " recov=0.23,\n", 589 | " maxT=50,\n", 590 | " add_background=False,\n", 591 | " title=\"Transmission rate = 0.6\",\n", 592 | ")" 593 | ] 594 | }, 595 | { 596 | "cell_type": "markdown", 597 | "metadata": {}, 598 | "source": [ 599 | "### Increasing the recovery rate\n", 600 | "Increasing the recovery rate also flattens the curve but this is much harder to do. The NHS is currently doing this by helping people to recover quickly, but in practice we know it takes people a week to recover and sometimes a lot longer." 601 | ] 602 | }, 603 | { 604 | "cell_type": "code", 605 | "execution_count": null, 606 | "metadata": {}, 607 | "outputs": [], 608 | "source": [ 609 | "solve_and_plot(\n", 610 | " transm=3, recov=0.2, maxT=50, add_background=False, title=\"Recovery rate = 0.2\"\n", 611 | ")\n", 612 | "solve_and_plot(\n", 613 | " transm=3, recov=0.9, maxT=50, add_background=False, title=\"Recovery rate = 0.9\"\n", 614 | ")" 615 | ] 616 | }, 617 | { 618 | "cell_type": "markdown", 619 | "metadata": {}, 620 | "source": [ 621 | "## R0: the basic reproductive number\n", 622 | "You may have heard people talking about [R0, the basic reproductive number](https://en.wikipedia.org/wiki/Basic_reproduction_number):\n", 623 | "- An R0 of 2-3 means that every infected person infects, on average, two to three others.\n", 624 | "- An R0 less than 1 indicates that each infected person results in fewer than one new infection. When this happens, the outbreak will slowly grind to a halt.\n", 625 | "- For COVID-19, [current estimates predict an R0 between 1.4-3.9](https://en.wikipedia.org/wiki/Basic_reproduction_number)." 626 | ] 627 | }, 628 | { 629 | "cell_type": "code", 630 | "execution_count": null, 631 | "metadata": {}, 632 | "outputs": [], 633 | "source": [ 634 | "# Let's calculate R0. This can be calculated as transm / recov.\n", 635 | "transm = 0.5\n", 636 | "recov = 0.14\n", 637 | "R_0 = transm / recov\n", 638 | "print(f\"R_0 = {R_0}\")" 639 | ] 640 | }, 641 | { 642 | "cell_type": "markdown", 643 | "metadata": {}, 644 | "source": [ 645 | "If the COVIF-19 R0 is 3, let's see what that looks like. (Remember that the units of time doesn't mean too much here.)" 646 | ] 647 | }, 648 | { 649 | "cell_type": "code", 650 | "execution_count": null, 651 | "metadata": {}, 652 | "outputs": [], 653 | "source": [ 654 | "solve_and_plot(\n", 655 | " transm=0.5,\n", 656 | " recov=0.14,\n", 657 | " maxT=30,\n", 658 | " add_background=False,\n", 659 | " title=f\"Transmission rate = 0.5, Recovery rate = 0.2, R0 = {0.5/0.14:.1f}\",\n", 660 | ")" 661 | ] 662 | }, 663 | { 664 | "cell_type": "markdown", 665 | "metadata": {}, 666 | "source": [ 667 | "We'll leave it to you to play with the sliders and get a feel for how each input changes the landscape of how the infection could play out." 668 | ] 669 | }, 670 | { 671 | "cell_type": "code", 672 | "execution_count": null, 673 | "metadata": {}, 674 | "outputs": [], 675 | "source": [ 676 | "interact(\n", 677 | " solve_and_plot,\n", 678 | " Istart=(0, 1, 0.01),\n", 679 | " Rstart=(0, 1, 0.01),\n", 680 | " transm=(0, 10, 0.01),\n", 681 | " recov=(0, 1, 0.01),\n", 682 | " maxT=(0, 50, 1),\n", 683 | " title=\"The Coronavirus Curve\",\n", 684 | " add_background=False,\n", 685 | ")" 686 | ] 687 | }, 688 | { 689 | "cell_type": "markdown", 690 | "metadata": {}, 691 | "source": [ 692 | "## Lights, camera, animate!\n", 693 | "Let's create an animated chart in Python for maximum social media virality." 694 | ] 695 | }, 696 | { 697 | "cell_type": "code", 698 | "execution_count": null, 699 | "metadata": {}, 700 | "outputs": [], 701 | "source": [ 702 | "# First, let's run the numbers\n", 703 | "transm = 2.4\n", 704 | "recov = 0.3\n", 705 | "maxT = 13\n", 706 | "\n", 707 | "\n", 708 | "def SIR(t, y):\n", 709 | " S, I, R = y\n", 710 | " return [\n", 711 | " dS_dT(S, I, transm),\n", 712 | " dI_dT(S, I, transm, recov),\n", 713 | " dR_dT(I, recov),\n", 714 | " ]\n", 715 | "\n", 716 | "\n", 717 | "solution = solve_ivp(\n", 718 | " fun=SIR,\n", 719 | " t_span=[0, maxT],\n", 720 | " t_eval=np.arange(0, maxT, 0.1),\n", 721 | " y0=[Sstart, Istart, Rstart],\n", 722 | ")\n", 723 | "df = pd.DataFrame(\n", 724 | " solution.y.T, columns=[\"Susceptible\", \"Infected\", \"Recovered\"], index=solution.t,\n", 725 | ")\n", 726 | "df.head()" 727 | ] 728 | }, 729 | { 730 | "cell_type": "markdown", 731 | "metadata": {}, 732 | "source": [ 733 | "We need to enable matplotlib's \"notebook\" plotting mode in order to see the graphs animating live in the notebook." 734 | ] 735 | }, 736 | { 737 | "cell_type": "code", 738 | "execution_count": null, 739 | "metadata": {}, 740 | "outputs": [], 741 | "source": [ 742 | "%matplotlib notebook\n", 743 | "%matplotlib notebook\n", 744 | "# See here for why we call this twice: https://github.com/ipython/ipython/issues/10873" 745 | ] 746 | }, 747 | { 748 | "cell_type": "markdown", 749 | "metadata": {}, 750 | "source": [ 751 | "First, let's animate without the Numberphile background." 752 | ] 753 | }, 754 | { 755 | "cell_type": "code", 756 | "execution_count": null, 757 | "metadata": {}, 758 | "outputs": [], 759 | "source": [ 760 | "def animate(i):\n", 761 | " \"\"\"We put all the animation update code into one function\n", 762 | " that loads the first i rows from the dataframe df.\"\"\"\n", 763 | " ax.clear()\n", 764 | " data = df.iloc[: i + 1]\n", 765 | " plot = data.plot(color=[\"blue\", \"red\", \"green\"], lw=4, ax=ax)\n", 766 | " plt.title(\"The Coronavirus Curve\", fontsize=15)\n", 767 | " plt.xlabel(\"Time (units)\", fontsize=12)\n", 768 | " plt.ylabel(\"Percentage of population\", fontsize=12)\n", 769 | " plot.set_xlim(0, maxT)\n", 770 | " plot.set_ylim(0, 1)\n", 771 | "\n", 772 | "\n", 773 | "fig, ax = plt.subplots(figsize=(14, 6))\n", 774 | "animate(0) # initialise the plot\n", 775 | "ani = FuncAnimation(\n", 776 | " fig, animate, frames=list(range(len(df))), repeat=False, interval=50, blit=True,\n", 777 | ")\n", 778 | "plt.show()" 779 | ] 780 | }, 781 | { 782 | "cell_type": "markdown", 783 | "metadata": {}, 784 | "source": [ 785 | "The live animation is a bit slow as it's rendering in real time, but you can also save the full-speed animation as a smooth movie." 786 | ] 787 | }, 788 | { 789 | "cell_type": "code", 790 | "execution_count": null, 791 | "metadata": {}, 792 | "outputs": [], 793 | "source": [ 794 | "ani.save(\"../images/coronavirus_curve_no_background.mp4\")" 795 | ] 796 | }, 797 | { 798 | "cell_type": "code", 799 | "execution_count": null, 800 | "metadata": {}, 801 | "outputs": [], 802 | "source": [ 803 | "Video(\"../images/coronavirus_curve_no_background.mp4\", width=1000)" 804 | ] 805 | }, 806 | { 807 | "cell_type": "markdown", 808 | "metadata": {}, 809 | "source": [ 810 | "
\n", 811 | "Danger: This next cell may be quite slow to run. Proceed with caution.\n", 812 | "
" 813 | ] 814 | }, 815 | { 816 | "cell_type": "markdown", 817 | "metadata": {}, 818 | "source": [ 819 | "Let's now add the Numberphile brown paper background." 820 | ] 821 | }, 822 | { 823 | "cell_type": "code", 824 | "execution_count": null, 825 | "metadata": {}, 826 | "outputs": [], 827 | "source": [ 828 | "# N.B. this may be very slow\n", 829 | "\n", 830 | "\n", 831 | "def animate(i):\n", 832 | " ax.clear()\n", 833 | " data = df.iloc[: i + 1]\n", 834 | " plot = data.plot(color=[\"blue\", \"red\", \"green\"], lw=4, ax=ax)\n", 835 | " plt.title(\"The Coronavirus Curve\", fontsize=15)\n", 836 | " plt.xlabel(\"Time (units)\", fontsize=12)\n", 837 | " plt.ylabel(\"Percentage of population\", fontsize=12)\n", 838 | " plot.set_xlim(0, maxT)\n", 839 | " plot.set_ylim(0, 1)\n", 840 | " plot.imshow(\n", 841 | " background,\n", 842 | " aspect=plot.get_aspect(),\n", 843 | " extent=plot.get_xlim() + plot.get_ylim(),\n", 844 | " zorder=1,\n", 845 | " )\n", 846 | "\n", 847 | "\n", 848 | "fig, ax = plt.subplots(figsize=(14, 6))\n", 849 | "background = mpimg.imread(\"../images/numberphile_background.jpg\")\n", 850 | "animate(0)\n", 851 | "ani = FuncAnimation(\n", 852 | " fig, animate, frames=list(range(len(df))), repeat=False, interval=50, blit=True,\n", 853 | ")" 854 | ] 855 | }, 856 | { 857 | "cell_type": "markdown", 858 | "metadata": {}, 859 | "source": [ 860 | "This is definitely quite slow, so let's save it as a full screen movie that you can be proud to share with friends & family." 861 | ] 862 | }, 863 | { 864 | "cell_type": "code", 865 | "execution_count": null, 866 | "metadata": {}, 867 | "outputs": [], 868 | "source": [ 869 | "ani.save(\"../images/coronavirus_curve.mp4\")" 870 | ] 871 | }, 872 | { 873 | "cell_type": "code", 874 | "execution_count": null, 875 | "metadata": {}, 876 | "outputs": [], 877 | "source": [ 878 | "Video(\"../images/coronavirus_curve.mp4\", width=1000)" 879 | ] 880 | }, 881 | { 882 | "cell_type": "markdown", 883 | "metadata": {}, 884 | "source": [ 885 | "---\n", 886 | "\n", 887 | "
\n", 888 | " Congratulations!\n", 889 | "

\n", 890 | " If you enjoyed this notebook please consider subscribing to Numberphile on YouTube. You can see more of Ben Sparks' videos here.\n", 891 | "


\n", 892 | "

Disclaimer: This notebook and its creator are not affiliated, associated, authorised, endorsed by, or in any way officially connected with Brady Haran, Ben Sparks, Numberphile or its affiliates. We made this simply because they inspire us.

\n", 893 | "
\n", 894 | "\n", 895 | "---\n", 896 | "\n", 897 | "
\n", 898 | " About\n", 899 | "

\n", 900 | " This notebook has been made by @John_Sandall. I run training workshops in Python, data science and data engineering.\n", 901 | "


\n", 902 | "

\n", 903 | " If you are interested in registering for our paid workshops in Python for data science and engineering, you can sign up to our mailing list here.\n", 904 | "


\n", 905 | "

\n", 906 | " You can follow my free First Steps with Python and First Steps with pandas workshops for free as part of PyData Bristol's Zero To Hero 2020 monthly free workshop series. PyData Bristol will be running more free virtual workshops over the coming months so sign up via Meetup.com or follow us @PyDataBristol on Twitter.\n", 907 | "


\n", 908 | "

\n", 909 | " I am the Founder of data science consultancy Coefficient. If you would like to work with us, our team can help you with your data science, software engineering and machine learning projects as an on-demand resource. We can also create bespoke training workshops adapted to your industry, virtual or in-person, with training clients currently including BNP Paribas, EY, the Met Police and the BBC.\n", 910 | "

\n", 911 | "
\n", 912 | "\n", 913 | "---\n", 914 | "\n", 915 | "
\n", 916 | " COVID-19\n", 917 | " \n", 931 | "
" 932 | ] 933 | } 934 | ], 935 | "metadata": { 936 | "kernelspec": { 937 | "display_name": "Python (numberphile)", 938 | "language": "python", 939 | "name": "numberphile" 940 | }, 941 | "language_info": { 942 | "codemirror_mode": { 943 | "name": "ipython", 944 | "version": 3 945 | }, 946 | "file_extension": ".py", 947 | "mimetype": "text/x-python", 948 | "name": "python", 949 | "nbconvert_exporter": "python", 950 | "pygments_lexer": "ipython3", 951 | "version": "3.7.7" 952 | }, 953 | "toc": { 954 | "base_numbering": 1, 955 | "nav_menu": {}, 956 | "number_sections": true, 957 | "sideBar": true, 958 | "skip_h1_title": true, 959 | "title_cell": "Table of Contents", 960 | "title_sidebar": "Contents", 961 | "toc_cell": true, 962 | "toc_position": { 963 | "height": "calc(100% - 180px)", 964 | "left": "10px", 965 | "top": "150px", 966 | "width": "232px" 967 | }, 968 | "toc_section_display": true, 969 | "toc_window_display": true 970 | } 971 | }, 972 | "nbformat": 4, 973 | "nbformat_minor": 4 974 | } 975 | --------------------------------------------------------------------------------