<jupyter_start><jupyter_text>This is my fourth post on modeling the Coronavirus epidemic.  I recommend starting with [the first post](http://www.davidketcheson.info/2020/03/17/SIR_model.html) and reading them in order.So far, we've [learned about the SIR model](http://www.davidketcheson.info/2020/03/17/SIR_model.html) and [used available data](http://www.davidketcheson.info/2020/03/19/SIR_Estimating_parameters.html) combined with the model to [predict the epidemic](http://www.davidketcheson.info/2020/03/19/SIR_predictions.html).  We're now going to detour into some additional mathematical ideas that will help us get further understanding.  Exponential growthIn the second post, we saw that the initial spread of a disease follows a differential equation of the form$$\frac{dI}{dt} = \beta I(t).$$Once you're comfortable with the idea that $dI/dt$ just means "the rate of change of $I$", you realize that this is one of the simplest equations imaginable.  So it shouldn't be surprising that this equation comes up a lot in the real world.  Its solution is$$I(t) = e^{\beta t} I(0).$$Here $e\approx 2.72$ is **Euler's number**.  This equation tells us that the number of infected grows very quickly.  In the case of Coronavirus, the number $I$ can double in about 3 days ([you can see more estimates of the doubling time here](https://ourworldindata.org/coronavirusgrowth-country-by-country-view)).  We refer to this kind of growth (where a given quantity doubles over a certain time interval) as **exponential growth**.  Financial advisors love to talk about the power of exponential growth because that's also how compound interest works.  Of course, with most investments it takes a lot more than 3 days to double your money, but the principle is the same.  In fact, exponentially growing functions are all around us, and learning how they behave can help you to understand a lot of things. Let's look again at our first prediction from the last post:<jupyter_code>%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import matplotlib.dates as dates
from scipy.integrate import solve_ivp
import pandas as pd
plt.style.use('seaborn-poster')
matplotlib.rcParams['figure.figsize'] = (10., 6.)
def SIR(beta=0.25, gamma=0.05, N = 7770000000, confirmed_fraction=0.15,T=400,plot='SIR',
        print_params=True,ymax=None):
    """ Model the current outbreak using the SIR model."""

    du = np.zeros(3)
    u0 = np.zeros(3)
    
    def f(t,u):
        du[0] = -beta*u[1]*u[0]/N
        du[1] = beta*u[1]*u[0]/N - gamma*u[1]
        du[2] = gamma*u[1]
        return du

    # Initial values
    total_cases = 199258./confirmed_fraction
    u0[2] = 81972./confirmed_fraction+7956  # Initial recovered
    u0[1] = total_cases-u0[2] # Initial infected
    u0[0] = N - u0[1] - u0[2]

    #dt = 0.01
    times = np.arange(0,T)
    solution = solve_ivp(f,[0,T],u0,t_eval=times)
    S = solution.y[0,:]
    I = solution.y[1,:]
    R = solution.y[2,:]

    today = '19/03/2020'
    start = dates.datestr2num(today)
    mydates = np.arange(T)+start
    
    fig = plt.figure(figsize=(12,8)); leg=[]
    if 'S' in plot:
        leg.append('Susceptible')
        plt.plot_date(mydates,S/1.e9,'-b',lw=3)
    if 'I' in plot:
        leg.append('Infected')
        plt.plot_date(mydates,I/1.e9,'-',lw=3,color='brown')
    if 'R' in plot:
        leg.append('Recovered')
        plt.plot_date(mydates,R/1.e9,'-g',lw=3)
    ax = plt.gca()
    ax.xaxis.set_major_locator(dates.MonthLocator())
    ax.xaxis.set_major_formatter(dates.DateFormatter('%b'))
    fig.autofmt_xdate()
    plt.legend(leg)
    plt.ylabel('Individuals (in billions)')
    plt.xlim(737503,737503+T)
    if ymax: plt.ylim(-ymax/10,ymax)
    if print_params:
        plt.text(737503+3*T/4, 6,r'$\beta$ = {}, $\gamma$ = {}'.format(beta,gamma),fontsize=15,bbox=dict(facecolor='white', alpha=1.0))
    return fig

SIR(beta=0.25, gamma=0.05, confirmed_fraction=0.15, plot='I',print_params=False);<jupyter_output><empty_output><jupyter_text>Right now, we are at the very left edge of this plot.  How many people are infected at the start of the plot?  It looks like zero, but we know there are over 100 thousand current cases.  It just looks like zero because the scale here is in billions, and 100 thousand is much too small to see on that scale!  This is a common problem with exponentially growing functions.  We can try to solve the problem by zooming in on the left end of the graph, showing results for just the next 30 days:<jupyter_code>SIR(T=30,plot='I',print_params=False);<jupyter_output><empty_output><jupyter_text>It's a little better, but even on this scale the current number of infections is too small to be seen.  We can try again to fix it by just changing the vertical scale:<jupyter_code>SIR(T=30,plot='I',print_params=False,ymax=0.01);<jupyter_output><empty_output><jupyter_text>Now we can see that the starting value is not zero, but we can't see the right part of the graph at all!  Let's find a better solution.  Logarithmic scalingIn the plots above, the scale of the $y$-axis is **linear**.  That means that equal distances in $y$ represent equal changes in the value of the function.  The problem with using this for an exponentially growing function is that the changes in the function at early times are tiny compared to the later growth.Instead, we can visualize the growth using a **logarithmic** scale:<jupyter_code>def SIR_semilog(beta=0.25, gamma=0.05, N = 7770000000, confirmed_fraction=0.15,T=400,plot='SIR',
        print_params=True,ymax=None):
    """ Model the current outbreak using the SIR model."""

    du = np.zeros(3)
    u0 = np.zeros(3)
    
    def f(t,u):
        du[0] = -beta*u[1]*u[0]/N
        du[1] = beta*u[1]*u[0]/N - gamma*u[1]
        du[2] = gamma*u[1]
        return du

    # Initial values
    total_cases = 199258./confirmed_fraction
    u0[2] = 81972./confirmed_fraction+7956  # Initial recovered
    u0[1] = total_cases-u0[2] # Initial infected
    u0[0] = N - u0[1] - u0[2]

    #dt = 0.01
    times = np.arange(0,T)
    solution = solve_ivp(f,[0,T],u0,t_eval=times)
    S = solution.y[0,:]
    I = solution.y[1,:]
    R = solution.y[2,:]

    today = '19/03/2020'
    start = dates.datestr2num(today)
    mydates = np.arange(T)+start
    
    fig = plt.figure(figsize=(12,8)); leg=[]
    if 'S' in plot:
        leg.append('Susceptible')
        plt.semilogy(mydates,S/1.e9,'-b',lw=3)
    if 'I' in plot:
        leg.append('Infected')
        plt.semilogy(mydates,I/1.e9,'-',lw=3,color='brown')
    if 'R' in plot:
        leg.append('Recovered')
        plt.semilogy(mydates,R/1.e9,'-g',lw=3)
    ax = plt.gca()
    ax.xaxis.set_major_locator(dates.MonthLocator())
    ax.xaxis.set_major_formatter(dates.DateFormatter('%b'))
    fig.autofmt_xdate()
    plt.legend(leg)
    plt.ylabel('Individuals (in billions)')
    plt.xlim(737503,737503+T)
    if ymax: plt.ylim(-ymax/10,ymax)
    if print_params:
        plt.text(737503+3*T/4, 6,r'$\beta$ = {}, $\gamma$ = {}'.format(beta,gamma),fontsize=15,bbox=dict(facecolor='white', alpha=1.0))
    return fig
SIR_semilog(plot='I',T=40,print_params=False);<jupyter_output><empty_output><jupyter_text>Look carefully at the $y$ axis here.  As you can see, each of the evenly space labels on the axis represents a value **10 times greater** than the value below it.  In other words, equal distances on this axis represent equal **ratios**.  The great thing about this scaling is that we can easily see how the function varies over the whole graph.  It might seem strange that it looks almost like a straight line, but that's exactly how an exponentially growing function should look on this kind of plot.  Remember, equal distances in $y$ represent equal ratios, and we said that this kind of function doubles over each interval of some fixed size. With this plot, it's easy to answer questions like "when do we expect to have more than 1 million cases?"  We couldn't possibly answer that by looking at the previous plots.We can also see more easily how rapidly the epidemic goes from something small to a true global crisis.  **At present there are less than a million cases, but before the end of April we would have (without mitigation) over 1 billion**.  Never in history have that many human beings been ill at the same time with a single disease. In technical terms, we say this is a **semi-log plot**, because the $y$ axis is logarithmic while the $x$ axis is linear.  If both the $x$ and $y$ axes were logarithmic, we would call it a **log-log plot**.Understanding logarithmic plots is a bit of a superpower, because it allows you to study data by looking at plots like the last one above, which let you simultaneously see the parts where the function is small and big.  The model predictions on a log scale Here's a semilog plot of our basic model for the epidemic over the next year:<jupyter_code>SIR_semilog(plot='SIR',T=365,print_params=False);<jupyter_output><empty_output><jupyter_text>Remember, this is exactly the same data that's in the first plot near the top of this post.  We're just looking at it in a different way.  But with this new plot we can much more easily see how many susceptibles remain at the end of a year: about a tenth of a billion, or 100 million.  We can also see that there are still about 1000 infected people in this model at the end of a year.  Notice that after the infection peaks, it declines in a way that also looks like a straight (downward-trending) line on this plot; that means that the decrease is also **exponential**.  In other words, after we pass the peak of infection, the number of infected individuals will consistently reduce by a factor of two over a certain time interval.  Looking back at the model, we see that the rate of decrease is determined by $\gamma$.  In the late stages of the epidemic, because the fraction of susceptible people is small, we have approximately:$$\frac{dI}{dt} = -\gamma I(t)$$whose solution is$$I(t+\tau) = e^{-\gamma \tau} I(t),$$which, for our estimate $\gamma \approx 0.05$, implies that the number of infections is reduced by half about every 14 days.  Assessing the effectiveness of mitigationArmed with our new superpower, let's look at our data from specific countries with this logarithmic scaling.  We'll start again with Italy.<jupyter_code>url = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_19-covid-Confirmed.csv"
data = pd.read_csv(url)
today = '3/19/20'  # Update this to include more data
days = pd.date_range(start='1/22/20',end=today)
dd = np.arange(len(days))
row=data['Country/Region'].tolist().index('Italy')
total_cases = [data[day.strftime('%-m/%-d/%y')][row] for day in days]
plt.semilogy(total_cases[:],'.k');
plt.xlabel('Days from January 22nd'); plt.ylabel('Total cases');<jupyter_output><empty_output><jupyter_text>Several things become clearer with this scaling.  Notice that the plot is **not** a straight line, but we can make good guesses as to why.  Before day 30 (February 21st), there were only one or two known cases, and after day 30 there was a very abrupt increase.  It seems likely that the virus was spreading before day 30 and the new cases were only detected later.  This matches with [statements from experts in Italy](https://en.wikipedia.org/wiki/2020_coronavirus_pandemic_in_Italycite_note-250).Recall our full model equation for $I(t)$:$$\frac{dI}{dt} = (\beta \frac{S}{N}-\gamma) I \\$$Since only a tiny fraction of the whole Italian population is infected, we have $S/N\approx 1$, so the slope of our exponential growth line on a semilog plot should be $\beta-\gamma \approx 0.2$.  Let's see how a line with that slope matches the data:<jupyter_code>beta=0.25; gamma=0.05;
plt.semilogy(total_cases[:],'.k');
plt.xlabel('Days from January 22nd'); plt.ylabel('Total cases');
beta=0.25; gamma=0.05;
plt.semilogy(np.arange(7,58),np.exp((beta-gamma)*dd[7:58])/1.5);
plt.legend(['Infected cases',r'Exponential growth with rate $\beta-\gamma = 0.2$']);
plt.title('Italy');<jupyter_output><empty_output><jupyter_text>Note that we are doing essentially the same thing that we did when trying to determine $\beta$ in the second post of this series; but now we are looking at the results on a log scale to reveal more detail.It seems plausible (not utterly convincing) that the virus has been spreading at this expected rate in Italy for about 50 days now.  However, notice that in the last week the slope seems to have decreased.  Since Italy is now making great efforts to detect all new cases, it seems most likely that this is the result of mitigation.  It's probably too soon to try to assess the effectiveness of that mitigation, but let's make an attempt anyway:<jupyter_code>plt.semilogy(total_cases[:],'.k');
plt.xlabel('Days from January 22nd'); plt.ylabel('Total cases');
beta=0.25; gamma=0.05;
plt.semilogy(np.arange(7,50),np.exp((beta-gamma)*dd[7:50])/1.5);
plt.semilogy(np.arange(51,58),np.exp(0.14*dd[51:58])*14);
plt.legend(['Infected cases',
            r'Exponential growth with rate $\beta-\gamma = 0.2$',
            r'Exponential growth with rate $0.14$']);
plt.title('Italy');<jupyter_output><empty_output><jupyter_text>We see that the slope over the last several days is about 0.14, corresponding to a mitigation factor $q \approx 0.7$ in the model I introduced in the last post.<jupyter_code>row=data['Country/Region'].tolist().index('Spain')
total_cases = [data[day.strftime('%-m/%-d/%y')][row] for day in days]

plt.semilogy(total_cases[:],'.k');
plt.xlabel('Days from January 22nd'); plt.ylabel('Total cases');
beta=0.25; gamma=0.05;
plt.semilogy(np.arange(8,58),np.exp((beta-gamma)*dd[8:58])/5);
plt.legend(['Infected cases',r'Exponential growth with rate $\beta-\gamma = 0.2$']);
plt.title('Spain');<jupyter_output><empty_output><jupyter_text>For Spain, we see a similar pattern, but no sign of any impact from mitigation yet.<jupyter_code>row=data['Country/Region'].tolist().index('Korea, South')
total_cases = [data[day.strftime('%-m/%-d/%y')][row] for day in days]

plt.semilogy(total_cases[:],'.k');
plt.xlabel('Days from January 22nd'); plt.ylabel('Total cases');
beta=0.25; gamma=0.05;
plt.semilogy(np.arange(0,58),np.exp((beta-gamma)*dd[0:58])/0.7);
plt.legend(['Infected cases',r'Exponential growth with rate $\beta-\gamma = 0.2$']);
plt.title('Korea, South');<jupyter_output><empty_output><jupyter_text>For South Korea, up to about day 40 we see the same pattern (with again a similar plateau in late February followed by a rapid rise when testing increased).  But afterward we see completely different behavior, as the curve flattens.  Given that South Korea has perhaps the most agressive testing strategy in the world, it seems unlikely that this can be attributed to new infections going undetected.  Instead, the evidence suggests that mitigation strategies have been very successful.  We can measure this success by looking at how much the slope of the curve has changed:<jupyter_code>row=data['Country/Region'].tolist().index('Korea, South')
total_cases = [data[day.strftime('%-m/%-d/%y')][row] for day in days]

plt.semilogy(total_cases[:],'.k');
plt.xlabel('Days from January 22nd'); plt.ylabel('Total cases');
beta=0.25; gamma=0.05;
plt.semilogy(np.arange(0,42),np.exp((beta-gamma)*dd[0:42])/0.7);
plt.semilogy(np.arange(44,58),np.exp(0.02*dd[44:58])*2800);
plt.legend(['Infected cases',
            r'Exponential growth with rate $\beta-\gamma = 0.2$',
            r'Exponential growth with rate $0.02$']);
plt.title('Korea, South');<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11929307805596466, 'fraction_numerical': 0.03188832682333355, 'mean_word_length': 4.3873749568816836, 'pattern_counts': {'":': 0, '<': 45, '<?xml version=': 0, '>': 45, 'https://': 3, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '281637', 'license': 'CC-BY-4.0', 'n_tokens_mistral': 5380, 'n_tokens_neox': 4916, 'n_words': 1785, 'path': '04_Exponential_growth.ipynb', 'repo_name': 'ketch/covid-blog-posts', 'size': '15617'}