```python
import numpy as np
import scipy
import sympy as sym
import pandas as pd
from scipy import linalg
from scipy import optimize
from scipy import interpolate
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
sym.init_printing(use_unicode=True)
```

# 1. Human capital accumulation

Consider a worker living in **two periods**, $t \in \{1,2\}$.

In each period she decides whether to **work ($l_t = 1$) or not ($l_t = 0$)**.

She can *not* borrow or save and thus **consumes all of her income** in each period.

If she **works** her **consumption** becomes:

$$c_t = w h_t l_t\,\,\text{if}\,\,l_t=1$$

where $w$ is **the wage rate** and $h_t$ is her **human capital**.

If she does **not work** her consumption becomes:

$$c_t = b\,\,\text{if}\,\,l_t=0$$

where $b$ is the **unemployment benefits**.

Her **utility of consumption** is:

$$ \frac{c_t^{1-\rho}}{1-\rho} $$

Her **disutility of working** is:

$$ \gamma l_t $$

From period 1 to period 2, she **accumulates human capital** according to:

$$ h_2 = h_1 + l_1 +
\begin{cases}
0 & \text{with prob. }0.5 \\
\Delta & \text{with prob. }0.5
\end{cases} \\
$$

where $\Delta$ is a **stochastic experience gain**.

In the **second period** the worker thus solves:

$$
\begin{eqnarray*}
v_{2}(h_{2}) & = &\max_{l_{2}} \frac{c_2^{1-\rho}}{1-\rho} - \gamma l_2
\\ & \text{s.t.} & \\
c_{2}& = & w h_2 l_2 \\
l_{2}& \in &\{0,1\}
\end{eqnarray*}
$$

In the **first period** the worker thus solves:

$$
\begin{eqnarray*}
v_{1}(h_{1}) &=& \max_{l_{1}} \frac{c_1^{1-\rho}}{1-\rho} - \gamma l_1 + \beta\mathbb{E}_{1}\left[v_2(h_2)\right]
\\ & \text{s.t.} & \\
c_1 &=& w h_1 l_1 \\
h_2 &=& h_1 + l_1 + \begin{cases}
0 & \text{with prob. }0.5\\
\Delta & \text{with prob. }0.5
\end{cases}\\
l_{1} &\in& \{0,1\}\\
\end{eqnarray*}
$$

where $\beta$ is the **discount factor** and $\mathbb{E}_{1}\left[v_2(h_2)\right]$ is the **expected value of living in period two**.

The **parameters** of the model are:

The **relevant levels of human capital** are:

```python
h_vec = np.linspace(0.1,1.5,100)
```

**Question 1:** Solve the model in period 2 and illustrate the solution (including labor supply as a function of human capital).

**Answer to Q1**

We start by defining the consumption function, which will depend on whether or not she is working. If she is not working, then $l=0$ and she will consume $c=b$, if she is working she will consume $c=w*h$.

Afterwards we define the utility function and bequest, the utility function is the first part of $v_2(h_2)$ defined in the text, and bequest is the second part in $v_2(h_2)$.

```python
l = [0, 1]

def consumption(w,h,b,l):
    if l == 0:
        c = b
    elif l == 1:
        c = w*h
    return c

def utility(rho,h,w,b,l):
    return consumption(w,h,b,l)**(1-rho)/(1-rho)

def bequest(l,gamma):
    return gamma*l

def v2(l2,h2,w,b,rho,gamma):
    return utility(rho,h2,w,b,l2) - bequest(l2,gamma)
```

```python
rho = 2
beta = 0.96
gamma = 0.1
w = 2
b = 1
Delta = 0.1
```

We are now ready to solve the model. We do that by maximizing the utility given a level of human capital.

```python

def solve_period_2(w,b,rho,gamma):

    # h2_vec is the same as h_vec
    # we then define an array without any entries, we define 100 of those for both l2 and v2.
    h2_vec = np.linspace(0.1,1.5,100)
    l2_vec = np.empty(100)
    v2_vec = np.empty(100)

    # Solve for each h2 in grid
    for i,h2 in enumerate(h2_vec):

        # We calculate the utility when working, that is v2_Yes and when not working (v2_No).
        v2_No = v2(l[0],h2,w,b,rho,gamma)
        v2_Yes = v2(l[1],h2,w,b,rho,gamma)

        # We then find the utility maximizing level of labor.
        v2_vec[i] = max(v2_No,v2_Yes) #the vector is the maximum of the utility of either working or not working
        l2_vec[i] = v2_Yes>v2_No

    return h2_vec,v2_vec,l2_vec
```

```python
h2_vec,v2_vec,l2_vec = solve_period_2(w,b,rho,gamma);
```

```python
solution = np.where(l2_vec == 1)[0][0];
print(str(round(h2_vec[solution],3)))
```

    0.567

We have that solution $h_2^*=0.567$ and she chooses the work whenever $h_2$ is greater than or equal to $0.567$

```python
fig = plt.figure(figsize=(20,4))
ax = fig.add_subplot(1,3,3)
ax.plot(h2_vec,l2_vec)
ax.grid()
ax.set_xlabel('$Human\ capital, period 2$')
ax.set_ylabel('$Labor, period 2$')
ax.set_xlim([0.0,1.2])
ax.plot(0.555567,0.5,marker='.')
```

**Question 2:** Solve the model in period 1 and illustrate the solution (including labor supply as a function of human capital).

**Answer to Q2**

To solve this, we start out by constructing an interpolator, since the values in period 1 will depend on the values from period 2.

```python
v2_interp = interpolate.RegularGridInterpolator([h2_vec], v2_vec,bounds_error=False,fill_value=None)

```

We can now define the utility in period 1, which will depend on the value of v2 in period 2.

```python
def v1(l1,h1,w,b,rho,gamma,beta,Delta,v2_interp):

    # The level of v2 when gain is 0 with probability 50%
    h2_zero = h1 + l1
    v2_zero = v2_interp([h2_zero])[0]

    # The level of v2 when gain is Delta with probability 50%
    h2_delta = h1 + l1 + Delta
    v2_delta = v2_interp([h2_delta])[0]

    # We know calculate the expected value of v2
    v2_expected = 0.5 * v2_zero + 0.5 * v2_delta

    # And at last we get the total utility
    return utility(rho,h1,w,b,l1) - bequest(l1,gamma) + beta*v2_expected
```

From here we do it the same way as we did in question 1.

```python
def solve_period_1(w,b,rho,gamma,Delta,beta,v2_interp):
    # This is done in the same way as in question 1:
    h1_vec = np.linspace(0.1,1.5,100)
    l1_vec = np.empty(100)
    v1_vec = np.empty(100)

    for i,h1 in enumerate(h1_vec):

        v1_No = v1(l[0],h1,w,b,rho,gamma,beta,Delta,v2_interp)
        v1_Yes = v1(l[1],h1,w,b,rho,gamma,beta,Delta,v2_interp)

        v1_vec[i] = max(v1_No,v1_Yes)
        l1_vec[i] = v1_Yes > v1_No

    return h1_vec,v1_vec,l1_vec
```

```python
h1_vec,v1_vec,l1_vec = solve_period_1(w,b,rho,gamma,Delta,beta,v2_interp)
```

```python
solution = np.where(l1_vec == 1)[0][0];
print(str(round(h1_vec[solution],3)))
```

    0.355

We have that $h_1^*=0.355$

So she will decide to work in period 1 if her human capital is greater that or equal to $0.355$. We illustrate a figure below with the labor supply as a function of human capital.

```python
fig = plt.figure(figsize=(20,4))
ax = fig.add_subplot(1,3,3)
ax.plot(h1_vec,l1_vec)
ax.grid()
ax.set_xlabel('$Human\ capital, period 1$')
ax.set_ylabel('$Labor, period 1$')
ax.set_xlim([0.0,1.0])
ax.plot(0.3445,0.5,marker='.')
```

**Question 3:** Will the worker never work if her potential wage income is lower than the unemployment benefits she can get? Explain and illustrate why or why not.

**Answer to Q3**

The worker might work even though her potential wage income in period 1 is lower than the unemployment benefit. The reason is the stochastic experience gain we have defined. She accumulates human capital, if she doesn't work than $h_2=h_1+l_1$ while if she works then $h_2=h_1+l_1+\Delta$. The reason she might be working in period 1 even though her wage income is lower than the unemployment benefit, is because she might accumulate enough human capital such that in period 2 she will get an even higher wage, and thus be able to consume more.

```python
def v1NO(l1,h1,w,b,rho,gamma,beta,v2_interp):

    # The level of v2 when gain is 0 with probability 50%
    h2_zero = h1 + l1
    v2_zero = v2_interp([h2_zero])[0]

    # d. total value
    return utility(rho,h1,w,b,l1) - bequest(l1,gamma) + beta*v2_zero
```

```python
def solve_period_1NO(w,b,rho,gamma,Delta,beta,v2_interp):
    # a. grids
    h1NO_vec = np.linspace(0.1,1.5,100)
    l1NO_vec = np.empty(100)
    v1NO_vec = np.empty(100)

    # b. solve for each h1 in grid
    for i,h1 in enumerate(h1_vec):

        # We now calculate the utility when working (v1_1) and not working (v1_0)
        v1NO_0 = v1NO(l[0],h1,w,b,rho,gamma,beta,v2_interp)
        v1NO_1 = v1NO(l[1],h1,w,b,rho,gamma,beta,v2_interp)

        v1NO_vec[i] = max(v1NO_0,v1NO_1)
        l1NO_vec[i] = v1NO_1 > v1NO_0

    return h1NO_vec,v1NO_vec,l1NO_vec
```

The idea here was to show the utility as defined in question two and a utility function where there are no stochastic gain (so only $h_1+l_1$ with probability 1), but it somehow messes up. The idea was to show how the utilities differ, and that might explain why sometimes she might choose to work even though she would consume less in the first period.

# AS-AD model
Consider the following **AS-AD model**. The **goods market equilibrium** is given by

$$ y_{t} = -\alpha r_{t} + v_{t} $$

where $y_{t}$ is the **output gap**, $r_{t}$ is the **ex ante real interest** and $v_{t}$ is a **demand disturbance**.

The central bank's **Taylor rule** is

$$ i_{t} = \pi_{t+1}^{e} + h \pi_{t} + b y_{t}$$

where $i_{t}$ is the **nominal interest rate**, $\pi_{t}$ is the **inflation gap**, and $\pi_{t+1}^{e}$ is the **expected inflation gap**.

The **ex ante real interest rate** is given by

$$ r_{t} = i_{t} - \pi_{t+1}^{e} $$

Together, the above implies that the **AD-curve** is

$$ \pi_{t} = \frac{1}{h\alpha}\left[v_{t} - (1+b\alpha)y_{t}\right]$$

Further, assume that the **short-run supply curve (SRAS)** is given by

$$ \pi_{t} = \pi_{t}^{e} + \gamma y_{t} + s_{t}$$

where $s_t$ is a **supply disturbance**.

**Inflation expectations are adaptive** and given by

$$ \pi_{t}^{e} = \phi\pi_{t-1}^{e} + (1-\phi)\pi_{t-1}$$

Together, this implies that the **SRAS-curve** can also be written as

$$ \pi_{t} = \pi_{t-1} + \gamma y_{t} - \phi\gamma y_{t-1} + s_{t} - \phi s_{t-1} $$

The **parameters** of the model are:

```python
par = {}

par['alpha'] = 5.76
par['h'] = 0.5
par['b'] = 0.5
par['phi'] = 0
par['gamma'] = 0.075

#Defining symbols
pi_t = sym.symbols('pi_t')
pi_t2 = sym.symbols('pi_t-1')
y_t = sym.symbols('y_t')
y_t2 = sym.symbols('y_t-1')
v_t = sym.symbols('v_t')
s_t = sym.symbols('s_t')
s_t2 = sym.symbols('s_t-1')
alpha = sym.symbols('alpha')
gamma = sym.symbols('gamma')
phi = sym.symbols('phi')
h = sym.symbols('h')
b = sym.symbols('b')
```

**Question 1:** Use the ``sympy`` module to solve for the equilibrium values of output, $y_t$, and inflation, $\pi_t$, (where AD = SRAS) given the parameters ($\alpha$, $h$, $b$, $\alpha$, $\gamma$) and $y_{t-1}$ , $\pi_{t-1}$, $v_t$, $s_t$, and $s_{t-1}$.

**Answer for Q1**

First, one start by defining the AD and SRAS functions, and then solving AD=SRAS with sympy solver for both $y_t$ and $\pi_t$.

```python
#Defining AD-curve
AD = sym.Eq(pi_t,(1/(h*alpha))*(v_t-(1+b*alpha)*y_t))
#Defining SRAS-curve
SRAS = sym.Eq(pi_t,pi_t2+gamma*y_t-phi*gamma*y_t2+s_t-phi*s_t2)
#Setting AD equal to SRAS and solving for equilibrium values of output, y*.
eq_1= sym.solve([AD, SRAS], [y_t, pi_t])
eq_y=eq_1[y_t]
print('The equilibrium for output y is:')
eq_y
```

```python
eq_pi=eq_1[pi_t]
print('The equilibrium for output pi is:')
eq_pi
```

```python
#lambdifying for later use
pi_func = sym.lambdify((pi_t2, y_t2, v_t, s_t, s_t2, alpha, h, b, phi, gamma), eq_pi)
y_func = sym.lambdify((pi_t2, y_t2, v_t, s_t, s_t2, alpha, h, b, phi, gamma), eq_y)
```

Now, one an just set the paramter values to find the equilibrium given the paramter values through the same procedure as above.

```python
#Setting parameter values

alpha = par['alpha']
h = par['h']
b = par['b']
phi = par['phi']
gamma = par['gamma']
#Resolving for equilibrium
AD_SRAS_val=sym.Eq((1/(h*alpha))*(v_t-(1+b*alpha)*y_t),pi_t2+gamma*y_t-phi*gamma*y_t2+s_t-phi*s_t2)
eq_y_val = sym.solve(AD_SRAS_val,y_t)
print('The equilibrium for output y given parameter values is:')
eq_y_val

```

```python
eq_pi_val=1/(h*alpha)*(v_t-(1+b*alpha)*eq_y_val[0])
print('The equilibrium for output pi given parameter values is:')
eq_pi_val
```

**Question 2:** Find and illustrate the equilibrium when $y_{t-1} = \pi_{t-1} = v_t = s_t = s_{t-1} = 0$. Illustrate how the equilibrium changes when instead $v_t = 0.1$.

**Answer to Q2**

To find the equilibrium when $y_{t-1} = \pi_{t-1} = v_t = s_t = s_{t-1} = 0$, one needs to set the new values for the variables, and here, just re-solve the same problem as earlier.

```python
#Setting variable values - parameter is similar to previous questions
pi_t2 = 0
y_t2 = 0
v_t = 0
s_t = 0
s_t2 = 0

#Solving for equilibrium given variable values
AD_SRAS_param=sym.Eq((1/(h*alpha))*(v_t-(1+b*alpha)*y_t),pi_t2+gamma*y_t-phi*gamma*y_t2+s_t-phi*s_t2)
eq_y_param = sym.solve(AD_SRAS_param,y_t)
eq_pi_param=1/(h*alpha)*(v_t-(1+b*alpha)*eq_y_param[0])
print('The equilibrium for output y given parameter and variable values is: %8.3f' % eq_y_param[0])
print('The equilibrium for output pi given parameter and variable values is: %8.3f' % eq_pi_param)
```

    The equilibrium for output y given parameter and variable values is:    0.000
    The equilibrium for output pi given parameter and variable values is:    0.000

With the assumption $y_{t-1} = \pi_{t-1} = v_t = s_t = s_{t-1} = 0$, one can see that $\pi_t=y_t=0$ in equilibrium.

Now, one can investigate how the equilibrium changes when $v_t=0.1$

```python
#Solving for equilibrium given v_t=0.1
v_t2 = 0.1
AD_SRAS_param2=sym.Eq((1/(h*alpha))*(v_t2-(1+b*alpha)*y_t),pi_t2+gamma*y_t-phi*gamma*y_t2+s_t-phi*s_t2)
eq_y_param2 = sym.solve(AD_SRAS_param2,y_t)
eq_pi_param2=1/(h*alpha)*(v_t2-(1+b*alpha)*eq_y_param2[0])
print('The equilibrium for output y given parameter and variable values is: %8.4f' % eq_y_param2[0])
print('The equilibrium for output pi given parameter and variable values is: %8.4f' % eq_pi_param2)
```

    The equilibrium for output y given parameter and variable values is:   0.0244
    The equilibrium for output pi given parameter and variable values is:   0.0018

In the case of $v_t=0.1$ both $\pi_t$ and $y_t$ becomes positive but $y_t^*>\pi_t^*$

Now, lets illustrate the equilibria. First, one creates a vector consisting of the equilibrium values for $y_t$ and $\pi_t$, respectively. Next step is to construct a illustration of the equilibrium values for given values of $v_t$.

```python
#Creating vector with equilibrium values for y_t and pi_t for given values of v_t
## Vector with equilibrium values for y
y_values=[]
y_values.append(eq_y_param[0])
y_values.append(eq_y_param2[0])
## Vector with equilibrium values for pi
pi_values=[]
pi_values.append(eq_pi_param)
pi_values.append(eq_pi_param2)
```

```python
#Creating illustration of the equilibrium values
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.scatter(pi_values[0], y_values[0], label='$v_t=0$')
ax.scatter(pi_values[1], y_values[1], label='$v_t=0.1$')

#Setting up illustration
ax.grid()
ax.legend()
ax.set_ylabel('$y_t^*$')
ax.set_xlabel('$\pi_t^*$')
ax.set_title('Equilibrium values for given values of $v_t$')
```

**Persistent disturbances:** Now, additionaly, assume that both the demand and the supply disturbances are AR(1) processes

$$ v_{t} = \delta v_{t-1} + x_{t} $$
$$ s_{t} = \omega s_{t-1} + c_{t} $$

where $x_{t}$ is a **demand shock**, and $c_t$ is a **supply shock**. The **autoregressive parameters** are:

```python
par['delta'] = 0.80
par['omega'] = 0.15
delta = par['delta']
omega = par['omega']
```

**Question 3:** Starting from $y_{-1} = \pi_{-1} = s_{-1} = 0$, how does the economy evolve for $x_0 = 0.1$, $x_t = 0, \forall t > 0$ and $c_t = 0, \forall t \geq 0$?

**Answer to Q3**

```python
par['delta'] = 0.80
par['omega'] = 0.15
delta = par['delta']
omega = par['omega']
```

Firstly, one needs to define the demand and supply disturbances as functions, set the value for the shocks, the amount of periods for the simulation to investigate the evolvement of the economy given $x_0 = 0.1$, $x_t = 0, \forall t > 0$ and $c_t = 0, \forall t \geq 0$. Then, one can simulate the model. This will also be the order of solution.

```python
#Defining demand and supply disturbances and vectors
v_t2 = sym.symbols('v_t2')
s_t2 = sym.symbols('s_t2')
v = lambda v_t2,x: delta*v_t2 + x
s = lambda s_t2,c: omega*s_t2 + c
v_vector = [0]
s_vector = [0]

#Defining amount of periods for simulation, demand and supply shock vectors.
T = 200
x = np.zeros(T)
c = np.zeros(T)
x[1] = 0.1
```

Now, one can run a loop to simulate the model for T periods.

```python
y_output = [0]
pi_inflation = [0]
#Running simulation of the model
for t in range (1,T):
    v_vector.append(v(v_vector[t-1], x[t]))
    s_vector.append(s(s_vector[t-1], c[t]))
```

```python
for t in range(1,T):
    y_output.append(y_func(pi_inflation[t-1], y_output[t-1], v_vector[t], s_vector[t], s_vector[t-1], alpha, h, b, phi, gamma))
    pi_inflation.append(pi_func(pi_inflation[t-1], y_output[t-1], v_vector[t], s_vector[t], s_vector[t-1], alpha, h, b, phi, gamma))
```

```python
#Creating illustration of the equilibrium values
fig = plt.figure(figsize= (10,5))
bx = fig.add_subplot(1,1,1)
bx.plot(range(0,200,1), y_output, label = 'Output')
bx.plot(range(0,200,1), pi_inflation, label = 'Inflation')

#Setting up illustration
bx.legend()
bx.grid()
bx.set_xlabel('t') #
bx.set_ylabel('$y_t, \pi_t$')
bx.set_title('Output and inflation gap for 200 periods')
plt.xticks(range(0, 201, 50))
plt.show()
```

One can see that the output has a faster convergence than inflation given $v_t$. Additionally, the output reacts immediately to the shock, whereas the inflation gap reacts slower to the demand shock.

**Stochastic shocks:** Now, additionally, assume that $x_t$ and $c_t$ are stochastic and normally distributed

$$ x_{t}\sim\mathcal{N}(0,\sigma_{x}^{2}) $$
$$ c_{t}\sim\mathcal{N}(0,\sigma_{c}^{2}) $$

The **standard deviations of the shocks** are:

```python
par['sigma_x'] = 3.492
par['sigma_c'] = 0.2
```

**Question 4:** Simulate the AS-AD model for 1,000 periods. Calculate the following five statistics:

1. Variance of $y_t$, $var(y_t)$
2. Variance of $\pi_t$, $var(\pi_t)$
3. Correlation between $y_t$ and $\pi_t$, $corr(y_t,\pi_t)$
4. Auto-correlation between $y_t$ and $y_{t-1}$, $corr(y_t,y_{t-1})$
5. Auto-correlation between $\pi_t$ and $\pi_{t-1}$, $corr(\pi_t,\pi_{t-1})$

**Answer Q4**

**Stochastic shocks:** Now, additionally, assume that $x_t$ and $c_t$ are stochastic and normally distributed

$$ x_{t}\sim\mathcal{N}(0,\sigma_{x}^{2}) $$
$$ c_{t}\sim\mathcal{N}(0,\sigma_{c}^{2}) $$

The **standard deviations of the shocks** are:

```python
par['sigma_x'] = 3.492
par['sigma_c'] = 0.2
sigma_x = par['sigma_x']
sigma_c = par['sigma_c']
```

The following will be more or less equal to the simulation from **Q3**. Just with the modelling of stochastic shocks which are normally distributed. Firstly, one define the stochastic process and then run the simulation

```python
#Defining stochastic shocks
np.random.seed(127)
T=1000
x = np.random.normal(loc = 0, scale=sigma_x, size =T)
c = np.random.normal(loc = 0, scale=sigma_c, size =T)
```

Now, one can start simulating the model for which a function, simulate, is defined.

```python
#Defining simulating function
def simulate(T,phi_var):
    #Defining empty lists / Assuming y_{-1} = \pi_{-1} = s_{-1} = 0
    v_vector2 = [0]
    s_vector2 = [0]
    pi_inflation2 = [0]
    y_output2 = [0]
    for t in range(1,T):
        v_vector2.append(v(v_vector2[t-1], x[t]))
        s_vector2.append(s(s_vector2[t-1], c[t]))
        #new output and inflation
        y_output2.append(y_func(pi_inflation2[t-1], y_output2[t-1], v_vector2[t], s_vector2[t], s_vector2[t-1], alpha, h, b, phi_var, gamma))
        pi_inflation2.append(pi_func(pi_inflation2[t-1], y_output2[t-1], v_vector2[t], s_vector2[t], s_vector2[t-1], alpha, h, b, phi_var, gamma))
    #Converting to numpy arrays
    y_output_sol=np.array(y_output2)
    pi_inflation_sol=np.array(pi_inflation2)
    return y_output_sol, pi_inflation_sol

#Simulation of the model
y_output_sol, pi_inflation_sol = simulate(T,phi)

#Printing the results
print('Variance of y is %8.3f' % y_output_sol.var())
print('Variance of pi is %8.3f' % pi_inflation_sol.var())
print('Correlation between y and $\pi$ is %8.3f' % np.corrcoef(y_output_sol, pi_inflation_sol)[1,0])
print('Auto-correlation between $y_t$ and $y_t-1$ is %8.3f' % np.corrcoef(y_output_sol[1:], y_output_sol[:-1])[1,0])
print('Auto-correlation between $pi_t$ and $\pi_t-1$ is %8.3f' % np.corrcoef(pi_inflation_sol[1:], pi_inflation_sol[:-1])[1,0])
```

    Variance of y is    1.993
    Variance of pi is    1.039
    Correlation between y and $\pi$ is   -0.167
    Auto-correlation between $y_t$ and $y_t-1$ is    0.775
    Auto-correlation between $pi_t$ and $\pi_t-1$ is    0.980

As expected $y$ has larger variance than $\phi$ due to the demand shock. The negative correlation between $y$ and $\pi$ reflects, the inflation gap decreases as/if the output gap increases.

**Question 5:** Plot how the correlation between $y_t$ and $\pi_t$ changes with $\phi$. Use a numerical optimizer or root finder to choose $\phi\in(0,1)$ such that the simulated correlation between $y_t$ and $\pi_t$ comes close to 0.31.

### Answer to Q5

To plot the correlation between inflation and output, one need to simulate output and inflation for different values of $\phi$. The simulation from **Q4** will be used, and then the saved to be plotted.

```python
#Definning new phi and empty list for simulation / Assuming y_{-1} = \pi_{-1} = s_{-1} = 0

phi_new = np.linspace(0,1,10)
y_sim = {}
pi_sim = {}
corr_ypi = []

```

```python
#Simulation with changes in phi
for i,p in enumerate(phi_new):
    pi_sim['pi_inflation3_%s' % i], y_sim['y_output3_%s' % i] = simulate(T, phi_new[i])
    corr_ypi.append(np.corrcoef(y_sim['y_output3_%s' %i], pi_sim['pi_inflation3_%s' % i])[1,0])
```

One can now illustrate the correlation for different values of $\phi$. This is perfomed below.

```python
#Creating illustration of the equilibrium values
fig = plt.figure(figsize= (10,5))
cx = fig.add_subplot(1, 1, 1)
cx.plot(phi_new, corr_ypi, label = 'Correlation')

#Setting up illustration
cx.grid()
cx.legend()
cx.set_ylabel('Correlation between $y$ and $\pi$')
cx.set_xlabel('$\phi$')
cx.set_title('Correlation for different values of $\phi$')
plt.show()
```

One can see from the illustration that when $corr(y_t,\pi_t)=0.31$, the value of $\phi$ is approaching  a values close to 1. To investigate this one can use the optimize module from the sympy packages. Define an objective, s olving the objective to finally print the result.

```python
#Objective
obj = lambda phi: np.corrcoef(simulate(T, phi)[0], simulate(T, phi)[1])[1,0] - 0.31

#Optimizing
phi_opt = optimize.root_scalar(obj, x0 = 0.8, bracket = [0,1], method = 'bisect')

#Result
sol = phi_opt.flag
phi_opt = phi_opt.root
print('Optimal values of phi to find corr(y_t,pi_t)=0.31: %8.3f' % phi_opt)
print('Correlation for optimal phi: %8.3f' % np.corrcoef(simulate(T, phi_opt)[0], simulate(T, phi_opt)[1])[1,0])
```

    Optimal values of phi to find corr(y_t,pi_t)=0.31:    0.983
    Correlation for optimal phi:    0.310

**Quesiton 6:** Use a numerical optimizer to choose $\sigma_x>0$, $\sigma_c>0$ and $\phi\in(0,1)$ to make the simulated statistics as close as possible to US business cycle data where:

1. $var(y_t) = 1.64$
2. $var(\pi_t) = 0.21$
3. $corr(y_t,\pi_t) = 0.31$
4. $corr(y_t,y_{t-1}) = 0.84$
5. $corr(\pi_t,\pi_{t-1}) = 0.48$

**Answer to Q6**:
We have tried to come up with a solution but no luck. This is one of the attempts did not work.

#Defining simulation smiliar to the one above

def simulate3(x):
    #Assuming y_{-1} = \pi_{-1} = s_{-1} = 0
    v_vector3 = [0]
    s_vector3 = [0]
    pi_inflation3 = [0]
    y_output3 = [0]
    corr_ypi3 = []

    #Disturbances
    np.random.seed(3)
    sigma_x = par['sigma_x']
    sigma_c = par['sigma_c']
    x = np.random.normal(loc = 0, scale=sigma_x, size =T)
    c = np.random.normal(loc = 0, scale=sigma_c, size =T)

    #Simulation
    for i in range(1,10):
        v_vector3.append(v(v_vector3[t-1], x[t]))
        s_vector3.append(s(s_vector3[t-1], c[t]))
        #new output and inflation
        y_output3.append(y_func(pi_inflation3[t-1], y_output3[t-1], v_vector3[t], s_vector3[t], s_vector3[t-1], alpha, h, b, phi_var, gamma))
        pi_inflation3.append(pi_func(pi_inflation3[t-1], y_output3[t-1], v_vector3[t], s_vector3[t], s_vector3[t-1], alpha, h, b, phi_var, gamma))
    #Converting to numpy arrays
    y_output_sol3=np.array(y_output3)
    pi_inflation_sol3=np.array(pi_inflation3)

    #Statistics
    y_var=np.var(y_output3)
    pi_var=np.inflation = np.var(pi_inflation3)
    corr_ypi=np.corrcoef(y_output_sol3, pi_inflation_sol3)[1,0]
    pi_ac = np.corrcoef(y_output_sol[1:], y_output_sol[:-1])[1,0]
    y_ac = np.corrcoef(pi_inflation_sol[1:], pi_inflation_sol[:-1])[1,0]

    #Differences from the US businees cycle data / squarred to aviod negative
    y_diff = (y_var - 1.64)**2
    pi_diff = (var_inflation - 0.21)**2
    corr_ypi_diff = (corr_ypi - 0.31)**2 #squared difference
    pi_ac_doff = (pi_ac - 0.84)**2
    y_ac_fiff = (y_ac - 0.48)**2

    #Sum of differences
    sum_diff=sum([y_diff, pi_diff, corr_ypi_diff, pi_ac_diff, y_ac_diff])

    return sum_diff

#Defining objective, guess and bounds for optimizer

obj = lambda sim: simulate3(sim)
x0 = (phi_opt, sigma_x, sigma_c)
bounds = [[0,1], [0,100], [0,100]]

#Simulation and solving the problem with scipy minimizer

sol = optimize.minimize(obj, x0=x0, method='L-BFGS-B', bounds=bounds)

# 3. Exchange economy

Consider an **exchange economy** with

1. 3 goods, $(x_1,x_2,x_3)$
2. $N$ consumers indexed by \\( j \in \{1,2,\dots,N\} \\)
3. Preferences are Cobb-Douglas with log-normally distributed coefficients

    $$ \begin{eqnarray*}
    u^{j}(x_{1},x_{2},x_{3}) &=&
    \left(x_{1}^{\beta_{1}^{j}}x_{2}^{\beta_{2}^{j}}x_{3}^{\beta_{3}^{j}}\right)^{\gamma}\\
     &  & \,\,\,\beta_{i}^{j}=\frac{\alpha_{i}^{j}}{\alpha_{1}^{j}+\alpha_{2}^{j}+\alpha_{3}^{j}} \\
     &  & \,\,\,\boldsymbol{\alpha}^{j}=(\alpha_{1}^{j},\alpha_{2}^{j},\alpha_{3}^{j}) \\
     &  & \,\,\,\log(\boldsymbol{\alpha}^j) \sim \mathcal{N}(\mu,\Sigma) \\
    \end{eqnarray*} $$

4. Endowments are exponentially distributed,

$$
\begin{eqnarray*}
\boldsymbol{e}^{j} &=& (e_{1}^{j},e_{2}^{j},e_{3}^{j}) \\
 &  & e_i^j \sim f, f(z;\zeta) =  1/\zeta \exp(-z/\zeta)
\end{eqnarray*}
$$

Let $p_3 = 1$ be the **numeraire**. The implied **demand functions** are:

$$
\begin{eqnarray*}
x_{i}^{\star j}(p_{1},p_{2},\boldsymbol{e}^{j})&=&\beta^{j}_i\frac{I^j}{p_{i}} \\
\end{eqnarray*}
$$

where consumer $j$'s income is

$$I^j = p_1 e_1^j + p_2 e_2^j +p_3 e_3^j$$

The **parameters** and **random preferences and endowments** are given by:

```python
# a. parameters
N = 50000
mu = np.array([3,2,1])
Sigma = np.array([[0.25, 0, 0], [0, 0.25, 0], [0, 0, 0.25]])
gamma = 0.8
zeta = 1

# b. random draws
seed = 1986
np.random.seed(seed)

# preferences
alphas = np.exp(np.random.multivariate_normal(mu, Sigma, size=N))
betas = alphas/np.reshape(np.sum(alphas,axis=1),(N,1))

# endowments
e1 = np.random.exponential(zeta,size=N)
e2 = np.random.exponential(zeta,size=N)
e3 = np.random.exponential(zeta,size=N)
```

**Question 1:** Plot the histograms of the budget shares for each good across agents.

Consider the **excess demand functions:**

$$ z_i(p_1,p_2) = \sum_{j=1}^N x_{i}^{\star j}(p_{1},p_{2},\boldsymbol{e}^{j}) - e_i^j$$

**Answer to Q1**

```python
# Plotting histogram of budget shares

# Plotting histogram of each beta with 200 bins.
fig = plt.figure(figsize=(12,5))
ax = fig.add_subplot(1,3,1)
ax.hist(betas[:,0],bins=200,histtype='bar')
ax.set_xlabel('$beta_1$')
ax.set_ylabel('Consumers')
ax.set_title('Good 1')
ax.set_ylim([0, 1250])

ax = fig.add_subplot(1,3,2)
ax.hist(betas[:,1],bins=200,histtype='bar')
ax.set_xlabel('$beta_2$')
ax.set_title('Good 2')
ax.set_ylim([0, 1250])

ax = fig.add_subplot(1,3,3)
ax.hist(betas[:,2],bins=200,histtype='bar')
ax.set_xlabel('$beta_3$')
ax.set_title('Good 3')
ax.set_ylim([0, 1250]);
```

**Question 2:** Plot the excess demand functions.

**Answer to Q2**

```python
#We first define the demand functions, as we set p3=1 we have just excluded that term.

def demand_good1(p1,p2,e1,e2,e3,betas):
    I = e1*p1+e2*p2+e3
    return betas[:,0]*I/p1

def demand_good2(p1,p2,e1,e2,e3,betas):
    I = e1*p1+e2*p2+e3
    return betas[:,1]*I/p2

def demand_good3(p1,p2,e1,e2,e3,betas):
    I = e1*p1+e2*p2+e3
    return betas[:,2]*I

#We then define the excess demand functions, and supply functions for good 1 and good 2.

def excess1(p1,p2,e1,e2,e3,betas):
    demand = np.sum(demand_good1(p1,p2,e1,e2,e3,betas))
    supply = np.sum(e1)
    excess1 = demand - supply
    return excess1

def excess2(p1,p2,e1,e2,e3,betas):
    demand = np.sum(demand_good2(p1,p2,e1,e2,e3,betas))
    supply = np.sum(e2)
    excess2 = demand - supply
    return excess2

# We then define an array of prices
p1_s = np.linspace(0.1,10,100)
p2_s = np.linspace(0.1,10,100)

# Creating grids for excess demand
excess_grid1 = np.empty((100,100))
excess_grid2 = np.empty((100,100))

# And then we transform our price-vectors into grids using meshgrid
p1_grid, p2_grid = np.meshgrid(p1_s,p2_s,indexing='ij')

# We can then calculate excess demand for each combination of prices:
for i,p1 in enumerate(p1_s):
    for j,p2 in enumerate(p2_s):
        excess_grid1[i,j] = excess1(p1,p2,e1,e2,e3,betas)
        excess_grid2[i,j] = excess2(p1,p2,e1,e2,e3,betas)
```

```python
# We can now plot our results
fig = plt.figure(figsize=(10,4))
ax = fig.add_subplot(1,2,1, projection='3d')
ax.plot_surface(p1_grid, p2_grid, excess_grid1)
ax.invert_xaxis()
ax.set_title('Excess demand, good 1')
ax.set_xlabel('$p_1$')
ax.set_ylabel('$p_2$')

ax = fig.add_subplot(1,2,2, projection='3d')
ax.plot_surface(p1_grid, p2_grid, excess_grid2)
ax.invert_xaxis()
ax.set_title('Excess demand, good 2')
ax.set_xlabel('$p_1$')
ax.set_ylabel('$p_2$');
```

**Quesiton 3:** Find the Walras-equilibrium prices, $(p_1,p_2)$, where both excess demands are (approximately) zero, e.g. by using the following tâtonnement process:

1. Guess on $p_1 > 0$, $p_2 > 0$ and choose tolerance $\epsilon > 0$ and adjustment aggressivity parameter, $\kappa > 0$.
2. Calculate $z_1(p_1,p_2)$ and $z_2(p_1,p_2)$.
3. If $|z_1| < \epsilon$ and $|z_2| < \epsilon$ then stop.
4. Else set $p_1 = p_1 + \kappa \frac{z_1}{N}$ and $p_2 = p_2 + \kappa \frac{z_2}{N}$ and return to step 2.

**Answer to Q3**

```python
def tatonnement(p1,p2,e1,e2,e3,betas,tol=1e-7,kappa=0.2,prints=True):
  # Calculating initial excess demands
    z_1 = excess1(p1,p2,e1,e2,e3,betas)
    z_2 = excess2(p1,p2,e1,e2,e3,betas)

# Setting iteration counter to 1
    t = 1

    # We start the loop here, we will have a maximum of 20.000 loops
    while t < 20000:

        # We calculate the initial excess demands
        z_1 = excess1(p1,p2,e1,e2,e3,betas)
        z_2 = excess2(p1,p2,e1,e2,e3,betas)

        # Set our tolerance level:
        if abs(z_1)<tol and abs(z_2)<tol:
            if prints:
                print(f'\nIn the Walras equilibrium we have: p1 = {p1:.2f} and p2 = {p2:.2f}')
            p1_eq = p1
            p2_eq = p2
            return p1_eq, p2_eq

        # If no convergence, we will multiply our excess demand by kappa and take the average:
        else:
            p1 += kappa*z_1/N
            p2 += kappa*z_2/N

        # We then print the iteration proces
        if prints:
            if t <= 10 or t%1000==0:
                print(f'Iter {t:6.0f}: Excess good 1 = {z_1:10.2f}, Excess good 2 = {z_2:10.2f} => p1 = {p1:10.2f}, p2 = {p2:10.2f}')

        t += 1

    # Print statement if maximum numbers of iterations is exceeded
    if t == 20000:
        text = 'No convergence \n'
        print(text)
        return None, None
```

```python
# Finding Walras-equilibrium prices

# Initial guess on prices
p1 = 4
p2 = 1

# Calling function to find equilibrium prices
p1_eq, p2_eq = tatonnement(p1,p2,e1,e2,e3,betas)
```

    Iter      1: Excess good 1 =   -1802.03, Excess good 2 =   27583.49 => p1 =       3.99, p2 =       1.11
    Iter      2: Excess good 1 =    -890.71, Excess good 2 =   21094.24 => p1 =       3.99, p2 =       1.19
    Iter      3: Excess good 1 =    -200.19, Excess good 2 =   16961.42 => p1 =       3.99, p2 =       1.26
    Iter      4: Excess good 1 =     346.52, Excess good 2 =   14060.01 => p1 =       3.99, p2 =       1.32
    Iter      5: Excess good 1 =     790.31, Excess good 2 =   11901.20 => p1 =       3.99, p2 =       1.37
    Iter      6: Excess good 1 =    1156.25, Excess good 2 =   10231.46 => p1 =       4.00, p2 =       1.41
    Iter      7: Excess good 1 =    1461.18, Excess good 2 =    8903.73 => p1 =       4.00, p2 =       1.44
    Iter      8: Excess good 1 =    1717.07, Excess good 2 =    7825.81 => p1 =       4.01, p2 =       1.47
    Iter      9: Excess good 1 =    1932.83, Excess good 2 =    6936.50 => p1 =       4.02, p2 =       1.50
    Iter     10: Excess good 1 =    2115.29, Excess good 2 =    6193.32 => p1 =       4.03, p2 =       1.53
    Iter   1000: Excess good 1 =      42.54, Excess good 2 =      15.87 => p1 =       6.44, p2 =       2.60
    Iter   2000: Excess good 1 =       1.08, Excess good 2 =       0.40 => p1 =       6.49, p2 =       2.62
    Iter   3000: Excess good 1 =       0.03, Excess good 2 =       0.01 => p1 =       6.49, p2 =       2.62
    Iter   4000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62
    Iter   5000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62
    Iter   6000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62

    In the Walras equilibrium we have: p1 = 6.49 and p2 = 2.62

**Question 4:** Plot the distribution of utility in the Walras-equilibrium and calculate its mean and variance.

**Answer to Q4**

```python
def u(p1, p2, e1, e2, e3, betas, gamma):

    # Income is given by:
    I = p1*e1+p2*e2+e3

    # Demand is given by:
    demand1 = betas[:,0]*(I/p1)
    demand2 = betas[:,1]*(I/p2)
    demand3 = betas[:,2]*I

    # Calculating utility
    u = (demand1**betas[:,0]+demand2**betas[:,1]+demand3**betas[:,2])**gamma
    return u
```

```python
# We make a function that creates a vector of utilities
u_vec = u(p1_eq, p2_eq, e1, e2, e3, betas, gamma)

# we can then find the mean and variance
mean = np.mean(u_vec)
var = np.var(u_vec)
print(f'Mean: {mean:.2f}')
print(f'Variance: {var:.2f}')
```

    Mean: 2.38
    Variance: 0.21

```python
# Plotting distribution of utilities, and printing mean and variance
plt.hist(u_vec, bins=500)
plt.title('Utility distribution')
plt.xlabel('Utility')
plt.ylabel('Consumers')
```

**Question 5:** Find the Walras-equilibrium prices if instead all endowments were distributed equally. Discuss the implied changes in the distribution of utility. Does the value of $\gamma$ play a role for your conclusions?

**Answer to Q5**

```python
e1_new = np.ones(N)*np.mean(e1)
e2_new = np.ones(N)*np.mean(e2)
e3_new = np.ones(N)*np.mean(e3)

```

```python
p1 = 4
p2 = 1

p1_new, p2_new = tatonnement(p1,p2,e1_new,e2_new,e3_new,betas)
```

    Iter      1: Excess good 1 =   -1811.29, Excess good 2 =   27618.68 => p1 =       3.99, p2 =       1.11
    Iter      2: Excess good 1 =    -898.66, Excess good 2 =   21117.58 => p1 =       3.99, p2 =       1.19
    Iter      3: Excess good 1 =    -207.23, Excess good 2 =   16978.68 => p1 =       3.99, p2 =       1.26
    Iter      4: Excess good 1 =     340.19, Excess good 2 =   14073.55 => p1 =       3.99, p2 =       1.32
    Iter      5: Excess good 1 =     784.53, Excess good 2 =   11912.24 => p1 =       3.99, p2 =       1.37
    Iter      6: Excess good 1 =    1150.94, Excess good 2 =   10240.69 => p1 =       4.00, p2 =       1.41
    Iter      7: Excess good 1 =    1456.26, Excess good 2 =    8911.60 => p1 =       4.00, p2 =       1.44
    Iter      8: Excess good 1 =    1712.48, Excess good 2 =    7832.63 => p1 =       4.01, p2 =       1.47
    Iter      9: Excess good 1 =    1928.53, Excess good 2 =    6942.48 => p1 =       4.02, p2 =       1.50
    Iter     10: Excess good 1 =    2111.24, Excess good 2 =    6198.61 => p1 =       4.03, p2 =       1.53
    Iter   1000: Excess good 1 =      42.32, Excess good 2 =      15.81 => p1 =       6.44, p2 =       2.60
    Iter   2000: Excess good 1 =       1.07, Excess good 2 =       0.40 => p1 =       6.48, p2 =       2.62
    Iter   3000: Excess good 1 =       0.03, Excess good 2 =       0.01 => p1 =       6.49, p2 =       2.62
    Iter   4000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62
    Iter   5000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62
    Iter   6000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62

    In the Walras equilibrium we have: p1 = 6.49 and p2 = 2.62

We see that the changes in the distribution of endowments doesn't have a significant effect on the prices. In fact the prices are identical to the previous situation.

```python
def u_new(p1, p2, e1, e2, e3, betas, gamma):

    # Income is given by:
    I = p1*e1+p2*e2+e3

    # Demand is given by:
    demand1_new = betas[:,0]*(I/p1)
    demand2_new = betas[:,1]*(I/p2)
    demand3_new = betas[:,2]*I

    # Calculating utility
    u = (demand1_new**betas[:,0]+demand2_new**betas[:,1]+demand3_new**betas[:,2])**gamma
    return u_new
```

```python
# We make a function that creates a vector of utilities
u_vec_new = u(p1_eq, p2_eq, e1, e2, e3, betas, 1.5)

# we can then find the mean and variance
mean = np.mean(u_vec_new)
var = np.var(u_vec_new)
print(f'Mean: {mean:.2f}')
print(f'Variance: {var:.2f}')
```

    Mean: 5.22
    Variance: 3.71

Even though the prices are unchanged, gamma will have an impact on the mean and variance. By increasing gamma from $0.8$ to $1.5$ the mean have increased from $2.38$ to $5.22$ while the variance have increased from $0.21$ to $3.71$

```python
fig = plt.figure(figsize=(12,5))
plt.hist(u_vec, bins=200, label='$\gamma$ = 0.8')
plt.hist(u_vec_new, bins=200, color='orange', label='$\gamma$ = 1.5')
plt.legend()
plt.title('Distribution of utility')
plt.xlabel('Utility')
plt.ylabel('Consumers');
```


# Fun applications

```python
import pandas as pd
import pandas_datareader
import datetime
import matplotlib.pyplot as plt
plt.style.use('seaborn')
```

# Friday effect in the stock market?

Let's investigate if there is a "Friday effect" on the stock market. That is, do stock prices on average increase more on Fridays than they do on other days of the week?

**Reading in data:**

```python
start = datetime.datetime(2014,1,1)
end   = datetime.datetime(2018,1,1)
```

```python
firms = []
for i,stock_name in enumerate(['IBM','AAPL', 'TSLA']):
    firm_stock = pandas_datareader.iex.daily.IEXDailyReader(stock_name, start, end).read()
    firm_stock['firm'] = stock_name
    firms.append(firm_stock)
```

```python
stocks = pd.concat(firms)
```

```python
stocks.sample(3)
```

      open
      high
      low
      close
      volume
      firm

      date

      2014-09-02
      275.5000
      284.8900
      274.3000
      284.120
      9852351
      TSLA

      2017-10-24
      338.8000
      342.8000
      336.1600
      337.340
      4491672
      TSLA

      2014-09-18
      93.8859
      94.2728
      93.5451
      93.757
      37299435
      AAPL

```python
# convert index from type 'O' to 'datetime'
stocks.index = pd.to_datetime(stocks.index)
```

```python
# sort dataframe
stocks.sort_values(['firm','date'], inplace=True)
```

```python
# create the weekday variable
stocks['weekday'] = stocks.index.weekday
stocks['year']    = stocks.index.year
```

**First differences:** Here we must avoid the temptation to use `df.var.diff(1)`, which will take differences across different firms. Instead, we have to use `df.groupby('firm').var.diff(1)` to ensure that we stay within the firm. This will produce a NaN as the first value for each firm.

```python
stocks['diff_close'] = stocks.groupby('firm').close.diff(1)
```

## Analysis based on raw averages

```python
ax = stocks.groupby('weekday')['close'].mean().plot(kind='line', style='-o');
ax.set_ylabel('Avg. closing price');
ax.set_xticks(range(5));
```

**Mean**

```python
ax = stocks.groupby('weekday')['diff_close'].mean().plot(kind='bar');
ax.set_ylabel('Avg. log-difference in closing price');
```

**Median**

```python
# could there be outliers? then the median is better than the mean
ax = stocks.groupby('weekday')['diff_close'].median().plot(kind='bar');
ax.set_ylabel('median log-difference in closing price');
```

**By firm**

```python
# effects by firms?
stocks.groupby(['firm','weekday']).diff_close.median().unstack().plot(kind='bar');
```

**Summing up so far:** It seems that companies iffer, but there is a tendency to higher prices on Mondays... surprisingly (maybe we'd expect people to be angry and pessimistic Monday morning?)

**Question:** What about quantities? If they differ over the week, it's not so surprising that prices differ as well.

```python
# Avg. quantity by day by firm
stocks.groupby(['firm','weekday']).volume.mean().unstack().plot(kind='bar');
```

Damn, Apple's volume is so much higher, we cannot compare these directly. We can't even look at deviations from the mean, since even those are larger. However, *percent deviations from the mean* might make sense.

```python
# transformation frunction
def pct_deviation_from_mean(x):
    m = np.mean(x)
    dev = (x-m)/m
    return dev

# one-step split-apply-combine
stocks['vol_pct_dev_from_mean'] = stocks.groupby('firm')['volume'].transform(pct_deviation_from_mean)

# plot
ax = stocks.groupby(['firm','weekday'])['vol_pct_dev_from_mean'].mean().unstack().plot(kind='bar');
ax.set_ylabel('Volume (pct. deviation from firm avg.)');
```

## Analysis based on regressions

Let's run the regression
\\[ \Delta p_{jt} = \beta_0 + \text{weekday dummies} + \text{firm fixed effect} + \text{year fixed effects} + \text{error} \\]

The weekday fixed effects will be so that 0 = Monday (left out, the reference category), 1 = Tuesday, ..., 4 = Friday. Thus, we test for the Friday effect by examining the p-value for the 4th weekday, denoted `C(weekday)[T.4]`.

***Note*** We will not expect regressions in any of your work.

```python
import statsmodels.formula.api as sm
res = sm.ols(formula = 'diff_close~C(weekday)+C(firm)+C(year)', data = stocks).fit()

res.summary()
```

OLS Regression Results

  Dep. Variable:	       diff_close	      R-squared:         	    0.001

  Model:	                   OLS	         Adj. R-squared:    	   -0.002

  Method:	             Least Squares	    F-statistic:       	   0.4364

  Date:	             Wed, 05 Jun 2019	   Prob (F-statistic):	   0.916

  Time:	                 23:02:51	       Log-Likelihood:    	  -7351.5

  No. Observations:	        2700	        AIC:               	 1.472e+04

  Df Residuals:	            2690	        BIC:               	 1.478e+04

  Df Model:	                   9

  Covariance Type:	      nonrobust

         	            coef	     std err	      t	      P>|t|	  [0.025	    0.975]

  Intercept	           0.2209	     0.250	     0.883	  0.377	    -0.270	     0.711

  C(weekday)[T.1]	    -0.0966	     0.227	    -0.426	  0.670	    -0.541	     0.348

  C(weekday)[T.2]	    -0.2176	     0.226	    -0.961	  0.337	    -0.662	     0.226

  C(weekday)[T.3]	    -0.2673	     0.228	    -1.171	  0.242	    -0.715	     0.180

  C(weekday)[T.4]	    -0.2515	     0.228	    -1.102	  0.271	    -0.699	     0.196

  C(firm)[T.IBM]	     -0.1015	     0.174	    -0.583	  0.560	    -0.443	     0.240

  C(firm)[T.TSLA]	     0.0265	     0.174	     0.152	  0.879	    -0.315	     0.368

  C(year)[T.2015]	    -0.0292	     0.222	    -0.131	  0.895	    -0.465	     0.406

  C(year)[T.2016]	    -0.0047	     0.222	    -0.021	  0.983	    -0.440	     0.431

  C(year)[T.2017]	     0.1677	     0.222	     0.755	  0.451	    -0.268	     0.604

  Omnibus:	       512.737	   Durbin-Watson:     	    1.920

  Prob(Omnibus):	  0.000	    Jarque-Bera (JB):  	 8797.757

  Skew:	          -0.396	    Prob(JB):          	     0.00

  Kurtosis:	      11.808	    Cond. No.          	     7.55

Warnings:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

Want $\LaTeX$? No problem!

```python
res.summary().as_latex()
```

    '\\begin{center}\n\\begin{tabular}{lclc}\n\\toprule\n\\textbf{Dep. Variable:}    &    diff_close    & \\textbf{  R-squared:         } &     0.001   \\\\\n\\textbf{Model:}            &       OLS        & \\textbf{  Adj. R-squared:    } &    -0.002   \\\\\n\\textbf{Method:}           &  Least Squares   & \\textbf{  F-statistic:       } &    0.4364   \\\\\n\\textbf{Date:}             & Wed, 05 Jun 2019 & \\textbf{  Prob (F-statistic):} &    0.916    \\\\\n\\textbf{Time:}             &     23:02:51     & \\textbf{  Log-Likelihood:    } &   -7351.5   \\\\\n\\textbf{No. Observations:} &        2700      & \\textbf{  AIC:               } & 1.472e+04   \\\\\n\\textbf{Df Residuals:}     &        2690      & \\textbf{  BIC:               } & 1.478e+04   \\\\\n\\textbf{Df Model:}         &           9      & \\textbf{                     } &             \\\\\n\\bottomrule\n\\end{tabular}\n\\begin{tabular}{lcccccc}\n                         & \\textbf{coef} & \\textbf{std err} & \\textbf{t} & \\textbf{P$>$$|$t$|$} & \\textbf{[0.025} & \\textbf{0.975]}  \\\\\n\\midrule\n\\textbf{Intercept}       &       0.2209  &        0.250     &     0.883  &         0.377        &       -0.270    &        0.711     \\\\\n\\textbf{C(weekday)[T.1]} &      -0.0966  &        0.227     &    -0.426  &         0.670        &       -0.541    &        0.348     \\\\\n\\textbf{C(weekday)[T.2]} &      -0.2176  &        0.226     &    -0.961  &         0.337        &       -0.662    &        0.226     \\\\\n\\textbf{C(weekday)[T.3]} &      -0.2673  &        0.228     &    -1.171  &         0.242        &       -0.715    &        0.180     \\\\\n\\textbf{C(weekday)[T.4]} &      -0.2515  &        0.228     &    -1.102  &         0.271        &       -0.699    &        0.196     \\\\\n\\textbf{C(firm)[T.IBM]}  &      -0.1015  &        0.174     &    -0.583  &         0.560        &       -0.443    &        0.240     \\\\\n\\textbf{C(firm)[T.TSLA]} &       0.0265  &        0.174     &     0.152  &         0.879        &       -0.315    &        0.368     \\\\\n\\textbf{C(year)[T.2015]} &      -0.0292  &        0.222     &    -0.131  &         0.895        &       -0.465    &        0.406     \\\\\n\\textbf{C(year)[T.2016]} &      -0.0047  &        0.222     &    -0.021  &         0.983        &       -0.440    &        0.431     \\\\\n\\textbf{C(year)[T.2017]} &       0.1677  &        0.222     &     0.755  &         0.451        &       -0.268    &        0.604     \\\\\n\\bottomrule\n\\end{tabular}\n\\begin{tabular}{lclc}\n\\textbf{Omnibus:}       & 512.737 & \\textbf{  Durbin-Watson:     } &    1.920  \\\\\n\\textbf{Prob(Omnibus):} &   0.000 & \\textbf{  Jarque-Bera (JB):  } & 8797.757  \\\\\n\\textbf{Skew:}          &  -0.396 & \\textbf{  Prob(JB):          } &     0.00  \\\\\n\\textbf{Kurtosis:}      &  11.808 & \\textbf{  Cond. No.          } &     7.55  \\\\\n\\bottomrule\n\\end{tabular}\n%\\caption{OLS Regression Results}\n\\end{center}\n\nWarnings: \\newline\n [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.'

**Conclusion:** The Friday dummy is not statistically significantly different from zero (p = 0.087 > 0.05). Thus, we conclude that there is no more growth in prices on Fridays compared to other days.

# The relationship between income and CO2 emissions

What is the relationship between GDP and emissions? Do the rich pollute more than the poor? Do fast-growing countries start polluting more?

## Downloading data

We will use the World Bank as the source of our data. See more here: https://data.worldbank.org/indicator/EN.ATM.CO2E.PC?locations=US-CN-DK-RU&view=chart

```python
from pandas_datareader import wb
```

```python
countries = ['DK','ZA','US','GB','CN','IN','BR','CA','RU','KR','VN','SE','DE','FR','BG','IT','PK','ID','MX','PL']
```

```python
co2 = wb.download(indicator='EN.ATM.CO2E.KT', country=countries, start=1970, end=2017) # alternatively, "EN.ATM.CO2E.PC" is per cap.
pop = wb.download(indicator='SP.POP.TOTL',    country=countries, start=1970, end=2017)
gdp = wb.download(indicator='NY.GDP.PCAP.KD', country=countries, start=1970, end=2017)
```

```python
# merge datasets
both = pd.merge(co2, gdp,how='outer',left_index=True,right_index=True)
both = pd.merge(both,pop,how='outer',left_index=True,right_index=True)

# process
both.reset_index(inplace=True)
both.year = both.year.astype(int) # datatype: object -> integer
both = both.rename(columns={'EN.ATM.CO2E.KT':'co2_tot', 'NY.GDP.PCAP.KD':'gdp', 'SP.POP.TOTL':'population'})
both['co2'] = both.co2_tot / both.population # per capita based on KT number

# sort by year
both = both.sort_values(['country','year'])

# drop rows with missings in any variable
both = both.dropna()
```

## Overview: Across-country comparisons

First we look at the average emissions over time. The US and Canada are the worst countries. Denmark is somewhere in the middle, but worse than Sweden :(

```python
ax = both.groupby('country').co2.mean().plot(kind='bar')
ax.set_ylabel('Avg. annual emissions per capita (kt CO2/cap.)');
```

How about the annualized growth rates? Recall that these are calculated using the formula
\\[ \text{annual growth} = \left( \frac{y_{t_1}}{y_{t_0}} \right) ^ {\frac{1}{t_1 - t_0}} \\]

```python
def annual_growth(x):
    x_last    = x.values[-1]
    x_first   = x.values[0]
    num_years = len(x)

    growth_annualized = (x_last/x_first)**(1/num_years) - 1.0
    return growth_annualized

ax = both.groupby('country')['co2'].agg(annual_growth).plot(kind='bar')
ax.set_ylabel('Annual growth from first to last year');
```

Now we see that many developing countries (China, Vietnam, Korea, India) stand out with massive increases in CO2 emissions. For most of the developed countries, we see modest decreases in emissions.

Next, we will zoom in on the developments over time for each country. We start with one big graph that is impossible to read.

```python
fig = plt.figure()
ax = plt.subplot(111)
both.set_index('year').groupby('country')['co2_tot'].plot(kind='line', legend=True, ax=ax);
ax.set_ylabel('Total CO2 emissions (kt)')
box = ax.get_position() # find plot coordinates
ax.set_position([box.x0, box.y0 + box.height * 0.1,box.width, box.height * 0.9]) # shrink height by 10% at bottom
ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15),ncol=5); # Put a legend below current axis
```

That graph is extremely hard to read so let's use a `FacetGrid` from the plotting package `seaborn`:

```python
import seaborn as sns # a package full of nice things! Google it...
```

```python
by_var = 'country'
y_var  = 'co2'

g = sns.FacetGrid(both, col=by_var, hue=by_var, col_wrap=4)
g = g.map(plt.plot, 'year', y_var) # draw the upper line
g = g.map(plt.fill_between, 'year', y_var, alpha=0.2).set_titles("{col_name} "+by_var) # draw the underlying area
g = g.set_titles("{col_name}") # # Control the title of each facet
```

**Tentative conclusion:** We have seen strong increases in emissions in many developing countries and minor falls in emissions in some of the more developed countries. Initially, this might indicate that there is no strong relationship between CO2 and GDP. However

## GDP and CO2

The above is across countries, what is the relationship between avg. GDP and avg. emissions. What about within a country? How do changes in GDP correlate with changes in CO2?

### Across-country comparisons

First, we analyze the problem by focusing on comparisons that go across countries. To do this, we must collapse the data within a country, e.g. by taking the average emissions over time.

```python
(both.groupby('country').mean()
      .apply(np.log)
      .plot(kind='scatter', x='gdp', y='co2'));
```

This graph seems to indicate that there is indeed a relationship between GDP and CO2: countries with a higher GDP per capita tend to have higher CO2 emissions per capita. It even looks like one log point in GDP translates into nearly a full log point in CO2.

### Within-country comparisons

It is possible that the across-country comparison above is invalid. There might be an omitted variable. It might be that large economies are located where it is cold and thus have to spend much effort to heat houses, which burns CO2. It might be that if we take any *given* country, that *all else equal* increasing GDP will lead to lower CO2. Maybe because the country can now afford to invest in green tech.

To explore whether that is the case, we can focus on *within-country variation* rather than *across country variation*. One example of what is meant by this is that we can focus on the relationship between observed changes in GDP and changes in CO2.

#### Average differences

First, we compute for each country (using a `groupby`), the average one-year growth rate for that country, using the function `pct_change()`.

```python
ax = both.groupby('country').agg(lambda x : x.pct_change().mean()).plot(kind='scatter',x='gdp',y='co2')
ax.set_xlabel('Avg. one-year growth in GDP');
ax.set_ylabel('Avg. one-year groth in CO2 emissions');
```

Just as a test, let us make sure that the *average log differences* give approximately the same picture:

```python
ax = both.groupby('country').agg(lambda x: (np.log(x)).diff().mean()).plot(kind='scatter',x='gdp',y='co2')
ax.set_xlabel('Avg. one-year log-difference in GDP');
ax.set_ylabel('Avg. one-year log-difference in CO2');
```

**Partial conclusion:** From these graphs, we draw the same conclusion: there is a positive relationship between changes in GDP and changes in CO2. *However*, we do note that for sufficiently low changes in GDP (less than 2%), CO2 emissions are actually *falling*. If we take that at face value, this appears to indicate that it is possible to have positive growth while still reducing emissions. But the relationship is monotonic (based on what our data tells us).

#### Pooled differences

Instead of taking the average (over time) change in GDP and in CO2  within each country, we can also just plot all the annual differences.

```python
logdiffs = both.set_index('country').groupby(level=0).transform(lambda x : (np.log(x)).diff(1) ).reset_index()
```

```python
logdiffs.plot(kind='scatter', x='gdp', y='co2');
```

Again we see there appears to be a positive relationship. However, we may be worried that it is driven by *outliers*. That is, we would like to think about how much data we have where. For this, we can use the `seaborn` plot, `jointplot`, which simultaneously indicates (with color intensity and marginal histograms) what the data density looks like. We would lik to mainly draw our conclusions based on where we have a fair amount of data.

```python
h = sns.jointplot(x=logdiffs.gdp, y=logdiffs.co2, kind='hex')
h.set_axis_labels('log change in GDP', 'log change in CO2');
```

### Regression model

In many settings, a linear regression can be a great way of summarizing the dataset.

We will estimate the following models
\begin{align}
  (0): \log \text{CO}_2 &= \beta_0 + \beta_1 \log \text{GDP}  + \text{error} \\
  (1): \log \text{CO}_2 &= \beta_0 + \beta_1 \log \text{GDP} + \text{time trend} + \text{error} \\
  (2): \log \text{CO}_2 &= \beta_0 + \beta_1 \log \text{GDP} + \text{time trend} + \text{country fixed effects} + \text{error} \\
 (3): \Delta \log \text{CO}_2 &= \tilde{\beta}_0 + \beta_1 \Delta \log \text{GDP} + \text{error} \\
  (4): \Delta \log \text{CO}_2 &= \tilde{\beta}_0 + \beta_1 \Delta \log \text{GDP} + \text{country fixed effects} + \text{error}.
\end{align}

In model (0), we are simply comparing all observations. If there is a general tendency over time that is unrelated to GDP but that has affected emissions, then we are not accounting for it. That is done by (1). However, there might still be omitted variables specific to each country that affect the emissions and affect the GDP and thus cause endogeneity (e.g. temperature / distance from equator). Model (2) takes this into account.

The models (3) and (4) are first-difference specifications. In principle, model (3) should be equivalent to (2) if it is correctly specified. In practice, the two can differ due to a number of misspecifications. For example, if the country fixed effects are slowly changing over time, perhaps due to slow, gradual changes in temperature (and thus in heating needs), or preferences or productivity. In that case, focusing on short-run movements in first-differences may be more acurate. On the other hand, if there are a lot of short-run fluctuations, then fixed effects are perhaps closer to the truth. In practice, these are questions that depend on the specific deviations. We will simply estimate both and check whether they are close.

```python
res0 = sm.ols(formula='np.log(co2)~1+np.log(gdp)',                    data=both).fit()
res1 = sm.ols(formula='np.log(co2)~1+np.log(gdp)+C(year)',            data=both).fit()
res2 = sm.ols(formula='np.log(co2)~1+np.log(gdp)+C(year)+C(country)', data=both).fit()
res3 = sm.ols(formula='co2~1+gdp',                                    data=logdiffs).fit() # in this dataframe, variables are already logged
res4 = sm.ols(formula='co2~1+gdp+C(country)',                         data=logdiffs).fit() # in this dataframe, variables are already logged

print(f'Baseline:        {res0.params["np.log(gdp)"] : 8.4f}')
print(f'Year FE:         {res1.params["np.log(gdp)"] : 8.4f}')
print(f'Year+Country FE: {res2.params["np.log(gdp)"] : 8.4f}')
print(f'FD:              {res3.params["gdp"]         : 8.4f}')
print(f'FD + country FE: {res4.params["gdp"]         : 8.4f}')
```

    Baseline:          0.6255
    Year FE:           0.6281
    Year+Country FE:   0.8482
    FD:                0.9055
    FD + country FE:   0.8637

We could easily extend the model with e.g. country fixed effects by writing `co2~gdp+C(country)`.

**Plotting** the estimates in a cool graph:

```python
res = pd.DataFrame(data = [
        ['Baseline',         res0.params["np.log(gdp)"], res1.bse["np.log(gdp)"]],
        ['Year FE',        res1.params["np.log(gdp)"], res1.bse["np.log(gdp)"]],
        ['Year+country FE', res2.params["np.log(gdp)"], res2.bse["np.log(gdp)"]],
        ['FD',              res3.params["gdp"],         res3.bse["gdp"]],
        ['FD+country FE',   res4.params["gdp"],         res4.bse["gdp"]],
    ], columns=['Model','estimate','se'])
```

```python
fig, ax = plt.subplots()
x_pos = range(res.shape[0])
ax.bar(x_pos, res.estimate, yerr=res.se,  alpha=0.5, ecolor='black', capsize=5);
ax.set_xticks(x_pos);
ax.set_xticklabels(res.Model);
ax.set_ylabel('Estimate');
ax.set_xlabel('Model');
```

### Conclusion

**Overall:** Unfortunately, our investigations document a clear correlation between GDP and CO2 Emissions. We estimate an elasticity between 0.63 and 0.90, although we put more confidence in the three models that account for country fixed effects, where the estimate ranges between 0.85 and 0.90. This is a quite high elasticity, implying that economic growth almost moves one-for-one with CO2 emissions.


# 1D Advection-Diffusion problem

## General formulation

This is a well known problem to FEM with Continuous Galerkin approximation. Spurious oscillations rise when certain (not rare) conditions are met. The general problem consists in:

Find $u \in \mathbb{C}^2(\overline{\Omega})$ in a closed domain $\overline{\Omega}\subset \mathbb{R}^n\,(n=1,2,3)$  such that:

\begin{equation}
\left\{
\begin{aligned}
- &k \Delta u + \mathbf{b}\cdot \nabla u = f(x), \quad \forall x \in \Omega \\
&\left. u \right|_{\partial \Omega} = g(x)
\end{aligned}
\right.
\end{equation}
where $\overline{\Omega} := \Omega \cup \partial \Omega$ denotes the closed domain, $\Omega$ is the open domain and $\partial \Omega$ is the domain's boundary (the closure). Note that $\Omega \cap \partial \Omega = \emptyset$. The solution is a scalar field such that $u: \overline{\Omega} \to \mathbb{R}$. Also, we have the "source term" that is a scalar field of the same form, $f: \overline{\Omega} \to \mathbb{R}$, but this function is a given data to the (direct) problem. The $g(x)$ is a prescribed function applied to the boundary, which is also a given data to the problem. The parameter $k$ (here a scalar field like $u$) sometimes is named as "diffusivity coefficient" related to the quantity $u$, in a physical context (transport of a quantity, look for Reynolds' Theorem for further clarifications). In some contexts, parameter $k$ must be a 2-rank tensor, as in anisotropic Porous Media flows. Analagously, we have the parameter $\mathbf{b}: \overline{\Omega} \to \mathbb{R}^n$ (a vector field), which is denoted as the "advective velocity" related to the medium velocity where $u$ is been transport. Parameters $k$ and $\mathbf{b}$ are given data to the (direct) problem too.

P.S.: I will not discuss the ill problems formulation, neither others mathematical requirements to assert the well-posedness of the problem, uniqueness and so on.

## 1D simplification

In some scenarios, the mathematical modeling approach can be simplified to the 1D case, which is written as follows:

Find $u \in \mathbb{C}^2$ such that:

\begin{equation}
\left\{
\begin{aligned}
- &k u''(x) + b u'(x) = f(x), \quad \forall x \in ((x_1, x_2) \subset \mathbb{R}) \\
&u(x_1) = u_1 \\
&u(x_2) = u_2
\end{aligned}
\right.
\end{equation}

where $u_1, u_2 \in \mathbb{R}$ are the prescribed boundary condition values, $u'(x) \equiv \dfrac{d u}{d x}$ and thus $u''(x) \equiv \dfrac{d^2 u}{d x^2}$. For the sake of simplicity, I will omit the argument of $u$ function. Such aforementioned problems are known as Two-Point Boundary Value problems.

### Variational formulation and Galerkin approximation

One can find the derivation of the weak form, but I will just give its result below.

Given the space of admissible solution:

\begin{equation}
\mathcal{U} (\overline{\Omega}) := \left\{ u \in H^1 (\overline{\Omega}) \left| \,u(x_1) = u_1, u(x_2) = u_2 \right. \right\}
\end{equation}

and the space of suitable variations as

\begin{equation}
\mathcal{V} (\overline{\Omega}) := \left\{ v \in H^1 (\overline{\Omega}) \left| \,u(x_1) = u(x_2) = 0 \right. \right\}
\end{equation}

The discretization of the above spaces is performed by Galerkin approximation with Lagrangean function space $\mathbb{P}$ of order $k$ defined over $\overline{\Omega}$. Thus we define here:

\begin{equation}
            \mathcal{S}_h^k(\overline{\Omega}) := \left\{ \varphi_h \in \mathcal{C}(\Omega^e): \left.\varphi\right|_{\Omega^e}
            \in \mathbb{P}_k(\Omega^e), \forall \Omega^e \in \mathcal{T}_h \right\}
\end{equation}

the space of Lagrange polynomials subject to a domain partition $\mathcal{T}_h:=\cup \Omega^e \approx \overline{\Omega}$ and $\cap \Omega^e = \emptyset$ (non-overlapping elements). Thus

\begin{equation}
    \mathcal{U}_h := \mathcal{U} \cap \mathcal{S}_h^k \quad \text{and} \quad
    \mathcal{V}_h := \mathcal{V} \cap \mathcal{S}_h^k
\end{equation}

we have the discretized spaces.

Now, we can stated out our "discretized" Variational Formulation as:

Find $u_h \in \mathcal{U}_h$ such that

\begin{equation}
a(u_h, v_h) = F(v_h), \quad \forall v_h \in \mathcal{V}_h
\end{equation}

where

\begin{align}
a(u, v) &:= \int_{\Omega} u' v' dx
          + \int_{\Omega} b\, u' v dx\\
F(v) &:= \int_{\Omega} f v dx
\end{align}

and the domain is $\overline{\Omega} \equiv [x_1, x_2]$.

## A practical FEniCS example

Solve this in FEniCS is very straightforward compared to classical FEM codes. The FEniCS framework provides a high-level interface which eases the pain at most.

Just for learning purpose, let the given data be setted as:

\begin{align*}
&f(x) \equiv f = 1 & \\
&u_1 = u_2 = 0 & \\
&x_1 = 0, &x_2 = 1 \\
&k = 10^{-8}, &b = 1
\end{align*}

With the source term as $f(x) = 1$, the exact solution can be easily obtained:

\begin{equation}
u(x) \approx
\left\{
\begin{aligned}
&u(x) = x, &\text{if } x \in [x_1, x_2) \\
&u(x) = 0, &\text{if } x = x_2
\end{aligned}
\right.
\end{equation}

for small values of $k$, say $k = 10^{-8}$.

So, how to solve the problem with FEniCS? We will construct the procedures stepwisely.

* Importing all the libs we'll need.

```python
from fenics import *  # all the FEniCS namespace (not a recommended Python practice)
import matplotlib.pyplot as plt
from matplotlib import rc
import numpy as np
```

* Defining the domain and related mesh

```python
x_left = 0.0
x_right = 1.0
numel = 15
mesh = IntervalMesh(numel, x_left, x_right)  # IntervalMesh(num_of_elements, inf_interval, sup_interval)
mesh_ref = IntervalMesh(100, x_left, x_right)
```

* Setting the degree of the functions in the Continuous Galerkin method and the variation space. Additionaly, we define here a space to be employed in the projection of the reference analytical solution

```python
p = 1
V = FunctionSpace(mesh, "CG", p)  # "CG" stands for Continuous Galerkin, p is the degree
Vref = FunctionSpace(mesh_ref, "CG", p)
```

* Defining a Python function which marks the boundaries

```python
def left(x, on_boundary):
    return x < 0+DOLFIN_EPS
def right(x, on_boundary):
    return x > 1-DOLFIN_EPS
```

* Setting the prescribed boundary values

```python
u1, u2 = 0.0, 0.0
g_left = Constant(u1)
g_right = Constant(u2)
```

* Now we modify the spaces, as expected

```python
bc_left = DirichletBC(V, g_left, left)
bc_right = DirichletBC(V, g_right, right)
dirichlet_condition = [bc_left, bc_right]
```

* Here we define the source function over the domain

```python
f = Constant(1)
```

* Now comes the good part. We set up the Trial and Test functions from the admissible space

```python
u_h = TrialFunction(V)
v_h = TestFunction(V)
```

* The parameters of the problem

```python
k = Constant(1e-8)
b = Constant(1.0)
```

* Then we write the unsymmetric form, very much like it is written in mathematical form

```python
a = k*inner(grad(v_h), grad(u_h))*dx + b*inner(grad(u_h)[0], v_h)*dx
```

* Also we define the associated linear form

```python
L = f*v_h*dx
```

* Now we declare the solution variable

```python
u_sol = Function(V)
```

* Thus we set the discretized variational problem to be solved

```python
problem = LinearVariationalProblem(a, L, u_sol, dirichlet_condition)
```

* And we go further and solve it with no difficulties!

```python
solver = LinearVariationalSolver(problem)
solver.solve()
```

* But we will need to check if the solution is "good enough". So we compare with the available exact solution

```python
sol_exact = Expression(
    'x[0]<=0+tol || x[0]>=1 / b - tol ? 0 : x[0]',
    degree=1,
    b=b,
    tol=DOLFIN_EPS
)
u_e = interpolate(sol_exact, Vref)
u_e = interpolate(sol_exact, Vref)
```

* Now, lets plot our results!

```python
plot(u_sol, marker='x', label='Approx')
plot(u_e, label='Exact')
# Setting the font
plt.rc('text',usetex=True)
plt.rc('font', size=14)
# Plotting
plt.xlim(x_left, x_right)   # Limites do eixo x
plt.ylim(np.min(u_sol.vector().get_local()), 1.02*np.max(u_e.vector().get_local()))  # Limites do eixo y
plt.grid(True, linestyle='--')  # Ativa o grid do grafico
plt.xlabel(r'$x$')  # Legenda do eixo x
plt.ylabel(r'$u(x)$')   # Legenda do eixo y
plt.legend(loc='best',borderpad=0.5)    # Ativa legenda no grafico e diz para se posicionar na melhor localizacao detectada
plt.show()  # Exibe o grafico em tela
```

## Something seems not good...

Terrible, it isn't? This is due the high Péclet number of the problem. The Péclet number (to a scalar quantity) is conceptually defined as

\begin{equation}
Pe := \frac{\text{advection rate of a physical quantity}}{\text{diffusion rate of a physical quantity}}
\end{equation}

In Finite Element context, we evaluate the Péclet number in an element-wise manner, because this is how we (normally) stabilize the method and vanish the spurious oscillations. The local (within each element) Péclet number is denote as

\begin{equation}
Pe_K := \frac{m_k \left|\mathbf{b}\right|_p h_k}{2k}
\end{equation}

where $\left|\bullet\right|_p$ denote the lenght in terms of the p-dimensional euclidian norm. The $m_k$ parameter comes from the error analysis. One can check a summarized analysis in the paper "Revisiting stabilized finite element methods for the advective–diffusive equation" (Franca et. al, 2006). Highly recommended reading, by the way. But I can antecipate that

\begin{align}
&m_k := \min \left\{ \frac{1}{3}, 2C_k \right\} \\
&C_k\sum_{K} h_K^2 ||\Delta v||^2_{0,K} \leq ||\nabla v ||^2_0, \quad \forall v \in V_h
\end{align}

where $h_K$ is the mesh parameter, related to some characteristic lenght of the element. In 1D elements, it is just the size of the element. For our purpose (1D), we can assume $m_k = \dfrac{1}{3}$.

So, where all this stuff will be applied?

## SUPG stabilization

A very commom, and one of the most employed even nowaday, is the Streamline Upwind Petrov-Galerkin stabilization. It follows the idea used in finite difference method of using an upwind term, interpreted as an artifficial diffusion, which is added to the Variational problem. Didn't read Franca et. al (2006) yet?!

With the modification, the variational problem is rewritten as follows:

\begin{equation}
B(u,v) = F(v)
\end{equation}

and

\begin{align}
&B(u,v) := a(u,v) + R(u,v) \\
&F(v) := L(v) + S(v)
\end{align}

with

\begin{align}
&R(u,v) := \sum_{K}\tau_k \left(-k \Delta u + \mathbf{b}\cdot \nabla u, \mathbf{b}\cdot \nabla v  \right)_K \\
&S(v) := \sum_{K}\tau_k \left(f, \mathbf{b}\cdot \nabla v  \right)_K
\end{align}

where

\begin{equation}
(u, v)_{K} := \int_{\Omega^K} u v dx
\end{equation}

denotes the $L^2$ inner product restricted to the element $K$.

Note the presence of a stabilizing parameter $\tau_k$. By this parameter, we control how much we want to add to an element local contribution aiming to stabilize in the local formulation. This parameter is computed locally as follows (see Franca et. al, I insist):

\begin{equation}
\tau_K := \frac{h_K}{2|a|_P} \xi(Pe_K)
\end{equation}

where

\begin{equation}
\xi(Pe_K) :=
\left\{
\begin{aligned}
&Pe_K, &0\leq Pe_K < 1 \\
&1, &Pe_K \geq 1
\end{aligned}
\right.
\end{equation}

As a remark, I would like to say that there is SEVERAL ways to define and derive stabilizing parameters. The approach provided above is described by Franca et. al (2006). Nonetheless, the FEM stabilization methods is a very active research area even today, so one can find several stabilizing parameters elsewhere.

### 1D SUPG

To simplify to 1D, just remember that the gradient reduce to $\nabla (\bullet) \equiv \dfrac{d (\bullet)}{d x} = u'$. Thus,

\begin{align}
&R(u,v) := \sum_{K}\tau_k \left(-k u'' + bu', bv' \right)_K \\
&S(v) := \sum_{K}\tau_k \left(f, bv'  \right)_K
\end{align}

## Revisiting the FEniCS implementation

Finally, we can stabilize with SUPG the previous Continuous Galerkin FEM approach.

* First, we need to define the stabilizing parameter and its related quantities

```python
m_k = 1.0/3.0
h_k = CellDiameter(mesh) # Mesh parameter
b_norm = abs(b)
Pe_k = m_k*b_norm*h_k/(2.0*k)
one = Constant(1.0)
eps_k = conditional(gt(Pe_k,one), one, Pe_k)
tau_k = h_k/(2.0*b_norm)*eps_k
```

* Now we add the stabilizing terms to the classical Galerkin formulation

```python
a += inner((b*grad(u_h)[0] - k*div(grad(u_h))), tau_k*b*grad(v_h)[0])*dx
L += f*tau_k*b*grad(v_h)[0]*dx
```

* Redefining the problem

```python
u_sol = Function(V)
problem = LinearVariationalProblem(a, L, u_sol, dirichlet_condition)
```

* Solving the stabilized problem

```python
solver = LinearVariationalSolver(problem)
solver.solve()
```

* Now we plot the results to check

```python
plot(u_sol, marker='x', label='Approx')
plot(u_e, label='Exact')
# Configuracoes de fontes no grafico
plt.rc('text',usetex=True)
plt.rc('font', size=14)
# Plotando
plt.xlim(x_left, x_right)   # Limites do eixo x
plt.ylim(np.min(u_sol.vector().get_local()), 1.02*np.max(u_e.vector().get_local()))  # Limites do eixo y
plt.grid(True, linestyle='--')  # Ativa o grid do grafico
plt.xlabel(r'$x$')  # Legenda do eixo x
plt.ylabel(r'$u(x)$')   # Legenda do eixo y
plt.legend(loc='best',borderpad=0.5)    # Ativa legenda no grafico e diz para se posicionar na melhor localizacao detectada
plt.show()  # Exibe o grafico em tela
```

Now things look right.

## Another stabilization: Galerkin Least Squares (GLS)

An alternative stabilization is the very well known method named Galerkin Least Squares, or GLS for short.
This methods was introduced by Hughes, Franca and Hulbert (see the paper "A new finite element formulation for computational fluid dynamics: VIII. The Galerkin/least-squares method for advective–diffusive equations" for further details).

It's very similar to SUPG method in the modification sense related to the classical Galerkin formulation. Now, we have the following terms to add:

\begin{align}
&R_{GLS}(u,v) := \sum_{K}\tau_k \left(-k \Delta u + \mathbf{b}\cdot \nabla u, \mathbf{b}\cdot \nabla v - k \Delta v \right)_K \\
&S_{GLS}(v) := \sum_{K}\tau_k \left(f, \mathbf{b}\cdot \nabla v - k \Delta v  \right)_K
\end{align}

which trivially is extended to 1D as

\begin{align}
&R_{GLS}(u,v) := \sum_{K}\tau_k \left(-k u'' + bu', bv' -k v'' \right)_K \\
&S_{GLS}(v) := \sum_{K}\tau_k \left(f, bv' -k v'' \right)_K
\end{align}

Note that for linear elements, better saying, for those with linear shape functions, the formulation of SUPG and GLS are the same, because $\Delta v = 0$.

### 1D GLS implementation in FEniCS

The stabilization parameter and its parameters can be pretty much the same as in SUPG. Minor changes are necessary, but we will solve within a different variational problem just to compare the methods.

* The classical Continuous Galerkin terms

```python
a_GLS = k*inner(grad(v_h), grad(u_h))*dx + b*inner(grad(u_h)[0], v_h)*dx
L_GLS = f*v_h*dx
```

* Adding the GLS terms

```python
a_GLS += inner((b*grad(u_h)[0] - k*div(grad(u_h))), tau_k*b*grad(v_h)[0] - k*div(grad(v_h)))*dx
L_GLS += tau_k*b*inner(f, grad(v_h)[0] - k*div(grad(v_h)))*dx
```

* Now we solve the problem with GLS

```python
u_sol_gls = Function(V)
problem = LinearVariationalProblem(a_GLS, L_GLS, u_sol_gls, dirichlet_condition)
solver = LinearVariationalSolver(problem)
solver.solve()
```

* Then we plot:

```python
plot(u_sol, marker='.', label='SUPG')
plot(u_sol_gls, marker='x', label='GLS')
plot(u_e, label='Exact')
# Configuracoes de fontes no grafico
plt.rc('text',usetex=True)
plt.rc('font', size=14)
# Plotando
plt.xlim(x_left, x_right)   # Limites do eixo x
plt.ylim(np.min(u_sol.vector().get_local()), 1.02*np.max(u_e.vector().get_local()))  # Limites do eixo y
plt.grid(True, linestyle='--')  # Ativa o grid do grafico
plt.xlabel(r'$x$')  # Legenda do eixo x
plt.ylabel(r'$u(x)$')   # Legenda do eixo y
plt.legend(loc='best',borderpad=0.5)    # Ativa legenda no grafico e diz para se posicionar na melhor localizacao detectada
plt.show()  # Exibe o grafico em tela
```

The above result confirms what we expected: Same result to linear polynomials $p = 1$.


```python
import numpy as np, matplotlib.pyplot as plt
from scipy.integrate import odeint
```

## Simplified Model

### Equations

\begin{equation}
\begin{split}
\dot{S} &= -\beta_1 I_1 S -\beta_2 I_2 S - \beta_3 I_3 S\\
\dot{E} &=\beta_1 I_1 S +\beta_2 I_2 S + \beta_3 I_3 S - a E \\
\dot{I_1} &= a E - \gamma_1 I_1 - p_1 I_1 \\
\dot{I_2} &= p_1 I_1 -\gamma_2 I_2 - p_2 I_2 \\
\dot{I_3} & = p_2 I_2 -\gamma_3 I_3 - \mu I_3 \\
\dot{R} & = \gamma_1 I_1 + \gamma_2 I_2 + \gamma_3 I_3 \\
\dot{D} & = \mu I_3
\end{split}
\end{equation}

Simplified:

\begin{equation}
\begin{split}
\dot{S} &= -\beta I S \\
\dot{E} &=\beta I S - a E \\
\dot{I} &= a E - \gamma  I \\
\dot{R} & = \gamma I \\
\dot{D} & = \mu I
\end{split}
\end{equation}

### Variables
* $S$: Susceptible individuals
* $E$: Exposed individuals - infected but not yet infectious or symptomatic
* $I_i$: Infected individuals in severity class $i$. Severity increaes with $i$ and we assume individuals must pass through all previous classes
  * $I_1$: Mild infection (hospitalization not required)
  * $I_2$: Severe infection (hospitalization required)
  * $I_3$: Critical infection (ICU required)
* $R$: individuals who have recovered from disease and are now immune
* $D$: Dead individuals
* $N=S+E+I_1+I_2+I_3+R+D$ Total population size (constant)

### Parameters
* $\beta_i$ rate at which infected individuals in class $I_i$ contact susceptibles and infect them
* $a$ rate of progression from the exposed to infected class
* $\gamma_i$ rate at which infected individuals in class $I_i$ recover from disease and become immune
* $p_i$ rate at which infected individuals in class $I_i$ progress to class $I_{I+1}$
* $\mu$ death rate for individuals in the most severe stage of disease

### Basic reproductive ratio

Idea: $R_0$ is the sum of
1. the average number of secondary infections generated from an individual in stage $I_1$
2. the probability that an infected individual progresses to $I_2$ multiplied by the average number of secondary infections generated from an individual in stage $I_2$
3.  the probability that an infected individual progresses to $I_3$ multiplied by the average number of secondary infections generated from an individual in stage $I_3$

\begin{equation}
\begin{split}
R_0 & = N\frac{\beta_1}{p_1+\gamma_1} + \frac{p_1}{p_1 + \gamma_1} \left( \frac{N \beta_2}{p_2+\gamma_2} + \frac{p_2}{p_2 + \gamma_2} \frac{N \beta_3}{\mu+\gamma_3}\right)\\
&= N\frac{\beta_1}{p_1+\gamma_1} \left(1 + \frac{p_1}{p_2 + \gamma_2}\frac{\beta_2}{\beta_1} \left( 1 + \frac{p_2}{\mu + \gamma_3} \frac{\beta_3}{\beta_2} \right) \right)
\end{split}
\end{equation}

```python
[0]*10
```

    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

```python
#Defining the differential equations

#Don't track S because all variables must add up to 1
#include blank first entry in vector for beta, gamma, p so that indices align in equations and code.
#In the future could include recovery or infection from the exposed class (asymptomatics)

def seir(y,t,b,a,g,u,N):
    dy=[0]*len(y)
    S=N-sum(y);
    dy[0] = -b*S-a*y[0] # E
    dy[1]= a*y[0]-g*y[1] #I
    dy[2]= g*y[1] #R
    dy[3]=u*y[1] #D

    return dy
```

```python
# Define parameters based on clinical observations

#I will add sources soon
# https://github.com/midas-network/COVID-19/tree/master/parameter_estimates/2019_novel_coronavirus

IncubPeriod=5  #Incubation period, days
DurMildInf=10 #Duration of mild infections, days
FracMild=0.8  #Fraction of infections that are mild
FracSevere=0.15 #Fraction of infections that are severe
FracCritical=0.05 #Fraction of infections that are critical
CFR=0.02 #Case fatality rate (fraction of infections resulting in death)
TimeICUDeath=7 #Time from ICU admission to death, days
DurHosp=11 #Duration of hospitalization, days

```

```python
# Define parameters and run ODE

N=1000
#b=np.zeros(4) #beta
#u=np.zeros(4) #gamma
#p=np.zeros(3)

a=1/IncubPeriod
u=(1/TimeICUDeath)*(CFR/FracCritical)
g = (1-u)
b=2.5e-2 # hospitalized cases don't transmit

#Calculate basic reproductive ratio
#R0=N*((b[1]/(p[1]+g[1]))+(p[1]/(p[1]+g[1]))*(b[2]/(p[2]+g[2])+ (p[2]/(p[2]+g[2]))*(b[3]/(u+g[3]))))
R0 = N*(b/g)
print("R0 = {0:4.1f}".format(R0))
```

    R0 = 26.5

```python
print(b)
print(a)
print(g)
#print(p)
print(u)
```

    0.025
    0.2
    0.9428571428571428
    0.057142857142857134

\begin{equation}
\begin{split}
\dot{S} &= -\beta I S \\
\dot{E} &=\beta I S - a E \\
\dot{I} &= a E - \gamma  I \\
\dot{R} & = \gamma I \\
\dot{D} & = \mu I
\end{split}
\end{equation}

```python
tmax=365
tvec=np.arange(0,tmax,0.1)
ic=np.zeros(4)
ic[0]=1

soln=odeint(seir,ic,tvec,args=(b,a,g,u,N))
soln=np.hstack((N-np.sum(soln,axis=1,keepdims=True),soln))

plt.figure(figsize=(13,5))
plt.subplot(1,2,1)
plt.plot(tvec,soln)
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("S","E","I","R","D"))
plt.ylim([0,10])

#Same plot but on log scale
plt.subplot(1,2,2)
plt.plot(tvec,soln)
plt.semilogy()
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("S","E","I","R","D"))
plt.ylim([0.001,10])
#plt.tight_layout()
```

```python
N
```

    1000

```python
soln
```

    array([[ 9.99000000e+02,  1.00000000e+00,  0.00000000e+00,
             0.00000000e+00,  0.00000000e+00],
           [ 1.00150062e+03, -1.49559574e+00, -5.17309775e-03,
             1.44375507e-04,  8.75003071e-06],
           [ 1.00400766e+03, -3.94797273e+00, -5.70915939e-02,
            -2.44411243e-03, -1.48128026e-04],
           ...,
           [ 1.45372503e+07, -1.60595188e+06, -3.31411146e+05,
            -1.18789509e+07, -7.19936417e+05],
           [ 1.45755376e+07, -1.61018153e+06, -3.32283994e+05,
            -1.19102394e+07, -7.21832688e+05],
           [ 1.46139256e+07, -1.61442232e+06, -3.33159142e+05,
            -1.19416102e+07, -7.23733954e+05]])

```python
plt.plot([-s[3] for s in soln])
plt.semilogy()
```

```python
# get observed growth rate r (and doubling time) for a particular variable between selected time points
#(all infected classes eventually grow at same rate during early infection)

#Don't have a simple analytic formula for r for this model due to the complexity of the stages

def growth_rate(tvec,soln,t1,t2,i):
  i1=np.where(tvec==t1)[0][0]
  i2=np.where(tvec==t2)[0][0]
  r=(np.log(soln[i2,1])-np.log(soln[i1,1]))/(t2-t1)
  DoublingTime=np.log(2)/r

  return r, DoublingTime
```

```python
(r,DoublingTime)=growth_rate(tvec,soln,10,20,1)
print("The epidemic growth rate is = {0:4.2f} per day and the doubling time {1:4.1f} days ".format(r,DoublingTime))
```

    The epidemic growth rate is = 0.08 per day and the doubling time  9.0 days

## Original Model

### Equations

\begin{equation}
\begin{split}
\dot{S} &= -\beta_1 I_1 S -\beta_2 I_2 S - \beta_3 I_3 S\\
\dot{E} &=\beta_1 I_1 S +\beta_2 I_2 S + \beta_3 I_3 S - a E \\
\dot{I_1} &= a E - \gamma_1 I_1 - p_1 I_1 \\
\dot{I_2} &= p_1 I_1 -\gamma_2 I_2 - p_2 I_2 \\
\dot{I_3} & = p_2 I_2 -\gamma_3 I_3 - \mu I_3 \\
\dot{R} & = \gamma_1 I_1 + \gamma_2 I_2 + \gamma_3 I_3 \\
\dot{D} & = \mu I_3
\end{split}
\end{equation}

### Variables
* $S$: Susceptible individuals
* $E$: Exposed individuals - infected but not yet infectious or symptomatic
* $I_i$: Infected individuals in severity class $i$. Severity increaes with $i$ and we assume individuals must pass through all previous classes
  * $I_1$: Mild infection (hospitalization not required)
  * $I_2$: Severe infection (hospitalization required)
  * $I_3$: Critical infection (ICU required)
* $R$: individuals who have recovered from disease and are now immune
* $D$: Dead individuals
* $N=S+E+I_1+I_2+I_3+R+D$ Total population size (constant)

### Parameters
* $\beta_i$ rate at which infected individuals in class $I_i$ contact susceptibles and infect them
* $a$ rate of progression from the exposed to infected class
* $\gamma_i$ rate at which infected individuals in class $I_i$ recover from disease and become immune
* $p_i$ rate at which infected individuals in class $I_i$ progress to class $I_{I+1}$
* $\mu$ death rate for individuals in the most severe stage of disease

### Basic reproductive ratio

Idea: $R_0$ is the sum of
1. the average number of secondary infections generated from an individual in stage $I_1$
2. the probability that an infected individual progresses to $I_2$ multiplied by the average number of secondary infections generated from an individual in stage $I_2$
3.  the probability that an infected individual progresses to $I_3$ multiplied by the average number of secondary infections generated from an individual in stage $I_3$

\begin{equation}
\begin{split}
R_0 & = N\frac{\beta_1}{p_1+\gamma_1} + \frac{p_1}{p_1 + \gamma_1} \left( \frac{N \beta_2}{p_2+\gamma_2} + \frac{p_2}{p_2 + \gamma_2} \frac{N \beta_3}{\mu+\gamma_3}\right)\\
&= N\frac{\beta_1}{p_1+\gamma_1} \left(1 + \frac{p_1}{p_2 + \gamma_2}\frac{\beta_2}{\beta_1} \left( 1 + \frac{p_2}{\mu + \gamma_3} \frac{\beta_3}{\beta_2} \right) \right)
\end{split}
\end{equation}

```python
#Defining the differential equations

#Don't track S because all variables must add up to 1
#include blank first entry in vector for beta, gamma, p so that indices align in equations and code.
#In the future could include recovery or infection from the exposed class (asymptomatics)

def seir(y,t,b,a,g,p,u,N):
    dy=[0,0,0,0,0,0]
    S=N-sum(y);
    dy[0]=np.dot(b[1:3],y[1:3])*S-a*y[0] # E
    dy[1]= a*y[0]-(g[1]+p[1])*y[1] #I1
    dy[2]= p[1]*y[1] -(g[2]+p[2])*y[2] #I2
    dy[3]= p[2]*y[2] -(g[3]+u)*y[3] #I3
    dy[4]= np.dot(g[1:3],y[1:3]) #R
    dy[5]=u*y[3] #D

    return dy
```

```python
# Define parameters based on clinical observations

#I will add sources soon
# https://github.com/midas-network/COVID-19/tree/master/parameter_estimates/2019_novel_coronavirus

IncubPeriod=5  #Incubation period, days
DurMildInf=10 #Duration of mild infections, days
FracMild=0.8  #Fraction of infections that are mild
FracSevere=0.15 #Fraction of infections that are severe
FracCritical=0.05 #Fraction of infections that are critical
CFR=0.02 #Case fatality rate (fraction of infections resulting in death)
TimeICUDeath=7 #Time from ICU admission to death, days
DurHosp=11 #Duration of hospitalization, days

```

```python
# Define parameters and run ODE

N=1000
b=np.zeros(4) #beta
g=np.zeros(4) #gamma
p=np.zeros(3)

a=1/IncubPeriod

u=(1/TimeICUDeath)*(CFR/FracCritical)
g[3]=(1/TimeICUDeath)-u

p[2]=(1/DurHosp)*(FracCritical/(FracCritical+FracSevere))
g[2]=(1/DurHosp)-p[2]

g[1]=(1/DurMildInf)*FracMild
p[1]=(1/DurMildInf)-g[1]

#b=2e-4*np.ones(4) # all stages transmit equally
b=2.5e-4*np.array([0,1,0,0]) # hospitalized cases don't transmit

#Calculate basic reproductive ratio
R0=N*((b[1]/(p[1]+g[1]))+(p[1]/(p[1]+g[1]))*(b[2]/(p[2]+g[2])+ (p[2]/(p[2]+g[2]))*(b[3]/(u+g[3]))))
print("R0 = {0:4.1f}".format(R0))
```

    R0 =  2.5

```python
print(b)
print(a)
print(g)
print(p)
print(u)
```

    [0.      0.00025 0.      0.     ]
    0.2
    [0.         0.08       0.06818182 0.08571429]
    [0.         0.02       0.02272727]
    0.057142857142857134

```python
tmax=365
tvec=np.arange(0,tmax,0.1)
ic=np.zeros(6)
ic[0]=1

soln=odeint(seir,ic,tvec,args=(b,a,g,p,u,N))
soln=np.hstack((N-np.sum(soln,axis=1,keepdims=True),soln))

plt.figure(figsize=(13,5))
plt.subplot(1,2,1)
plt.plot(tvec,soln)
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("S","E","I1","I2","I3","R","D"))
plt.ylim([0,1000])

#Same plot but on log scale
plt.subplot(1,2,2)
plt.plot(tvec,soln)
plt.semilogy()
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("S","E","I1","I2","I3","R","D"))
plt.ylim([1,1000])
#plt.tight_layout()
```

```python
# get observed growth rate r (and doubling time) for a particular variable between selected time points
#(all infected classes eventually grow at same rate during early infection)

#Don't have a simple analytic formula for r for this model due to the complexity of the stages

def growth_rate(tvec,soln,t1,t2,i):
  i1=np.where(tvec==t1)[0][0]
  i2=np.where(tvec==t2)[0][0]
  r=(np.log(soln[i2,1])-np.log(soln[i1,1]))/(t2-t1)
  DoublingTime=np.log(2)/r

  return r, DoublingTime
```

```python
(r,DoublingTime)=growth_rate(tvec,soln,10,20,1)
print("The epidemic growth rate is = {0:4.2f} per day and the doubling time {1:4.1f} days ".format(r,DoublingTime))
```

    The epidemic growth rate is = 0.08 per day and the doubling time  9.0 days

### Repeat but with a social distancing measure that reduces transmission rate

```python
bSlow=0.6*b
R0Slow=N*((bSlow[1]/(p[1]+g[1]))+(p[1]/(p[1]+g[1]))*(bSlow[2]/(p[2]+g[2])+ (p[2]/(p[2]+g[2]))*(bSlow[3]/(u+g[3]))))

solnSlow=odeint(seir,ic,tvec,args=(bSlow,a,g,p,u,N))
solnSlow=np.hstack((N-np.sum(solnSlow,axis=1,keepdims=True),solnSlow))

plt.figure(figsize=(13,5))
plt.subplot(1,2,1)
plt.plot(tvec,solnSlow)
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("S","E","I1","I2","I3","R","D"))
plt.ylim([0,1000])

#Same plot but on log scale
plt.subplot(1,2,2)
plt.plot(tvec,solnSlow)
plt.semilogy()
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("S","E","I1","I2","I3","R","D"))
plt.ylim([1,1000])

(rSlow,DoublingTimeSlow)=growth_rate(tvec,solnSlow,30,40,1)

plt.show()
print("R0 under intervention = {0:4.1f}".format(R0Slow))
print("The epidemic growth rate is = {0:4.2f} per day and the doubling time {1:4.1f} days ".format(rSlow,DoublingTimeSlow))
```

#### Compare epidemic growth with and without intervention

```python
### All infectious cases (not exposed)

plt.figure(figsize=(13,5))
plt.subplot(1,2,1)
plt.plot(tvec,np.sum(soln[:,2:5],axis=1,keepdims=True))
plt.plot(tvec,np.sum(solnSlow[:,2:5],axis=1,keepdims=True))
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("No intervention","Intervention"))
plt.ylim([0,1000])
plt.title('All infectious cases')
```

## COVID19 Cases vs Hospital Capacity

Depending on the severity ($I_i$) stage of COVID-19 infection, patients need different level of medical care.

Individuals in $I_1$ have "mild" infection, meaning they have cough/fever/other flu-like symptoms and may also have mild pneumonia. Mild pneumonia does not require hospitalization, although in many outbreak locations like China and South Korea all symptomatic patients are being hospitalized. This is likely to reduce spread and to monitor these patients in case they rapidly progress to worse outcome. However, it is a huge burden on the health care system.

Individuals in $I_2$ have "severe" infection, which is categorized medically as having any of the following: "dyspnea, respiratory frequency 30/min, blood oxygen saturation 93%, partial pressure of arterial oxygen to fraction of inspired oxygen ratio $<$300, lung infiltrates $>$50% within 24 to 48 hours". These individuals require hospitalization but can be treated on regular wards. They may require supplemental oxygen.

Individuals in $I_3$ have "critical" infection, which is categorized as having any of the following: "respiratory failure, septic shock, and/or multiple organ dysfunction or failure".
They require ICU-level care, generally because they need mechanical ventilation.

We consider different scenarios for care requirements. One variation between scenarios is whether we include hospitalization for all individuals or only those with severe or critical infection. Another is the care of critical patients. If ICUs are full, hospitals have protocols developed for pandemic influenza to provide mechanical ventilation outside regular ICU facility and staffing requirements. Compared to "conventional" ventilation protocols, there are "contingency" and "crisis" protocols that can be adopted to increase patient loads. These protocols involve increasing patient:staff ratios, using non-ICU beds, and involving non-critical care specialists in patient care.

```python
#Parameter sources: https://docs.google.com/spreadsheets/d/1zZKKnZ47lqfmUGYDQuWNnzKnh-IDMy15LBaRmrBcjqE

# All values are adjusted for increased occupancy due to flu season

AvailHospBeds=2.6*(1-0.66*1.1) #Available hospital beds per 1000 ppl in US based on total beds and occupancy
AvailICUBeds=0.26*(1-0.68*1.07) #Available ICU beds per 1000 ppl in US, based on total beds and occupancy. Only counts adult not neonatal/pediatric beds
ConvVentCap=0.062 #Estimated excess # of patients who could be ventilated in US (per 1000 ppl) using conventional protocols
ContVentCap=0.15 #Estimated excess # of patients who could be ventilated in US (per 1000 ppl) using contingency protocols
CrisisVentCap=0.42 #Estimated excess # of patients who could be ventilated in US (per 1000 ppl) using crisis protocols

```

### Assumptions 1
* Only severe or critical cases go to the hospital
* All critical cases require ICU care and mechanical ventilation

```python
NumHosp=soln[:,3]+soln[:,4]
NumICU=soln[:,4]

plt.figure(figsize=(13,4.8))
plt.subplot(1,2,1)
plt.plot(tvec,NumHosp)
plt.plot(np.array((0, tmax)),AvailHospBeds*np.ones(2),color='C0',linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing Hospitalization","Available Hospital Beds"))
ipeakHosp=np.argmax(NumHosp) #find peak
peakHosp=10*np.ceil(NumHosp[ipeakHosp]/10)#find time at peak
plt.ylim([0,peakHosp])

plt.subplot(1,2,2)
plt.plot(tvec,NumICU,color='C1')
plt.plot(np.array((0, tmax)),AvailICUBeds*np.ones(2),color='C1',linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing ICU","Available ICU Beds"))
ipeakICU=np.argmax(NumICU) #find peak
peakICU=10*np.ceil(NumICU[ipeakICU]/10)#find time at peak
plt.ylim([0,peakICU])
plt.ylim([0,10])

#Find time when hospitalized cases = capacity
icross=np.argmin(np.abs(NumHosp[0:ipeakHosp]-AvailHospBeds)) #find intersection before peak
TimeFillBeds=tvec[icross]

#Find time when ICU cases = capacity
icross=np.argmin(np.abs(NumICU[0:ipeakICU]-AvailICUBeds)) #find intersection before peak
TimeFillICU=tvec[icross]

plt.show()
print("Hospital and ICU beds are filled by COVID19 patients after {0:4.1f} and {1:4.1f} days".format(TimeFillBeds,TimeFillICU))
```

Note that we have not taken into account the limited capacity in the model itself. If hospitals are at capacity, then the death rate will increase, since individuals with severe and critical infection will often die without medical care. The transmission rate will probably also increase, since any informal home-care for these patients will likely not include the level of isolation/precautions used in a hospital.

#### Allow for mechanical ventilation outside of ICUs using contingency or crisis capacity

```python
plt.plot(tvec,NumICU)
plt.plot(np.array((0, tmax)),ConvVentCap*np.ones(2),linestyle=":")
plt.plot(np.array((0, tmax)),ContVentCap*np.ones(2),linestyle=":")
plt.plot(np.array((0, tmax)),CrisisVentCap*np.ones(2),linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing Mechanical Ventilation","Conventional Capacity","Contingency Capacity","Crisis Capacity"))
plt.ylim([0,peakICU])
plt.ylim([0,10])

#Find time when ICU cases = conventional capacity
icrossConv=np.argmin(np.abs(NumICU[0:ipeakICU]-ConvVentCap)) #find intersection before peak
TimeConvCap=tvec[icrossConv]
icrossCont=np.argmin(np.abs(NumICU[0:ipeakICU]-ContVentCap)) #find intersection before peak
TimeContCap=tvec[icrossCont]
icrossCrisis=np.argmin(np.abs(NumICU[0:ipeakICU]-CrisisVentCap)) #find intersection before peak
TimeCrisisCap=tvec[icrossCrisis]

plt.show()
print("Capacity for mechanical ventilation is filled by COVID19 patients after {0:4.1f} (conventional), {1:4.1f} (contingency) and {2:4.1f} (crisis) days".format(TimeConvCap,TimeContCap,TimeCrisisCap))
```

Compare to the case with intervention

```python
NumHospSlow=solnSlow[:,3]+solnSlow[:,4]
NumICUSlow=solnSlow[:,4]

plt.figure(figsize=(13,4.8))
plt.subplot(1,2,1)
plt.plot(tvec,NumHosp)
plt.plot(tvec,NumHospSlow,color='C0',linestyle="--")
plt.plot(np.array((0, tmax)),AvailHospBeds*np.ones(2),color='C0',linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing Hospitalization","Cases Needing Hospitalization (Intervetion)","Available Hospital Beds"))
plt.ylim([0,peakHosp])

plt.subplot(1,2,2)
plt.plot(tvec,NumICU,color='C1')
plt.plot(tvec,NumICUSlow,color='C1',linestyle="--")
plt.plot(np.array((0, tmax)),AvailICUBeds*np.ones(2),color='C1',linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing ICU","Cases Needing ICU (Intervetion)","Available ICU Beds"))
plt.ylim([0,peakICU])

#Find time when hospitalized cases = capacity
ipeakHospSlow=np.argmax(NumHospSlow) #find peak
icross=np.argmin(np.abs(NumHospSlow[0:ipeakHospSlow]-AvailHospBeds)) #find intersection before peak
TimeFillBedsSlow=tvec[icross]

#Find time when ICU cases = capacity
ipeakICUSlow=np.argmax(NumICUSlow) #find peak
icross=np.argmin(np.abs(NumICUSlow[0:ipeakICU]-AvailICUBeds)) #find intersection before peak
TimeFillICUSlow=tvec[icross]

plt.show()
print("With intervention, hospital and ICU beds are filled by COVID19 patients after {0:4.1f} and {1:4.1f} days".format(TimeFillBedsSlow,TimeFillICUSlow))
```

And for expanded mechanical ventilation capacity

```python
plt.plot(tvec,NumICU)
plt.plot(tvec,NumICUSlow)
plt.plot(np.array((0, tmax)),ConvVentCap*np.ones(2),linestyle=":")
plt.plot(np.array((0, tmax)),ContVentCap*np.ones(2),linestyle=":")
plt.plot(np.array((0, tmax)),CrisisVentCap*np.ones(2),linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing Mechanical Ventilation","Cases Needing Mechanical Ventilation (Intervention)","Conventional Capacity","Contingency Capacity","Crisis Capacity"))
plt.ylim([0,peakICU])

#Find time when ICU cases = conventional capacity (with intervention)
icrossConvSlow=np.argmin(np.abs(NumICUSlow[0:ipeakICUSlow]-ConvVentCap)) #find intersection before peak
TimeConvCapSlow=tvec[icrossConvSlow]
icrossContSlow=np.argmin(np.abs(NumICUSlow[0:ipeakICUSlow]-ContVentCap)) #find intersection before peak
TimeContCapSlow=tvec[icrossContSlow]
icrossCrisisSlow=np.argmin(np.abs(NumICUSlow[0:ipeakICUSlow]-CrisisVentCap)) #find intersection before peak
TimeCrisisCapSlow=tvec[icrossCrisisSlow]

plt.show()
print("Capacity for mechanical ventilation is filled by COVID19 patients after {0:4.1f} (conventional), {1:4.1f} (contingency) and {2:4.1f} (crisis) days".format(TimeConvCapSlow,TimeContCapSlow,TimeCrisisCapSlow))
```

Interpretation: While interventions that reduce infectiousness do "flatten the curve", cases are still WAY over hospital capacity. There is no way to get anywhere close to staying under hospital bed capacity or mechanical ventilation capacity without reducing $R_0<1$.

### Assumptions 2
* All cases go to the hospital
* All critical cases require ICU care and mechanical ventilation

Note: No point running this scenario because it would be even more extreme than Assumption 1 (mild cases stayed home) and Assumption 1 already lead to rapid overlow of hospital resources

### Assumptions 3
* Only severe or critical cases go to the hospital
* All critical cases require ICU care and mechanical ventilation
* When hospital capacity is exceed, individual dies

Note: Could be used to simulate expected increases in death if capacity exceeded

## Alternative Models

To be continued, including
* Assuming ~30% of cases are asymptomatic (as seen on Diamond Princess) (This would lead to a re-interpretation of the reported rates of severe and critical infection, so the prevalence of these stages would decrease)
* A parallel instead of series model of disease course (because it is unclear if it is realistic that individuals who pass through the mild stage on the way to a severe state spend as long in the mild stage as individuals who never progress)
* Including pre-symptomatic transmission (for about last ~2 days of exposed period, as estimated in some studies)