```python
%load_ext autoreload
%autoreload 2
# You can load your python module as this:
import Functions
```

```python
import numpy as np
import sympy as sm
import pandas as pd
from scipy import interpolate
from scipy import optimize
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display
```

```python
from bokeh.io import output_notebook,show, push_notebook
from bokeh.layouts import row, column
from bokeh.models import Label
output_notebook()
```

    Loading BokehJS ...

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

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

**Maximization problem**:
$$
\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*}
$$

First we define basic functions from the equations given in the assignment.

```python
def u(c,rho):
    '''
    Utility_function
    Args:
        c (float)  : Amount consumed
        rho (float): Parameter

    returns:
        Utility of worker gained from comsumption

    '''
    if rho==1:
        return np.log(c)
    else:
        return c**(1-rho)/(1-rho)

def v2(c2,l2,gamma,rho):
    '''
    Total utility in period 2
    Args:
        c2 (float)   : Amount consumed in period 2
        l2 (int)     : Whether the worker works or not, in period 2
                       can either be 0 or 1.
        gamma,rho (float): parameters

    Returns:
        v2 (float)   : Net utility in period 2.

    '''
    v2 = u(c2,rho)-gamma*l2
    return v2

def eq_constraint(c2,w,h2,l2):
    return c2-w*h2*l2

```

Then we define a function that compares the utility from wokring and not working, choosing the highest:

```python
def solve_period_2(h2,b,rho,gamma,output_utility=True):
    '''
    Solves the optimization problem for period two,
    e.g compares utility of the two possible scenarios,
    and chooses the highest utility.

    Args:
        h2 (float)   : Human capital of the worker
        b (float)    : Unemployment benefit if she decides not to work
        rho,gamma (floats) : Parameters
        output_utility(bool): If True returns utility if False returns l2

    Returns:
        Utility (float) : Net utility in period 2
        l2 (int)        : Either 0 or 1, if 0 she decides not to work, if 1 she does work.

    '''

    u_work = v2(w*h2,1,gamma,rho)
    u_nowork = v2(b,0,gamma,rho)

    if u_work > u_nowork:
        utility = u_work
        l2 = 1
    else:
        utility = u_nowork
        l2 = 0

    if output_utility:
        return utility
    else:
        return l2
```

Now we solve the problem for human capital between 0.1 and 1, and plot it:

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

for i,h in enumerate(h_vec):
    v2s[i] = solve_period_2(h,b,rho,gamma)
    labour_supply[i] =  solve_period_2(h,b,rho,gamma,output_utility=False)
```

```python
p1 = Functions.plotting('Human capital in period 2',['Utility'],h_vec,[v2s],
                        'Utility',legendlocation='bottom_right',
                       title = 'Expected utility in period 2 for optimal choice')
p2 = Functions.plotting('Human capital in period 2',['Expected utility from working',
                        'Expected utility from not working'],h_vec,[v2(w*h_vec,1,gamma,rho),
                        [v2(b,0,gamma,rho) for i in range(100)]],'Utility',legendlocation='bottom_right',
                       title= 'Expected utility in period 2 depended on h2')
p3 = Functions.plotting('Human capital in period 2',['Labour supply'],h_vec,[labour_supply],
                        'Labour',legendlocation='bottom_right',
                       title='Labour supply as a function of h2')

show(column(row(p1,p2),p3),notebook_handle=True)
```

<Bokeh Notebook handle for In[8]>

We can se that the intersection point between not working and wokring, is exactly at the point where wage income exceeds unemployment benifits, when only considering utility in period 2.

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

$$
\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*}
$$

First we define basic functions from the equations given in the assignment.

```python
def v1(l1,h1,b,w,rho,beta,gamma,Delta):
    '''
    Calculates the expected utility from period 1

    Args:
        l1 (int)   : Either 0 (for not working) or 1 (for working)
        h1 (float) : Inital amount of human capital
        b (float)  : Unemployment benefits
        w (float)  : Standard wage rate (income is calculated as the products between this and human capital)
        rho,beta,gamma (floats) : Parameters
        Delta (float) : Stochatic increase in human capital (with probability 0.5)

    Returns:
        Utility (float): Expected utility in period 1

    '''

    # utility expected in period 2 is calculated as the mean of the two possible outcomes of increase in human capital:
    h2 = h1 + l1
    u2_good = solve_period_2(h2+Delta,b,rho,gamma)
    u2_bad =solve_period_2(h2,b,rho,gamma)
    expected_u2 = (u2_good+u2_bad)/2

    if l1==1:
        utility = u(w*h1,rho)-gamma+beta*expected_u2
        return utility
    elif l1==0:
        utility = u(b,rho)+beta*expected_u2
        return utility
    else:
        print('l1 must be either 1 or 0')
```

Then we define a function that compares the utility from wokring and not working, choosing the highest:

```python
def solve_period1(h1,b,w,rho,beta,gamma,Delta,output_utility=True):
    '''
    Solves the utility maximization problem in period 1, by comparing the two options
    of the worker, and choosing the highest utility.

    Args:
        Args:
        h1 (float) : Inital amount of human capital
        b (float)  : Unemployment benefits
        w (float)  : Standard wage rate (income is calculated as the products between this and human capital)
        rho,beta,gamma (floats) : Parameters
        Delta (float) : Stochatic increase in human capital (with probability 0.5)
        output_utility(bool): If True returns utility if False returns l1

    Returns:
        Utility (float) : Net utility in period 2
        l1 (int)        : Either 0 or 1, if 0 she decides not to work, if 1 she does work.

    '''

    u1_work = v1(1,h1,b,w,rho,beta,gamma,Delta)
    u1_nowork = v1(0,h1,b,w,rho,beta,gamma,Delta)

    if u1_work > u1_nowork:
        u1 = u1_work
        l1 = 1
    else:
        u1 = u1_nowork
        l1 = 0

    if output_utility:
        return u1
    else:
        return l1
```

Then we plot our results:

```python
v1s = np.empty(100)
labour_supply = np.empty(100)
for i,h in enumerate(h_vec):
    v1s[i] = solve_period1(h,b,w,rho,beta,gamma,Delta)
    labour_supply[i] = solve_period1(h,b,w,rho,beta,gamma,Delta,output_utility=False)
```

```python
p1 = Functions.plotting('Initial human capital',['Expected utility'],
                        h_vec,[v1s],'Expexted utility',legendlocation='bottom_right',
                       title = 'Expected utility in period 1 for optimal choice')
p2 = Functions.plotting('Initial human capital',['Expected utility from working in period 1',
                        'Expected utility from not working in period 1'],h_vec,
                        [[v1(1,h,b,w,rho,beta,gamma,Delta) for h in h_vec],
                         [v1(0,h,b,w,rho,beta,gamma,Delta) for h in h_vec]],
                       'Utility',legendlocation='bottom_right',
                       title = 'Expected utility in period 1')
p3 = Functions.plotting('Human capital in period 1',['Labour supply in period 1'],h_vec,[labour_supply],
                        'Labour',legendlocation='bottom_right',
                       title='Labour supply as a function of h1')

show(column(row(p1,p2),p3),notebook_handle=True)
```

<Bokeh Notebook handle for In[12]>

Now we see that the worker requires less human capital to work that in period two.
We also see that even if the worker decides not to work in period 1, for $h_{1}>0.45$ expected utilty is still rising with $h_{1}$ this is because the worker expects to work (if $h_{1}<0.55$, only in the good outcome) in period 2.

**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.

The answer is yes, intuitively it can be explained like this:
In period 1 there are two payoffs from choosing to work, you have your wage income that you can spend on comsumption, but you also increase your human capital, enabling you to earn more in the following period. Thus if the positive effect on the utility in period 2 (given that you decide to work in period 2) from working in period 1, is greater than the negative utility-difference from working versus not-working in period 1. The worker will decide to work even though she will comsume less in period 1.

We can look at the utility for the two situations:

When deciding not to work:
$$
v1_{l_{1}=0} = \frac{b^{1-\rho}}{1-\rho}  + \beta\mathbb{E}_{1}\left[v_2(h_2)\vert l_{1}=0\right]
$$
When deciding to work:
$$
v1_{l_{1}=1} = \frac{\left(wh_{1}\right)^{1-\rho}}{1-\rho} - \gamma + \beta\mathbb{E}_{1}\left[v_2(h_2)\vert l_{1}=1\right]
$$
Although $b>wh_{1}$, we can still have $v1_{l_{1}=0}>v1_{l_{1}=1}$ if $\mathbb{E}_{1}\left[v_2(h_2)\vert l_{1}=0\right]$ is sufficently larger than $\mathbb{E}_{1}\left[v_2(h_2)\vert l_{1}=1\right]$

```python
def info(h1, b,w,rho,beta,gamma,Delta):
    '''
    Prints our result for given inital values
    so we can print them interactively for different
    amounts of inital human capital.
    '''

    u1_work = v1(1,h1,b,w,rho,beta,gamma,Delta)
    u1_nowork = v1(0,h1,b,w,rho,beta,gamma,Delta)
    wage_income_1 = w*h1
    expected_wage_income_2_work = w*(h1+1+0.05)
    expected_wage_income_2_nowork = w*(h1+0.05)
    print(f'Benefit:                                                                            {b:.2f}')
    print(f'Possible wage income in period 1:                                                   {wage_income_1:.2f}')
    print(f'Expected wage income in period 2, given that the worker works in period 1:          {expected_wage_income_2_work:.2f}')
    print(f'Expected wage income in period 2, given that the worker does not work in period 1:  {expected_wage_income_2_nowork:.2f}')
    print('\n')
    print(f'Expected utility from not working:                                                  {u1_nowork:.4f}')
    print(f'Expected utility from working:                                                      {u1_work:.4f}')

    print('\n')
    print('Conclusion:')
    if b>wage_income_1:
        print('Benefits are higer than possible wage income in period 1')
    elif b==wage_income_1:
        print('Benefits are equal to possible wage income in period 1')
    else:
        print('Possible wage income in period 1 is higher than benefits.')
    if u1_work>u1_nowork:
        print('The expected utility from working in period 1, is higher than from not working,\
        \nthus the consumer decides to work.')
    else:
        print('The expected utility from working in period 1, is lower than from not working,\
        \nthus the consumer decides not to work.')

inter_info = lambda h1 : info(h1, b,w,rho,beta,gamma,Delta)
```

```python
widgets.interactive(inter_info, h1=widgets.FloatSlider(min=0.1,max=1.5,step=0.01,value=0.5))
```

    interactive(children=(FloatSlider(value=0.5, description='h1', max=1.5, min=0.1, step=0.01), Output()), _dom_c…

At $h_{1}=0.5$ benefits equals possible wage income in period 1. But for the worker to decide not to work in period one, requires: $h_{1}<0.36$

```python
p1 = Functions.plotting('Human capital in period 1',['Benefits', 'Possible wage income'],
                        h_vec,[[b for h in h_vec],[h*w for h in h_vec]],'Income',
                       title = 'Comparing benefits with wage income',
                       legendlocation='top_left', width=700)
p1.line(x=[h_vec[19] for i in range(100)],y=np.linspace(0.2,3,100),color='black',
         legend= 'Intersectionpoint between utility from working and from not working')
show(p1,notebook_handle=True)
```

<Bokeh Notebook handle for In[15]>

Before the black vertical line the total expected utility from not working in period 1 is higher than from working, after the black vertical total expected utility is higher for working.

Here the wage income in period 1 is lower than the benefit, but the expected total utility is still higher for working.

# 2. AS-AD model

```python
par = {}

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

**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}$.

**AD-curve** is

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

**SRAS-curve** is

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

```python
sm.init_printing(use_unicode=True)
```

```python
pi_t = sm.symbols('\pi_{t}')
h = sm.symbols('h')
alpha = sm.symbols('\\alpha')
v_t = sm.symbols('v_{t}')
b = sm.symbols('b')
y_t = sm.symbols('y_{t}')
y_t1 = sm.symbols('y_{t-1}')
pi_t1 = sm.symbols('\pi_{t-1}')
gamma = sm.symbols('\gamma')
phi = sm.symbols('phi')
s_t = sm.symbols('s_t')
s_t1 = sm.symbols('s_{t-1}')
```

```python
ad = sm.Eq(pi_t,1/(h*alpha)*(v_t-(1+b*alpha)*y_t))
ad
```

```python
sras =sm.Eq(pi_t,pi_t1+gamma*y_t-phi*gamma*y_t1+s_t-phi*s_t1)
sras
```

We set the two equations equal to one another:

```python
pi_from_ad = sm.solve(ad,pi_t)[0]
equilibrium_y = sras.subs(pi_t,pi_from_ad)
equilibrium_y
```

And solve for $y$ and $\pi$ :

```python
y_eq = sm.solve(equilibrium_y,y_t)[0]
y_equation = sm.Eq(y_t,y_eq)
# define as python function:
_y_equilibrium = sm.lambdify((y_t1,pi_t1,v_t,s_t,s_t1,alpha,gamma,h,b,phi),y_eq)

# print our solution symbolicaly
y_equation
```

```python
equilibrium_pi_equation = ad.subs(y_t,y_eq)
pi_equation = sm.Eq(pi_t,sm.solve(equilibrium_pi_equation,pi_t)[0])
# define as python function:
_pi_equilibrium = sm.lambdify((y_t1,pi_t1,v_t,s_t,s_t1,alpha,gamma,h,b,phi),sm.solve(equilibrium_pi_equation,pi_t)[0])
# print our solution symbolicaly
pi_equation
```

If we insert phi=0 we get:

```python
y_equation.subs(phi,0)
```

```python
pi_equation.subs(phi,0)
```

**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$.

```python
vars_0 = {}
for var in [y_t1,pi_t1,v_t,s_t,s_t1]:
    vars_0[var]=0

y_equation.subs(vars_0)
```

```python
pi_equation.subs(vars_0)
```

Thus, when there are no shocks and no earlier output or inflation gab, there will also be no gabs in the following period

For $v_{t}=0.1$:

```python
vars_0[v_t]=0.1
```

```python
y_v1 = y_equation.subs(vars_0)
y_v1
```

```python
pi_v1 = pi_equation.subs(vars_0)
pi_v1
```

Both $y_{t}$ and $\pi_{t}$ is positvely afffected by demand_chocks in the current period.

Then we substitute the parameter values, to see the outcome given our model and parameter values:

```python
paras = {}
for var, value in zip([alpha,gamma,h,b],[par['alpha'],par['gamma'],par['h'],par['b']]):
    paras[var]=value
y_v1.subs(paras)
```

```python
pi_v1.subs(paras)
```

```python
sm.init_printing(use_unicode=False)
```

```python
# We redifine our lambdifyed funtion to accept the par-dictionairy, and check that they give the same result:
def y_equilibrium(y_t1,pi_t1,v_t,s_t,s_t1,par):
    '''
    Calculates the equilibrium outputgab using the analytical solution
    derived in sympy.

    Args:
        y_t1 (float)   : The outputgab in the previous period
        pi_t1 (float)  : The inflationgab in the previous period
        v_t (float)    : The demand chock in the current period
        s_t (float)    : The supply chock in the current period
        s_t1 (float)   : The supply chock in the previous period
        par (dict)     : Dictionairy contaning values of parameters.

    Returns:
        y_equil(float) : Equilibrium outputgab
    '''

    y_equil = _y_equilibrium(y_t1,pi_t1,v_t,s_t,s_t1,par['alpha'],par['gamma'],par['h'],par['b'],par['phi'])
    return y_equil

assert y_equilibrium(0,0,0,0,0,par)==0
y_equilibrium(0,0,0.1,0,0,par)
```

```python
def pi_equilibrium(y_t1,pi_t1,v_t,s_t,s_t1,par):
    '''
    Calculates the equilibrium inflation gab using the analytical solution
    derived in sympy.

    Args:
        y_t1 (float)   : The outputgab in the previous period
        pi_t1 (float)  : The inflationgab in the previous period
        v_t (float)    : The demand chock in the current period
        s_t (float)    : The supply chock in the current period
        s_t1 (float)   : The supply chock in the previous period
        par (dict)     : Dictionairy contaning values of parameters.

    Returns:
        pi_equil(float) : Equilibrium inflation gab
    '''

    pi_equil = _pi_equilibrium(y_t1,pi_t1,v_t,s_t,s_t1,par['alpha'],par['gamma'],par['h'],par['b'],par['phi'])
    return pi_equil

assert pi_equilibrium(0,0,0,0,0,par)==0
pi_equilibrium(0,0,0.1,0,0,par)
```

Now we wish to visualize this, we start by plotting the AD and SRAS, with $y_{t-1} = \pi_{t-1} = v_t = s_t = s_{t-1} = 0$:

```python
def ad(y_t,v_t,par):
    '''
    Aggregate demand

    Args:
        y_t (float)   : Outputgab in current period
        v_t (float)   : Demand chock in current period
        par (dict)    : Dictionairy contaning values of parameters
    Returns
        ad (float)    : Aggregate demand

    '''
    h = par['h']
    alpha =par['alpha']
    b = par['b']

    ad = 1/(h*alpha)*(v_t-(1+b*alpha)*y_t)
    return ad

def sras(y_t, y_t1,pi_t1,s_t,s_t1,par):
    '''
    Short run aggregate supply

    Args:
        y_t (float)   : Outputgab in current period
        y_t1 (float)  : The outputgab in the previous period
        pi_t1 (float) : The inflationgab in the previous period
        s_t (float)   : Supply chock in current period
        s_t1 (float)  : Supply chock in previous period
        par (dict)    : Dictionairy contaning values of parameters
    Returns
        sras (float)  : Short run aggregate supply
    '''

    phi = par['phi']
    gamma = par['gamma']
    sras = pi_t1+gamma*y_t-phi*gamma*y_t1+s_t-phi*s_t1
    return sras
```

```python
# we find equilibrium path
t = 41
pis = [0,pi_equilibrium(0,0,0.1,0,0,par)]
ys = [0,y_equilibrium(0,0,0.1,0,0,par)]
for i in range(1,t):
    ys.append(y_equilibrium(ys[i],pis[i],0,0,0,par))
    pis.append(pi_equilibrium(ys[i],pis[i],0,0,0,par))
```

```python
y_gabs = np.linspace(-0.005,0.03,100)
y_gabs = np.append(y_gabs,0)
```

```python
# We find the ad-and sras curves:
# period 0:
ads = [ad(y_gabs,0,par)]
srass = [sras(y_gabs,0,0,0,0,par)]
# append period 1:
ads.append(ad(y_gabs,0.1,par))
srass.append(sras(y_gabs,0,0,0,0,par))
#append period 2-40:
for i in range(2,t):
    ads.append(ad(y_gabs,0,par))
    srass.append(sras(y_gabs,ys[i],pis[i],0,0,par))

```

```python
# Then we plot our results for period 0 and 1, and the equilibrium path:

p1 = Functions.plotting('Output gab',['AD','SRAS'],y_gabs[:-70],[ads[0][:-70],srass[0][:-70]],
                        'Inflation gab',legendlocation='top_right',title='AS-AD-model')

p2 = Functions.plotting('Output gab',['AD, v=0, period 0','SRAS','AD, v=0.1, period 1'],
                        y_gabs,[ads[0],srass[0],ads[1]],'Inflation gab',
                       legendlocation='top_right',title='AS-AD with demand chock')
p1.circle(x=[0],y=[0],legend='Equilibrium',line_width=3,color='red')
p2.circle(x=[0],y=[0],legend='Equilibrium in period 0',line_width=3,color='red')
p2.circle(x=[y_equilibrium(0,0,0.1,0,0,par)],y=[pi_equilibrium(0,0,0.1,0,0,par)],
          legend='Equilibrium in period 1',line_width=3,color='green')
p2.circle(x=ys,y=pis,legend='Path of Equilibrium',line_width=1,color='black')

show(row(p1,p2),notebook_handle=True)
```

<Bokeh Notebook handle for In[2]>

```python
# Then we plot everything interactively:
Functions.plotting_asad('Output gab',y_gabs,[srass,ads],'Inflation gab',
                    legendlocation='bottom_right', width=500, height=500,
                    colors = ['blue',(226, 50, 86),'purple','green'],
                    equilibrium_path=[ys,pis],keep_ad1 = True,line_width=3,
                   y_range=[-0.02,0.02],title='AS-AD with demand chock')
```

    interactive(children=(IntSlider(value=0, description='period', max=39), Output()), _dom_classes=('widget-inter…

We see that inflation and output rises from the positive demand chock of $v_{1}=0.1$ in period 1, but in the following periods the inflationgab is still positive, but the outputgab becomes negative (boom-bust), afterwards we converge to the long term equilibrium.

**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$?

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

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

We define these new equations:

```python
def demand_persistence(v_t1,x_t,par):
    '''
    Args:
        v_t1 (float)  : Demand chock in previous period
        x_t (float)   : Added demand chock in current period

    Returns:
        v_t (float)   : Demand chock in current period
    '''
    v_t = par['delta']*v_t1+x_t
    return v_t
def supply_persistence(s_t1,c_t,par):
    '''
    Args:
        s_t1 (float)  : Supply chock in previous period
        c_t (float)   : Added supply chock in current period

    Returns:
        s_t (float)   : Supply chock in current period
    '''
    s_t = par['omega']*s_t1+c_t
    return s_t
```

Period 0:

We assume that $v_{-1}$ is also equal to 0, and we can calculate, with $x_{0}$=0.1:

```python
v0 = demand_persistence(0,0.1,par)
y0 = y_equilibrium(0,0,v0,0,0,par)
pi0 = pi_equilibrium(0,0,v0,0,0,par)
print(f'Outputgab in period 0 is    {y0:.4f}')
print(f'Inflationgab in period 0 is {pi0:.4f}')
```

    Outputgab in period 0 is    0.0244
    Inflationgab in period 0 is 0.0018

```python
v1 = demand_persistence(v0,0,par)
y1 = y_equilibrium(y0,pi0,v1,0,0,par)
pi1 = pi_equilibrium(y0,pi0,v1,0,0,par)
print(f'Outputgab in period 1 is    {y1:.4f}')
print(f'Inflationgab in period 1 is {pi1:.4f}')
```

    Outputgab in period 1 is    0.0182
    Inflationgab in period 1 is 0.0032

So initially $\pi$ and $y$ is positive, but we need to look at more periods to get the full picture:

Lets look at the development for 70 periods, first we find the equilibriums and plot them, then we look at how aggregate demand and supply:

```python
T = 70
vs = [0,v0]
ys = [0,y0]
pis= [0,pi0]

for i in range(1,T):
    vs.append(demand_persistence(vs[i],0,par))
    ys.append(y_equilibrium(ys[i],pis[i],vs[i+1],0,0,par))
    pis.append(pi_equilibrium(ys[i],pis[i],vs[i+1],0,0,par))

zeros = [0 for i in range(-1,T)]
```

```python
p1 = Functions.plotting('T',['Outputgab','Inflationgab',''],range(-1,T),
                        [[y*100 for y in ys],[pi*100 for pi in pis],zeros],
                        'Percentage gab from long run level',colors=['red','blue','black'],y_unit='%',
                       title='Progression of eqiulibrium after demand chock')
p2 = Functions.plotting('T',['Demand chock',''],range(-1,T),[vs,zeros],'',
                       colors = ['blue','black'],title='Autoregressive demand chock')
show(row(p1,p2),notebook_handle=True)
```

<Bokeh Notebook handle for In[6]>

Now we plot the progression for the demand and supply curves

```python
y_gabs = np.linspace(-0.02,0.05,100)
```

```python
ad_curves = [ad(y_gabs,v,par) for v in vs]
sras_curves = [sras(y_gabs,0,0,0,0,par)]
for i in range(T+1):
    sras_curves.append(sras(y_gabs,ys[i],pis[i],0,0,par))
```

```python
curve_names = ['SRAS, t=0,1','AD, t=0','AD, t=1','SRAS, t=2','AD, t=2',
               'SRAS, t=5','AD, t=5','SRAS, t=10','AD, t=10']
curves = [sras_curves[0],ad_curves[0],ad_curves[1],sras_curves[2],
          ad_curves[2],sras_curves[5],ad_curves[5],sras_curves[10],
         ad_curves[10]]

colors = [(0,0,153),(102,0,51),(153,0,76),(0,0,255),(204,0,102),(102,102,51),
         (255,102,178),(102,132,51),(255, 132, 208)]

p1 = Functions.plotting('Output gab',curve_names,y_gabs,curves,'Inflation gab',
                        legendlocation='top_right',width=600, height=600,
                        colors = colors, title ='AS-AD after demand chock')

p1.circle(x=ys,y=pis,legend='Path of equilibrium',color=(66, 244, 134))
p1.circle(x=ys[0],y=pis[0],line_width=3,color=(102,0,51))
p1.circle(x=ys[1],y=pis[1],line_width=3,color=(153,0,76))
p1.circle(x=ys[2],y=pis[2],line_width=3,color=(204,0,102))
p1.circle(x=ys[5],y=pis[5],line_width=3,color=(255,102,178))
p1.circle(x=ys[10],y=pis[10],line_width=3,color=(255,132,178))

show(p1,notebook_handle=True)
```

<Bokeh Notebook handle for In[6]>

```python
Functions.plotting_asad('Output gab',y_gabs,[sras_curves,ad_curves],
                    'Inflation gab', legendlocation='top_right',
                    width=600, height=600,line_width=2,
                    colors = ['blue',(226, 50, 86),(65, 134, 244),'green'], equilibrium_path=[ys,pis],
                   keep_ad1=True,title='AS-AD after demand chock')
```

    interactive(children=(IntSlider(value=0, description='period', max=69), Output()), _dom_classes=('widget-inter…

We can see that the demand-curve jumps out in period 0, and slowly contract back to LR-AD, e.g. we can see the persistence of the initial demand chock. The SRAS on the other hand expands slowly, until around period 8, where it cotracts slowly towards SRAS

**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})$

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

Since the chocks in the model are stochastic and independent of the movements of the model (equilibrium etc.), we can compute and plot them before simulating the the model:

```python
np.random.seed(1986)
xs = np.random.normal(0,par['sigma_x'],t)
cs = np.random.normal(0,par['sigma_c'],t)
```

```python
vs = [demand_persistence(0,xs[0],par)]
ss = [supply_persistence(0,cs[0],par)]
for i in range(1,t):
    vs.append(demand_persistence(vs[i-1],xs[i],par))
    ss.append(supply_persistence(ss[i-1],cs[i],par))
```

```python
p = Functions.plotting('T',['Demand chock','Supply chock'],range(t),[vs,ss],
                       'Chock',legendlocation='bottom_left',width=700,
                      title='Our simulated chocks',colors = ['blue','red'],line_width=1)
show(p,notebook_handle=True)
```

<Bokeh Notebook handle for In[6]>

We can clearly see the higher standard deviation of the demand chocks.

Now we are ready to simulate the model:

```python
def sim_asad(par,t=1000):
    '''
    Simulates the AS-AD model, with persistence stochastic chocks

    Args:
        par (dict)  : Dictionairy of parameter values
        t (int)     : Amount of periods simulated

    Returns:
        correlations (list): containing:
                                    Variance of outputgab
                                    Variance of inflationgab
                                    Correlations between output and inflation
                                    Autocorrelation of output 1 period back
                                    Autocorrelation of inflation 1 period back

        ys (list)   : The simulated outputgabs
        pis (list)  : The simulated inflation gbs

    '''

    np.random.seed(1986)
    xs = np.random.normal(0,par['sigma_x'],t)
    cs = np.random.normal(0,par['sigma_c'],t)

    vs = [demand_persistence(0,xs[0],par)]
    ss = [supply_persistence(0,cs[0],par)]

    ys = [y_equilibrium(0,0,vs[0],ss[0],0,par)]
    pis = [pi_equilibrium(0,0,vs[0],ss[0],0,par)]

    for i in range(1,t):
        vs.append(demand_persistence(vs[i-1],xs[i],par))
        ss.append(supply_persistence(ss[i-1],cs[i],par))
        ys.append(y_equilibrium(ys[i-1],pis[i-1],vs[i],ss[i],ss[i-1],par))
        pis.append(pi_equilibrium(ys[i-1],pis[i-1],vs[i],ss[i],ss[i-1],par))

    correlations = [np.var(ys),np.var(pis),np.corrcoef(ys,pis)[0,1],
             np.corrcoef(ys[:-1],ys[1:])[0,1],
             np.corrcoef(pis[:-1],pis[1:])[0,1]]
    return correlations,ys,pis

```

```python
correlations,ys,pis = sim_asad(par)
```

```python
p = Functions.plotting('T',['Inflation gab','Output gab'],range(t),[pis,ys],
                       'Percentage deviation',legendlocation='bottom_left',width=900,
                      title='Simulation of AS-AD-model',line_width=1)
show(p,notebook_handle=True)
```

<Bokeh Notebook handle for In[6]>

```python
data = [correlations]
df = pd.DataFrame(data, columns = [r'Var(\(y\))',r'Var(\(\pi\))',
                r'Corr(\(\pi,y\))',r'Corr(\(y_{t},y_{t-1}\))',r'Corr (\(\pi_{t},\pi_{t-1}\))    .'],index=['Value'])
df
```

      Var(\(y\))
      Var(\(\pi\))
      Corr(\(\pi,y\))
      Corr(\(y_{t},y_{t-1}\))
      Corr (\(\pi_{t},\pi_{t-1}\))    .

      Value
      1.995473
      1.794619
      -0.152624
      0.780791
      0.986923

We see a higher variance and loweer autocorrelation for outputgab than for the inflationgab, this could be due to the model having a relatively flat SRAS-curve causing our stronger (relative to supply-) demand chocks to mainly influence output. Furthermore the strong autocorrelation of $\pi$ could also come as a result of the parameter $\phi$ being set to zero, meaning that we assumed static inflation expectations.

**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.

```python
# Before this we save our original parametervalues
org_par = [par['phi'],par['sigma_x'],par['sigma_c']]
```

```python
def corr_y_pi(par,t=t):
    ys = [y_equilibrium(0,0,vs[0],ss[0],0,par)]

    pis = [pi_equilibrium(0,0,vs[0],ss[0],0,par)]

    for i in range(1,t):
        ys.append(y_equilibrium(ys[i-1],pis[i-1],vs[i],ss[i],ss[i-1],par))
        pis.append(pi_equilibrium(ys[i-1],pis[i-1],vs[i],ss[i],ss[i-1],par))
    return np.corrcoef(ys,pis)[0,1]
```

```python
corrs =[]
phis = np.linspace(0,1,500)
for phi in phis:
    par['phi']=phi
    corrs.append(corr_y_pi(par))
```

```python
p = Functions.plotting('\u03C6',['Correlation between inflation- and outputgab'],phis,[corrs],
                       'Correlation Coefficient',legendlocation='top_left',width=900,
                      title='Correlation between inflation- and outputgab depended on \u03C6',line_width=1)
show(p,notebook_handle=True)
```

<Bokeh Notebook handle for In[6]>

Now to find the $\phi$ that makes corr($\pi,y$)=0.31:

```python
'''
An another way to find the right phi is to use an interpolater
but since the problem  explicityly says to use either an optimizeer
or a root_finder, we will also solve the problem using a rootfinder
'''
corr_interp = interpolate.interp1d(corrs,phis,kind='cubic')
phi_interperted = corr_interp([0.31])[0]
```

```python
def obj1(phi):
    par['phi']=phi
    return corr_y_pi(par)-0.31
res = optimize.root_scalar(obj1,method='bisect',bracket=[0,1])
phi_optimized = res.root
```

```python
assert np.isclose(phi_interperted,phi_optimized)
```

```python
print('The phi that causes the correlation between the output- and inflationgab to be equal to 0.31 is:')
print(f'Phi = {phi_optimized}')
```

    The phi that causes the correlation between the output- and inflationgab to be equal to 0.31 is:
    Phi = 0.9640651869867725

**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$

```python
def obj2(paras,par=par,t=t):
    par['phi']=paras[0]
    par['sigma_x']=paras[1]
    par['sigma_c']=paras[2]

    corrs, ys, pis = sim_asad(par)
    roots = np.array(corrs) -np.array([1.64,0.21,0.31,0.84,0.48])
    return roots
```

```python
res = optimize.root(obj2,[0.96,par['sigma_x'],par['sigma_c']],method='lm')
diff = res.fun
print(f'As close to zero as we can get with this optimizer: \n{diff}')
print(f'The values found was: \nPhi = {res.x[0]:.4f}, Sigma_x = {res.x[1]:.4f}, Sigma_c = {res.x[2]:.4f}')
```

    As close to zero as we can get with this optimizer:
    [ 0.00524204 -0.15399737 -0.02923761 -0.02311971  0.00360194]
    The values found was:
    Phi = 0.9679, Sigma_x = 2.9329, Sigma_c = 0.1880

```python
par['phi']=res.x[0]
par['sigma_x']=res.x[1]
par['sigma_c']=res.x[2]
corrs, ys, pis = sim_asad(par)
```

```python
for i in range(3):
    correlations.append(org_par[i])
    corrs.append(res.x[i])

data = [correlations,corrs,[1.64,0.21,0.31,0.84,0.48],list(diff),]
df = pd.DataFrame(data, columns = [r'Var(\(y\))',r'Var(\(\pi\))',
                r'Corr(\(\pi,y\))',r'Corr(\(y_{t},y_{t-1}\))',r'Corr (\(\pi_{t},\pi_{t-1}\))    .',
                r'\(\phi\)',r'\(\sigma_{x}\)',r'\(\sigma_{c}\)'],
                  index=['Original values','Imitated values','US-values','Difference'])

pd.set_option('display.max_colwidth', -1)
df
```

      Var(\(y\))
      Var(\(\pi\))
      Corr(\(\pi,y\))
      Corr(\(y_{t},y_{t-1}\))
      Corr (\(\pi_{t},\pi_{t-1}\))    .
      \(\phi\)
      \(\sigma_{x}\)
      \(\sigma_{c}\)

      Original values
      1.995473
      1.794619
      -0.152624
      0.780791
      0.986923
      0.000000
      3.492000
      0.200000

      Imitated values
      1.645242
      0.056003
      0.280762
      0.816880
      0.483602
      0.967858
      2.932851
      0.187962

      US-values
      1.640000
      0.210000
      0.310000
      0.840000
      0.480000
      NaN
      NaN
      NaN

      Difference
      0.005242
      -0.153997
      -0.029238
      -0.023120
      0.003602
      NaN
      NaN
      NaN

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

```python
%matplotlib inline
from mpl_toolkits import mplot3d
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.patches as mpatches
```

```python
plt.style.use('seaborn-pastel')
```

```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
e1s = np.random.exponential(zeta,size=N)
e2s = np.random.exponential(zeta,size=N)
e3s = np.random.exponential(zeta,size=N)
```

```python
#preparing data for histogram
hist1, edges1 = np.histogram(betas[:,0], bins=50)
hist2, edges2 = np.histogram(betas[:,1], bins=50)
hist3, edges3 = np.histogram(betas[:,2], bins=50)
colors = ['blue','green','red']
names =['Good 1','Good 2','Good 3']

#plotting
p = Functions.plot_hist([hist1,hist2,hist3], [edges1,edges2,edges3],
              line_colors= colors, fill_colors = colors,
             alpha = 0.5,plot_range=[0,1],names=names,x_label='\u03B2',
            y_label='Observations',title='Distribution of budgetshare')
show(p,notebook_handle=True)
```

<Bokeh Notebook handle for In[6]>

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

```python
def income(p1,p2,p3,e1,e2,e3):
    I = p1*e1+p2*e2+p3*e3
    return I

def demand_func(p1,p2,p3,e1,e2,e3,beta,good):
    '''
    Calculates the demand of a given good for a comsumer
    using the analytical solution

    Args:
        p1,p2,p3 (floats): Prices the consumer is faced with
        e1,e2,e3 (floats): Endowments of the consumer
        beta (float)     : Share of budget that the consumer wishes to spend on the good
        good (int)       : Either 1,2 or 3, indicating which good we are looking at

    Returns:
        demand (float)   : Amount of the good that the consumer wishes to consume given constraints

    '''
    I = income(p1,p2,p3,e1,e2,e3)
    if good==1:
        demand = beta*I/p1
    elif good==2:
        demand = beta*I/p2
    elif good==3:
        demand = beta*I/p3
    else:
        print('Error, we only have three goods')

    return demand

def excess_demand_good_func(p1,p2,p3,e1s,e2s,e3s,betas,good):
    '''
    Calculates the total excess demand in the market for a given good.

    Args:
        p1,p2,p3 (floats)     : Prices the consumers are faced with
        e1s,e2s,e3s (arrays)  : Endowments of all consumers for each good
        betas (array)         : Shares of budget that the consumers wishes to spend on each good
        good (int)            : Either 1,2 or 3, indicating which good we are looking at

    Returns:
        Excees_demand (float) : The excess demand in the market for the good given the prices.

    '''

    beta = betas[:,good-1]

    demand = np.sum(demand_func(p1,p2,p3,e1s,e2s,e3s,beta,good=good))

    if good==1:
        supply = np.sum(e1s)
    elif good==2:
        supply = np.sum(e2s)
    else:
        supply = np.sum(e3s)

    excess_demand = demand-supply

    return excess_demand
```

```python
precision  = 60
# Setting p3 numeria
p3 = 1
p1_space = np.linspace(2,7,precision)
p2_space = np.linspace(2,7,precision)
p1s,p2s = np.meshgrid(p1_space,p2_space,indexing='ij')
excess_demands = [np.empty((precision,precision)),np.empty((precision,precision)),np.empty((precision,precision))]
```

```python
for k in range(3):
    for i in range(precision):
        for j in range(precision):
            excess_demands[k][i,j]=excess_demand_good_func(p1s[i,j],p2s[i,j],p3,e1s,e2s,e3s,betas,good=(k+1))
```

```python
%matplotlib widget
fig = plt.figure(figsize = (15,5))
ax = fig.add_subplot(1,1,1,projection='3d')
ax.plot_surface(p1s,p2s,excess_demands[0],cmap=cm.jet)
ax.set_xlabel('p1')
ax.set_ylabel('p2')
ax.set_zlabel('Excess demand of good 1')
```

    FigureCanvasNbAgg()

    Text(0.5, 0, 'Excess demand of good 1')

```python
fig = plt.figure(figsize = (15,5))
ax = fig.add_subplot(1,1,1,projection='3d')
ax.plot_surface(p1s,p2s,excess_demands[1],alpha=1,cmap=cm.jet)
ax.set_xlabel('p1')
ax.set_ylabel('p2')
ax.set_zlabel('Excess demand of good 2')
ax.invert_yaxis()
```

    FigureCanvasNbAgg()

```python
fig = plt.figure(figsize = (15,5))
ax = fig.add_subplot(1,1,1,projection='3d')

ax.plot_surface(p1s,p2s,excess_demands[2],alpha=1,cmap=cm.jet)
ax.set_xlabel('p1')
ax.set_ylabel('p2')
ax.set_zlabel('Excess demand of good 3')
```

    FigureCanvasNbAgg()

    Text(0.5, 0, 'Excess demand of good 3')

```python
%matplotlib inline
```

**Question 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.

```python
def find_equilibrium(p1, p2, p3, e1s, e2s, e3s, betas, kappa=0.5, eps=0.1, maxiter=5000):
    '''
    Finds the Walras eqilibrium prices, which clears the markets for good 1 and 2,
    implictly also clearing the market for good 3

    Args:
        p1,p2,p3 (floats)     : Prices the consumers are faced with
        e1s,e2s,e3s (arrays)  : Endowments of all consumers for each good
        betas (array)         : Shares of budget that the consumers wishes to spend on each good
        kappa (float)         : Aggressivity parameter
        epsilon (float)       : Tolerance
        maxiter (int)         : Maximum number of iterations to calculate

    Returns:
        p1,p2 (floats)        : Walras eqiulibrium prices

    '''

    t = 0
    while True:
        Z1 = excess_demand_good_func(p1,p2,p3,e1s,e2s,e3s, betas, 1)
        Z2 = excess_demand_good_func(p1,p2,p3,e1s,e2s,e3s, betas, 2)

        if (np.abs(Z1) < eps and np.abs(Z2)<eps) or t >= maxiter:
            print(f'Step {t:3d}:')
            print(f'p1 = {p1:12.8f} -> excess demand 1 -> {Z1:14.8f}')
            print(f'p2 = {p2:12.8f} -> excess demand 2 -> {Z2:14.8f}')
            Z3 = excess_demand_good_func(p1,p2,p3,e1s,e2s,e3s, betas, 3)
            print(f'Also --------------> excess demand 3 -> {Z3:14.8f}')
            break

        p1 = p1 + kappa*Z1/N
        p2 = p2 + kappa*Z2/N
        if t < 5 or t%2500 == 0:
            print(f'Step {t:3d}:')
            print(f'p1 = {p1:12.8f} -> excess demand 1-> {Z1:14.8f}')
            print(f'p2 = {p2:12.8f} -> excess demand 2-> {Z2:14.8f}')

        elif t == 5:
            print("  ...")
        t += 1
    return p1, p2

```

```python
kappa = 0.1
eps = 0.000001
maxiter = 50000
p3 = 1
p2 =1
p1 = 1
p1,p2 = find_equilibrium(p1, p2, p3, e1s, e2s, e3s,betas, kappa=kappa, eps=eps, maxiter=maxiter)
```

    Step   0:
    p1 =   1.09260612 -> excess demand 1-> 46303.06210182
    p2 =   0.97796409 -> excess demand 2-> -11017.95327569
    Step   1:
    p1 =   1.17305159 -> excess demand 1-> 40222.73067623
    p2 =   0.95953050 -> excess demand 2-> -9216.79648110
    Step   2:
    p1 =   1.24453025 -> excess demand 1-> 35739.33136121
    p2 =   0.94431730 -> excess demand 2-> -7606.59981301
    Step   3:
    p1 =   1.30907401 -> excess demand 1-> 32271.87934840
    p2 =   0.93199522 -> excess demand 2-> -6161.04112688
    Step   4:
    p1 =   1.36807540 -> excess demand 1-> 29500.69778865
    p2 =   0.92226760 -> excess demand 2-> -4863.80959762
      ...
    Step 2500:
    p1 =   6.46564122 -> excess demand 1->    22.41209085
    p2 =   2.60758603 -> excess demand 2->     8.36130845
    Step 5000:
    p1 =   6.48980287 -> excess demand 1->     0.23073526
    p2 =   2.61659989 -> excess demand 2->     0.08607759
    Step 7500:
    p1 =   6.49005253 -> excess demand 1->     0.00239291
    p2 =   2.61669302 -> excess demand 2->     0.00089269
    Step 10000:
    p1 =   6.49005512 -> excess demand 1->     0.00002482
    p2 =   2.61669399 -> excess demand 2->     0.00000926
    Step 11758:
    p1 =   6.49005514 -> excess demand 1 ->     0.00000100
    p2 =   2.61669400 -> excess demand 2 ->     0.00000037
    Also --------------> excess demand 3 ->    -0.00000746

Now we wish to plot our optimal prices with the excess demands for all goods

```python
precision  = 60
p3 = 1
p1_space = np.linspace(6,7,precision)
p2_space = np.linspace(2,3,precision)
p1s,p2s = np.meshgrid(p1_space,p2_space,indexing='ij')
excess_demands = [np.empty((precision,precision)),np.empty((precision,precision)),np.empty((precision,precision))]
```

```python
for k in range(3):
    for i in range(precision):
        for j in range(precision):
            excess_demands[k][i,j]=excess_demand_good_func(p1s[i,j],p2s[i,j],p3,e1s,e2s,e3s,betas,good=(k+1))
```

```python
%matplotlib widget
fig = plt.figure(figsize = (15,5))
ax = fig.add_subplot(1,1,1,projection='3d')
ax.plot_surface(p1s,p2s,excess_demands[0],alpha=0.8)
ax.plot_surface(p1s,p2s,excess_demands[1],alpha=0.8)
ax.plot_surface(p1s,p2s,excess_demands[2],alpha=0.8,cmap='Purples')
p1_vec= [p1 for i in range(20000)]
p2_vec = [p2 for i in range(20000)]
z = np.linspace(-2500,12000,20000)
ax.plot(p1_vec,p2_vec,z,color='red',linewidth=3)
ax.set_xlabel('p1')
ax.set_ylabel('p2')
ax.set_zlabel('Excess demand')
ax.invert_xaxis()

labels = [mpatches.Patch(color='blue',label='Good 1')]
labels.append(mpatches.Patch(color='green',label='Good 2'))
labels.append(mpatches.Patch(color='purple',label='Good 3'))
labels.append(mpatches.Patch(color='red',label='Walras equilibrium prices'))

ax.legend(handles=labels,loc='upper left',prop={'size':10})

#Set initial view:
ax.view_init(elev=10, azim=-30)
```

    FigureCanvasNbAgg()

```python
%matplotlib inline
```

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

```python
x1s = demand_func(p1,p2,p3,e1s,e2s,e3s,betas[:,0],1)
x2s = demand_func(p1,p2,p3,e1s,e2s,e3s,betas[:,1],2)
x3s = demand_func(p1,p2,p3,e1s,e2s,e3s,betas[:,2],3)

betas[:,1]

def utility(x1,x2,x3,beta1,beta2,beta3, gamma):
    utility = (x1**beta1*x2**beta2*x3**beta3)**gamma

    return utility

utilitys = []
for i in range(N):
    utilitys.append(utility(x1s[i],x2s[i],x3s[i],betas[i,0],betas[i,1],betas[i,2], gamma))
```

```python
hist, edges = np.histogram(utilitys, bins=100)
plot1 = Functions.plot_hist([hist], [edges],names= [''],plot_range=[0,4],
                       y_label='Observations',x_label='Utility')
mean = np.mean(utilitys)
variance = np.var(utilitys)

meantext = Label(x=290, y=370, text=f'Mean       = {mean:.3f}',
              text_font_size='10pt',x_units='screen', y_units='screen')
vartext = Label(x=290, y=355, text=f'Variance  = {variance:.3f}',
              text_font_size='10pt',x_units='screen', y_units='screen')
plot1.add_layout(meantext)
plot1.add_layout(vartext)

show(plot1,notebook_handle=True)
```

<Bokeh Notebook handle for In[18]>

We can see that the distriubtion is right skewed with a mean close to 1, indicating that the average comsumer is not much better of, given random distribution of endowments and free trade oportunities, than if all endowments was distributed equally and no trade was allowed, so all consumers consumed precisly one of each good, giving a utility of one independent of parameter-preferences.

**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?

First we find a plot the new distriubtion with still kepping $\gamma=0.8$

```python
e = np.full(N,1)
p1_equal,p2_equal = find_equilibrium(p1, p2, p3, e, e, e, betas,kappa=kappa, eps=eps, maxiter=maxiter)
```

    Step   0:
    p1 =   6.49007569 -> excess demand 1->    10.27306936
    p2 =   2.61638187 -> excess demand 2->  -156.06712716
    Step   1:
    p1 =   6.49009303 -> excess demand 1->     8.67191455
    p2 =   2.61607875 -> excess demand 2->  -151.55895838
    Step   2:
    p1 =   6.49010728 -> excess demand 1->     7.12421295
    p2 =   2.61578436 -> excess demand 2->  -147.19285237
    Step   3:
    p1 =   6.49011854 -> excess demand 1->     5.62826125
    p2 =   2.61549843 -> excess demand 2->  -142.96436254
    Step   4:
    p1 =   6.49012690 -> excess demand 1->     4.18240975
    p2 =   2.61522070 -> excess demand 2->  -138.86917806
      ...
    Step 2500:
    p1 =   6.44778466 -> excess demand 1->    -0.42306241
    p2 =   2.59099067 -> excess demand 2->    -0.15730786
    Step 5000:
    p1 =   6.44733119 -> excess demand 1->    -0.00419167
    p2 =   2.59082205 -> excess demand 2->    -0.00155860
    Step 7500:
    p1 =   6.44732670 -> excess demand 1->    -0.00004153
    p2 =   2.59082038 -> excess demand 2->    -0.00001544
    Step 9519:
    p1 =   6.44732665 -> excess demand 1 ->    -0.00000100
    p2 =   2.59082037 -> excess demand 2 ->    -0.00000037
    Also --------------> excess demand 3 ->     0.00000741

Thus, the price of good 1 slightly drops, while the price of good 2 slightly rises.

```python
x1s_equal = demand_func(p1_equal,p2_equal,p3,e,e,e,betas[:,0],1)
x2s_equal = demand_func(p1_equal,p2_equal,p3,e,e,e,betas[:,1],2)
x3s_equal = demand_func(p1_equal,p2_equal,p3,e,e,e,betas[:,2],3)

utilitys_equal = []
for i in range(N):
    utilitys_equal.append(utility(x1s_equal[i],x2s_equal[i],x3s_equal[i],betas[i,0],betas[i,1],betas[i,2], 0.9))

hist, edges = np.histogram(utilitys_equal, bins=100)
plot2 = Functions.plot_hist([hist], [edges],names= [''],plot_range=[0,4],
                       y_label='Observations',x_label='Utility')
mean = np.mean(utilitys_equal)
variance = np.var(utilitys_equal)

meantext = Label(x=290, y=370, text=f'Mean       = {mean:.3f}',
              text_font_size='10pt',x_units='screen', y_units='screen')
vartext = Label(x=290, y=355, text=f'Variance  = {variance:.3f}',
              text_font_size='10pt',x_units='screen', y_units='screen')
plot2.add_layout(meantext)
plot2.add_layout(vartext)

show(plot2,notebook_handle=True)
```

<Bokeh Notebook handle for In[18]>

We have a sligthly bigger mean, and a much lower variance, with the endowemnts having been distributed equally.

With everyone starting out with one of each good, all comsumers have a minimum utility of 1, and much less trade seems to be occuring.

Now we plot the same figures, both for different values of $\gamma$:

```python
gammas = [0.2,0.4,0.6,0.8,1,1.5]

plots =[]

for i,gamma in enumerate(gammas):
    plots.append(Functions.utility_distribution(x1s,x2s,x3s,x1s_equal,x2s_equal,x3s_equal,betas,gamma))
```

```python
show(column(row(plots[0][0],plots[0][1]),row(plots[1][0],plots[1][1]),row(plots[2][0],plots[2][1]),
           row(plots[3][0],plots[3][1]),row(plots[4][0],plots[4][1]),row(plots[5][0],plots[5][1])))
```

We see that the variance rises with gamma for both random and equally distributed endowments. The mean also mostly rises, but not for small $\gamma$ with randomly distributed endowments where we see that it drops.

We have to note though, that this is only the distriubtion of utility function for the level of comsumption for each consumer. Changing gamma does not affect the prices nor the distribution of comsumption, only the value of the utility function.


# Занятие 10
# Прикладная алгебра и численные методы
## Матричные нормы. Приближение матриц меньшего ранга

```
import numpy as np
import sympy
import scipy.linalg
import numpy.linalg
import pandas as pd
import matplotlib.pyplot as plt
from google.colab import files
```

```
!python -m pip install scipy --upgrade
```

    Collecting scipy
      Downloading https://files.pythonhosted.org/packages/b6/3a/9e0649ab2d5ade703baa70ef980aa08739226e5d6a642f084bb201a92fc2/scipy-1.6.1-cp37-cp37m-manylinux1_x86_64.whl (27.4MB)
         |████████████████████████████████| 27.4MB 147kB/s
    Requirement already satisfied, skipping upgrade: numpy>=1.16.5 in /usr/local/lib/python3.7/dist-packages (from scipy) (1.19.5)
    ERROR: albumentations 0.1.12 has requirement imgaug<0.2.7,>=0.2.5, but you'll have imgaug 0.2.9 which is incompatible.
    Installing collected packages: scipy
      Found existing installation: scipy 1.4.1
        Uninstalling scipy-1.4.1:
          Successfully uninstalled scipy-1.4.1
    Successfully installed scipy-1.6.1

```
sympy.__version__, scipy.__version__, np.__version__
```

    ('1.7.1', '1.6.1', '1.19.5')

## Подчиненные (операторные) матричные нормы.

https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html

numpy.linalg.norm(x, ord=None, axis=None, keepdims=False)

https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.norm.html

scipy.linalg.norm(a, ord=None, axis=None, keepdims=False, check_finite=True)

если check_finite=True, проверяется, все ли элементы матрицы конечны

#### Возможные значения параметра ord
одинаково в numpy и scipy
$$
\begin{matrix}
ord &	norm\ for\ matrices\\
None &		Frobenius\ norm\\
‘fro’	 &	Frobenius\ norm\\
‘nuc’ &		nuclear\ norm\\
inf &		max(sum(abs(x),\ axis=1))\\
-inf &		min(sum(abs(x),\ axis=1))\\
0	 &	–\\
1	 &	max(sum(abs(x),\ axis=0))\\
-1 &		min(sum(abs(x),\ axis=0))\\
2	 &	2-norm\ (largest\ singular\ value)\\
-2 &		smallest\ singular\ value\\
other &		–
\end{matrix}
$$

Матричная норма
$ \|A\|_{1}=\max \limits _{1\leq j\leq n}\sum _{i=1}^{m}|a_{ij}|$, подчинённая векторной норме$ \|x\|_{1}=\sum _{i=1}^{n}|x_{i}|$.

Матричная норма $\|A\|_{\infty }=\max \limits _{1\leq i\leq m}\sum _{j=1}^{n}|a_{ij}|$, подчинённая векторной норме $\|x\|_{\infty }=\max \limits _{1\leq i\leq n}|x_{i}|$.

Спектральная норма $\|A\|_{2}=\sup \limits _{\|x\|_{2}=1}\|Ax\|_{2}=\sup \limits _{(x,x)=1}{\sqrt {(Ax,Ax)}}={\sqrt {\lambda _{max}(A^{*}A)}}$, подчиненная векторной норме $\|x\|_{2}={\sqrt {\sum _{i=1}^{n}|x_{i}|^{2}}}$.

Норма Фробениуса $ \|A\|_{F} = \sqrt{\sum _{i, j}|a_{ij}|^2} = {\rm tr}(A^*A) = \sqrt{\sum _{i=1}^{n}\sigma_{i}^2}$

## Пример 1.
Вычислим матричные нормы $||M||_1$, $||M||_2$, $||M||_\infty$ и $||M||_F$  для матрицы

$$
\left(
\begin{matrix}
1 & 3 & 5\\
2 & 5 & 8\\
3 & 7 & 11
\end{matrix}
  \right)
$$

```
M = [[1, 3, 5], [2, 5, 8], [3, 7, 11]]
for package in (np, scipy):
  display(package.__name__, *[('ord', item, '||M||', package.linalg.norm(M, ord=item)) for item in (1, 2, np.inf, 'fro')])
```

    'numpy'

    ('ord', 1, '||M||', 24.0)

    ('ord', 2, '||M||', 17.518067568404707)

    ('ord', inf, '||M||', 21.0)

    ('ord', 'fro', '||M||', 17.52141546793523)

    'scipy'

    ('ord', 1, '||M||', 24.0)

    ('ord', 2, '||M||', 17.518067568404707)

    ('ord', inf, '||M||', 21.0)

    ('ord', 'fro', '||M||', 17.52141546793523)

https://docs.sympy.org/latest/modules/matrices/matrices.html#sympy.matrices.matrices.MatrixBase.norm

В sympy реализованы нормы с такими же значениями ord, как в scipy и numpy:

norm(ord=None)

это метод в классе матриц.

## Пример 2
Вычислим нормы $||M||_1$, $||M||_2$, $||M||_\infty$ и $||M||_F$  матрицы Примера 1 с помощью sympy:

```
Msym = sympy.Matrix(M)
display([('ord', item, '||M||', [(Mnorm, round(Mnorm, 5)) for Mnorm in [Msym.norm(ord=item)]]) for  item in (1, 2, sympy.oo, 'fro')])
```

    [('ord', 1, '||M||', [(24, 24)]),
     ('ord', 2, '||M||', [(sqrt(sqrt(94105)/2 + 307/2), 17.51807)]),
     ('ord', oo, '||M||', [(21, 21)]),
     ('ord', 'fro', '||M||', [(sqrt(307), 17.52142)])]

## Спектр матрицы - множество ее собственных значений.
https://numpy.org/doc/stable/reference/generated/numpy.linalg.eigvals.html

numpy.linalg.eigvals(a)

https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.eigvals.html

scipy.linalg.eigvals(a, b=None, overwrite_a=False, check_finite=True, homogeneous_eigvals=False)

## Пример 3
Найдем собственные числа матрицы из файла A_10_3.xlsx

```
uploaded = files.upload()

for fn in uploaded.keys():

  print('User uploaded file "{name}" with length {length} bytes'.format(name=fn, length=len(uploaded[fn])))
```

<input type="file" id="files-1718c62f-5087-4cf6-b98a-67463e0b4311" name="files[]" multiple disabled
   style="border:none" />
<output id="result-1718c62f-5087-4cf6-b98a-67463e0b4311">
 Upload widget is only available when the cell has been executed in the
 current browser session. Please rerun this cell to enable.
 </output>

    Saving A_10_3.xlsx to A_10_3.xlsx
    User uploaded file "A_10_3.xlsx" with length 12784 bytes

```
A3 = pd.read_excel(fn, index_col=0, header=0, dtype={'Name': int, 'Value': int}).values

A3_spectr = numpy.linalg.eigvals(A3)
display(*A3_spectr)
```

    (999.6319322682816+0j)

    (15.544588050261174+109.26947516511581j)

    (15.544588050261174-109.26947516511581j)

    (88.71954071691479+31.229896226005277j)

    (88.71954071691479-31.229896226005277j)

    (78.16595494011501+0j)

    (53.652504475303154+42.59017589133343j)

    (53.652504475303154-42.59017589133343j)

    (10.8819667049992+75.52752013123322j)

    (10.8819667049992-75.52752013123322j)

    (-102.1390320856124+40.62023986302754j)

    (-102.1390320856124-40.62023986302754j)

    (-99.78578669709361+20.64267357079272j)

    (-99.78578669709361-20.64267357079272j)

    (-19.761777313467+71.5172096845359j)

    (-19.761777313467-71.5172096845359j)

    (12.121686002207094+0j)

    (-25.248735665312054+50.26721265487169j)

    (-25.248735665312054-50.26721265487169j)

    (-55.64610958258963+0j)

Получились в основном комплексные числа. Модули найдем с помощью abs, составим список модулей собственных значений и отсортируем его по возрастанию, послений элемент списка - максимальное собственное число.

```
A3_spectr_abs = abs(A3_spectr)
A3_spectr_abs.sort()
radiusA3 = A3_spectr_abs[-1]
display('спектральный радиус', radiusA3, 'спектр', A3_spectr_abs)
```

    'спектральный радиус'

    999.6319322682816

    'спектр'

    array([ 12.121686  ,  55.64610958,  56.25203393,  56.25203393,
            68.50192931,  68.50192931,  74.19729863,  74.19729863,
            76.30742753,  76.30742753,  78.16595494,  94.05563951,
            94.05563951, 101.89859272, 101.89859272, 109.91990612,
           109.91990612, 110.36961729, 110.36961729, 999.63193227])

Сингулярные числа матрицы можно найти с помощью scipy.linalg.svdvals

https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.svdvals.html

## Пример 4
С помощью разложения SVD (scipy.linalg.svdvals) получим сингулярные числа матрицы Примера 3. Изобразим на комплексной плоскости сингулярные числа, собственные числа и окружность с радиусом, равным спектральному радиусу матрицы A3.

```
sing_val_A3 = scipy.linalg.svdvals(A3)
ax = plt.gca()
radius2 = radiusA3**2
X = np.linspace(-radiusA3, radiusA3, 256)
Y = np.sqrt(radius2 - X**2)
ax.axis('equal')
ax.plot(sing_val_A3, np.zeros(len(sing_val_A3)), 'ro', np.real(A3_spectr), np.imag(A3_spectr), 'go', X, Y, 'm--', X, -Y, 'm--')
ax.set_xlabel('Re')
ax.set_ylabel('Im')
print(sing_val_A3)
```

## Пример 5
Проверим утверждение $||A||_F = \sqrt{\sigma_1^2 + \sigma_1^2 + ... + \sigma_1^n}$ на матрице А3.

```
display(numpy.linalg.norm(sing_val_A3, ord=2),
        numpy.linalg.norm(A3, ord='fro'))
```

    1158.4355830170268

    1158.4355830170273

## Пример 6
Проверим утверждение, что норма, согласованная с некоторой векторной нормой, равна числу, которое не меньше, чем спектральный радиус.

Вычислим нормы матрицы А3 и сравним их со спектральным радиусом.

```
display(*[('ord', item, '||A3|', [(normA3, normA3 >= radiusA3) for normA3 in (scipy.linalg.norm(A3, ord=item),)][0]) for item in (1, 2, np.inf, 'fro')], radiusA3)
```

    ('ord', 1, '||A3|', (1391.0, True))

    ('ord', 2, '||A3|', (1019.7132622612315, True))

    ('ord', inf, '||A3|', (1210.0, True))

    ('ord', 'fro', '||A3|', (1158.4355830170273, True))

    999.6319322682816

## Приближенное разложение меньшего ранга
$$
A_{n\times n} = A_{n\times r} \times  A_{r\times n},\quad
r - \mbox{ ранг матрицы А}.
$$
Пусть SVD-разложение матрицы $A = V\Sigma U^*$, где
$$
\Sigma =
\left(
\begin{matrix}
\sigma_1 & 0 & 0 & ... & 0\\
0 & \sigma_2 & 0 & ... & 0 \\
0 &0 & ...& ... & 0\\
0 & ... & ...& ... & 0 \\
0 &0 & ... & ... & \sigma_n
\end{matrix}
\right),
$$
тогда $A_r = V\Sigma_r U^*$, где
$$
\Sigma_r  =
\left(
\begin{matrix}
\sigma_1 & 0 & 0 & ... & ... & 0\\
0 & \sigma_2 & 0 & ... & ... &0 \\
0 &0 & ...& ... & ... & 0\\
0 & ... & ...& \sigma_r & ... & 0 \\
0 &0 & ... & ... &... & 0\\
 ...  & ...  & ... & ... &... &  ... \\
0 &0 & ... & ... &... & 0\\
\end{matrix}
\right),
$$
## Пример 7
Построим приближение ранга 1 для матрицы А7 в спектральной норме, найдем норму разности матриц.
Пусть SVD-разложение матрицы $A = V\Sigma U^*$, где
$$
A_7 =
\left(
\begin{matrix}
6 & 0 & 6\\
0 & 12 & 6\\
6 & 6 & 9
\end{matrix}
\right),
$$

```
A7 = [[6, 0, 6], [0, 12, 6], [6, 6, 9]]
U7, sigmaA7, Vh7 = scipy.linalg.svd(A7)
Sigma7 = np.diag(sigmaA7)
UsVh7 = U7 @ Sigma7 @ Vh7
Sigma7_1 = np.zeros((3, 3))
Sigma7_1[0] = Sigma7[0]
A7r = U7 @ Sigma7_1 @ Vh7
display(U7, sigmaA7, Vh7, UsVh7, np.allclose(UsVh7, A7), A7r, scipy.linalg.norm(A7 - A7r))
```

    array([[-0.33333333,  0.66666667, -0.66666667],
           [-0.66666667, -0.66666667, -0.33333333],
           [-0.66666667,  0.33333333,  0.66666667]])

    array([1.80000000e+01, 9.00000000e+00, 4.94791776e-16])

    array([[-0.33333333, -0.66666667, -0.66666667],
           [ 0.66666667, -0.66666667,  0.33333333],
           [-0.66666667, -0.33333333,  0.66666667]])

    array([[ 6.00000000e+00,  2.92251872e-15,  6.00000000e+00],
           [-7.04209823e-16,  1.20000000e+01,  6.00000000e+00],
           [ 6.00000000e+00,  6.00000000e+00,  9.00000000e+00]])

    True

    array([[2., 4., 4.],
           [4., 8., 8.],
           [4., 8., 8.]])

    9.0

## Круги Гершгорина
#### Первая теорема Гершгорина

Все собственные значения матрицы $A_{n\times n} = (a_{ij})$ содержатся в объединении кругов на комплексной плоскости
$$
|\lambda - a_{ii}| \le \sum_{k = 1, k \neq i}^n |a_{ik}|, \quad i = 1, ..., n.
$$

## Пример 8
$$
A_8 =
\left(
\begin{matrix}
1 & 2 & 0 & 0 & 0\\
3 & 1 & 2 & 0 & 0 \\
0 & 3 & 1 & 2 & 0\\
0 & 0 & 3 & 1 & 2 \\
0 &0 & 0 & 3 & 1
\end{matrix}
\right),
$$

```
A8 = np.array([[1, 2, 0, 0, 0], [3, -1, 2, 0, 0 ], [0, 3, 2, 2, 0], [0, 0, 3, 5, 2 ], [0, 0, 0, 3, 1]])
n = A8.shape[0]
A8_spectr = numpy.linalg.eigvals(A8)
center = [A8[i, i] for i in range(n)]
R_i = [sum(A8[:, i]) for i in range(n)]
ax = plt.gca()
ax.axis('equal')
style = ['r', 'g', 'b', 'k', 'c']*10
for i in  range(n):
  radius_i = R_i[i]
  center_i = center[i]
  style_i = style[i]
  X = np.linspace(center_i - radius_i, center_i + radius_i, 256)
  Y = np.sqrt(radius_i**2 - (X - center_i)**2)
  ax.plot(X, Y, style_i + '-', X, -Y, style_i + '-')
ax.plot(np.real(A8_spectr), np.imag(A8_spectr), 'go')
ax.set_xlabel('Re')
ax.set_ylabel('Im')
print(A8_spectr)
```


```python
import numpy as np
import matplotlib.pyplot as plt
from typing import Callable, Optional, Tuple
import numdifftools as nd
import sympy as sp
from iminuit import Minuit
from iminuit.cost import LeastSquares
import tabulate
```

```python
def fodd(f, x, p):
    return 0.5 * (f(x, p) - f(x, -p))

def central(f, x, p, h):
    hinv = 1.0 / h
    return fodd(f, x, p + h) * hinv

def f(x, p):
#     return np.round(np.exp(x + p), 5)
    return np.exp(x + p)
#     return (x + p) ** -10
#     return ((x + p) ** 3)

def fpt(x):
#     return -10 * x ** -11
    return np.exp(x)
#     return 3 * x ** 2

def steps(p, h0=0.125, factor=0.5 / 1.618033988749895):
    eps = np.finfo(float).resolution
    h = p * h0
    if h == 0:
        h = h0
    n = int(np.log(eps / h0 ** 2) / np.log(factor)) + 1
    return h * factor ** np.arange(n)

x = np.linspace(-0.2, 0.2, 10)
p = 0.0

results = []
for hi in steps(p, 0.5):
    fdi = central(f, x, p, hi)
    results.append((hi, fdi))

for i in range(len(x)):

    def reldev(fx):
        return np.abs(fx / fpt(x[i]) - 1)

    fddev = []
    fd = []
    h = []
    for hi, fdi in results:
        h.append(hi)
        fd.append(fdi[i])
        fddev.append(reldev(fdi[i]))
    h = np.array(h)

    fdd = np.diff(fd) ** 2
    for n in range(1, len(fdd)):
        if fdd[n] >= fdd[n-1]:
            break
    n = min(n, 5)

    if n == 1:
        p_est = fd[:1]
        fd_err = np.inf
    elif n == 2:
        p_est = np.polyfit(h[:n] ** 2, fd[:n], n - 1,
                              rcond=None, cov=False)
        fd_err = fdd[0]
    else:
        p_est, C = np.polyfit(h[:n] ** 2, fd[:n], n - 2,
                              rcond=None, cov=True)
        fd_err = C[-1, -1] ** 0.5

    plt.figure()
    plt.plot(h, fddev, "o")
    plt.loglog()

    hm = np.geomspace(1e-9, h[0])
    plt.plot(hm, reldev(np.polyval(p_est, hm ** 2)),
             color="r")
    plt.axhline(reldev(p_est[-1]), color="g")
    plt.axhline(fd_err / p_est[-1], color="g", ls="--")
    plt.title(f"x = {x[i]} "
              f"rel-err[est] = {fd_err / p_est[-1]:.1e} "
              f"rel-err = {reldev(p_est[-1]):.1e}")
    plt.plot(h[:-1], np.abs(np.diff(fd)) / fpt(x[i]), "s", mfc="none", ms=10)
    plt.plot(h[:n], reldev(fd[:n]), "+", zorder=5, ms=15, color="k")
    plt.loglog()
```

```python
def _central(f, x, h):
    return (f(x + h) - f(x - h)) * (0.5 / h)

def _steps(x, h0, factor, maxiter):
    h = x * h0
    if h == 0:
        h = h0
    return h * factor ** np.arange(maxiter)

def derive(f, x, rtol=0, maxiter=10,
    factor=0.3090169943749474,
    initial_step=0.5,
    diagnostic=None
):
    squeeze = np.ndim(x) == 0
    x = np.atleast_1d(x)
    x_shape = np.shape(x)

    h = _steps(x, initial_step, factor, maxiter)

    r = _central(f, x, h[0])
    r_shape = np.shape(r)
    squeeze &= r_shape == x_shape
    re = np.full(r_shape, np.inf)
    todo = np.ones(r_shape, dtype=bool)

    fd = []
    fd.append(r)
    for i in range(1, len(h)):
        fdi = _central(f, x, h[i])
        fd.append(fdi[todo])

        # polynomial fit with one extra degree of freedom
        grad = min(i - 1, 3)
        start = i - (grad + 1)
        stop = i + 1
        q, c = np.polyfit(h[start:stop] ** 2, fd[start:], grad,
                          rcond=None, cov=True)
        ri = q[-1]
        # pulls have roughly unit variance, however,
        # the pull distribution is not gaussian and looks
        # more like student's t
        rei = c[-1, -1] ** 0.5

        # update estimates that have significantly smaller error now
        sub_todo = rei < 2 * re[todo] * factor ** 2
        todo1 = todo.copy()
        todo[todo1] = sub_todo
        r[todo] = ri[sub_todo]
        re[todo] = rei[sub_todo]

        # do not improve estimates further which meet the tolerance
        if rtol:
            sub_todo &= rei > rtol * np.abs(ri)
            todo[todo1] = sub_todo

#         print("dev", r / (3 * x ** 2) - 1)
#         print("est", re / np.abs(r))
#         print(todo)

        if np.sum(todo) == 0:
            break

        # shrink previous vectors of estimates
        fd = [fdi[sub_todo] for fdi in fd]

    if squeeze:
        return np.squeeze(r), np.squeeze(re)
    return r, re

x = np.linspace(-10, 10, 5)

def f(p):
    return np.exp(x + p)

def fpt(x):
    return np.exp(x)

print("exp(x)")
fp, fpe = derive(f, 0)
print(tabulate.tabulate([
    ["est  rel-err"] + list(fpe / np.abs(fp)),
    ["true rel-err"] + list(fp / fpt(x) - 1),
], tablefmt="presto", floatfmt=".1e"))

def f(p):
    return (x + p) ** 3

def fpt(x):
    return 3 * x ** 2

print("x^3")
x = np.linspace(-0.0001, 0.0001, 6)
fp, fpe = derive(f, 0)
print(tabulate.tabulate([
    ["value"] + list(fp),
    ["est  rel-err"] + list(fpe / np.abs(fp)),
    ["true rel-err"] + list(fp / fpt(x) - 1),
], tablefmt="presto", floatfmt=".1e"))
```

    exp(x)
     est  rel-err | 2.3e-13 |  8.0e-14 | 5.2e-15 |  7.3e-14 | 2.6e-13
     true rel-err | 2.4e-13 | -1.1e-13 | 5.6e-15 | -7.8e-14 | 2.6e-13
    x^3
     value        |  3.0e-08 |  1.1e-08 |  1.2e-09 |  1.2e-09 |  1.1e-08 |  3.0e-08
     est  rel-err |  1.6e-14 |  1.1e-13 |  7.7e-13 |  7.7e-13 |  1.1e-13 |  1.6e-14
     true rel-err | -1.4e-12 | -1.6e-13 | -1.6e-12 | -1.6e-12 | -1.6e-13 | -1.4e-12

```python
derive(np.exp, -100)
```

    (array(3.72007598e-44), array(1.71881269e-56))

```python
derive(lambda p: (x + p) ** 3, 1e-10, 0)
```

    (array([2.9999940e-08, 1.0799964e-08, 1.1999880e-09, 1.2000120e-09,
            1.0800036e-08, 3.0000060e-08]),
     array([5.03651887e-18, 5.16670001e-20, 3.86891275e-20, 4.42922251e-20,
            4.04502765e-19, 5.35943323e-18]))

```python
nd.Derivative(lambda p: (x + p) ** 3)(1e-10)
```

    array([2.9999940e-08, 1.0799964e-08, 1.1999880e-09, 1.2000120e-09,
           1.0800036e-08, 3.0000060e-08])

```python
%%timeit -n 1 x = np.linspace(0, 10, 10000)
derive(lambda p: np.exp(x + p), 0)
```

    10.6 ms ± 2.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

```python
%%timeit -n 1 x = np.linspace(0, 10, 10000)
derive(lambda p: np.exp(x + p), 0, rtol=1e-2)
```

    2.24 ms ± 551 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

```python
%%timeit -n 1 -r 1 x = np.linspace(0, 10, 10000)
nd.Derivative(lambda p: np.exp(x + p))(0)
```

    1.45 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

```python
x = np.linspace(-10, 10, 1000)

fp1 = nd.Derivative(lambda p: np.exp(x + p).astype(np.float32))(0)
fp2 = derive(lambda p: np.exp(x + p).astype(np.float32), 0, initial_step=0.5, factor=0.5)[0]

plt.plot(x, np.abs(fp1 / np.exp(x) - 1), label="numdifftools")
plt.plot(x, np.abs(fp2 / np.exp(x) - 1), label="derive")
plt.semilogy()
plt.legend();
```

```python
x = np.geomspace(1e-10, 1, 100)

fp1 = nd.Derivative(lambda p: (x + p) ** 3)(0)
fp2, fpe2 = derive(lambda p: (x + p) ** 3, 0)

plt.plot(x, np.abs(fp1 / fpt(x) - 1), label="numdifftools")
plt.plot(x, np.abs(fp2 / fpt(x) - 1), label="derive")
plt.plot(x, np.abs(fpe2 / fpt(x)), label="derive-err")
plt.loglog()
plt.legend();
```

```python
class F:
    nf = 0
    nx = 0

    def __init__(self, x):
        self.x = x

    def __call__(self, p):
        x = self.x
        self.nf += 1
        self.nx += len(x)
        y = (x + p)
        return np.sin(y)/(y**2 + 1)

x = np.linspace(-10, 10, 201)
f = F(x)
fpx, fpxe = derive(f, 0)

plt.plot(x, F(x)(0), label="f(x)")
plt.plot(x, fpx, label="f'(x)")
plt.legend();
```

```python
f = F(x)
fp1 = nd.Derivative(f)(0)
print(f.nf, f.nx, round(f.nx / len(x), 1))
```

    30 6030 30.0

```python
f = F(x)
fp2 = derive(f, 0)[0]
print(f.nf, f.nx, round(f.nx / len(x), 1))
```

    16 3216 16.0

```python
f = F(x)
fp3 = derive(f, 0, rtol=1e-3)[0]
print(f.nf, f.nx, round(f.nx / len(x), 1))
```

    8 1608 8.0

```python
# compute exact derivative of sin(x)/x
fp = sp.lambdify("x", sp.diff("sin(x)/(x**2 + 1)").simplify(), "numpy")
plt.plot(x, fp(x));
```

```python
fpx = fp(x)
plt.plot(x, np.abs(fp1/fpx-1), label=f"numdifftools stdev={np.nanstd(fp1/fpx):.0e}")
plt.plot(x, np.abs(fp2/fpx-1), label=f"vjacobi stdev={np.nanstd(fp2/fpx):.0e}")
plt.plot(x, np.abs(fp3/fpx-1), label=f"vjacobi rtol=1e-3 stdev={np.nanstd(fp3-fpx):.0e}")
plt.semilogy()
plt.legend();
```

```python
from scipy.stats import median_abs_deviation as mad

x = np.linspace(-10, 10, 20001)

fp1, fp1e = derive(F(x), 0)

pull = (fp1 - fp(x)) / fp1e
plt.hist(pull, bins=100, range=(-5,5));
plt.title(f"{np.std(pull):.2f} {mad(pull):.2f}");
```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```


# ***Introduction to Radar Using Python and MATLAB***
## Andy Harrison - Copyright (C) 2019 Artech House
<br/>

# Search Radar Output Signal to Noise Ratio
***

The output signal to noise ratio for a search radar may be written as (Equation 4.54)

\begin{equation}\label{eq:radar_equation_search_final}
    {SNR}_o = \frac{P_{av}\, A_e\, \sigma }{(4\pi)^3\, k\, T_0\, F\, L\, r^4} \, \frac{T_{scan}}{\Omega}
\end{equation}
***

Begin by getting the library path

```python
import lib_path
```

Set the minimum and maximum target range (m)

```python
target_min_range = 10e3

target_max_range = 100e3
```

Import the `linspace` routine from `scipy` and set up the target range array

```python
from numpy import linspace

target_range = linspace(target_min_range, target_max_range, 2000)
```

Set the noise figure (dB), the radar losses (dB), the target RCS (dBsm), the system temperature (K), the search volume (sr), the scan time (s), and the power aperture (dB)

```python
from scipy.constants import pi

noise_figure = 4.0

losses = 9.0

target_rcs = -5.0

system_temperature = 305

search_volume = 2.0 * pi

scan_time = 0.1

power_aperture = 80
```

Set up the keyword args

```python
kwargs = {'target_range': target_range,

           'system_temperature': system_temperature,

                  'search_volume': search_volume,

                  'noise_factor': 10 ** (noise_figure / 10.0),

                  'losses': 10 ** (losses / 10.0),

                  'power_aperture': 10 ** (power_aperture / 10.0),

                  'scan_time': scan_time,

                  'target_rcs': 10 ** (target_rcs / 10.0)}
```

Import the `output_snr` routine from `search_radar_range`

```python
from Libs.radar_range.search_radar_range import output_snr
```

Calculate the output signal to noise ratio

```python
 output_snr = output_snr(**kwargs)
```

Import the `matplotlib` routines and the `log10` routine for plotting the output signal to noise ratio

```python
from matplotlib import pyplot as plt

from numpy import log10
```

Display the results

```python
# Set the figure size

plt.rcParams["figure.figsize"] = (15, 10)

# Display the results

plt.plot(target_range / 1.0e3, 10.0 * log10(output_snr), '')

# Set the plot title and labels

plt.title('Output Signal to Noise for Search', size=14)

plt.xlabel('Target Range (km)', size=12)

plt.ylabel('Output Signal to Noise Ratio (dB)', size=12)

# Set the tick label size

plt.tick_params(labelsize=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)
```


## Selecting airport hubs for a new airline

The problem of facility location is common to industries that have physical locations ("facilities") and need to identify the best new location as their business grows. There are numerous examples of this, ranging from chain stores and restaurants trying to open new locations, to placing public electric car charging stations in cities and along well-traveled routes. A common thread among these examples is that, generally, many locations have already been picked out and cannot be moved.

A simple strategy for placing locations would be to make selections based on their total (or expected) number of served people. However, depending on how a location is defined, this approach might leave people in smaller cities or lower density areas completely unserved. For instance, if locations were defined as small contiguous regions and choices made based on their population, some large cities are likely to get multiple locations.  Put another way, there may be redundancy among the people who are served.

A more complex strategy is to use a clustering method applied to data where each potential location is encoded as an example weighted by the number of people at the location. Using k-medoids with k being the number of locations that would be open guarantees that exact locations are picked and can try to minimize the redundancy among those served. However, clustering approaches have the significant downside that, when placing a new facility, one would have to re-run the clustering algorithm with a larger number of clusters. Given that it is likely impossible to move currently existing facilities in these locations, this approach does not seem feasible.

In contrast to traditional clustering approaches, submodular optimization provides an appealing framework for this task. Other notebooks in this repository go more in depth on the details of submodular optimization, but essentially, one can use a greedy approach that iteratives selects examples which minimize redundancy with those that have already been chosen. This property is convenient when you can't move previous facilities.

Let's say you're a new airline and you want to know where to put your hubs. You might want these hubs to be at locations that service the most routes so that you can minimize the distance an airplane has to fly to return to a hub. Initially, you might suspect that putting these hubs at the biggest cities is likely the best choice. However, since this notebook is being written as part of a tutorial on submodular optimization, you also suspect that submodular optimization might yield a better selection.

```python
%pylab inline
numpy.random.seed(0)

import pandas
import seaborn; seaborn.set_style('whitegrid')
```

    Populating the interactive namespace from numpy and matplotlib

### Loading the Data

We will be using two tables of data from openflights.org for this example. The first table contains information about airports around the world and the second table contains information about recorded routes. A limitation of this data is that it was updated most recently in 2014.

Let's first load up the airport table. Because we are focusing on the United States, we will filter out the airports from other parts of the world. This table includes a lot of information about each airport, but the only things we really need are the ID and the name, for human interpretability.

```python
names = 'Airport ID', 'Name', 'City', 'Country', 'IATA', 'ICAO', 'Latitude', 'Longitude', 'Altitude', 'Timezone', 'DST', 'Tz', 'Type', 'Source'
airports = pandas.read_csv("airports.csv", header=None, names=names)
airports = airports[airports['Country'] == 'United States']
airports.head()
```

      Airport ID
      Name
      City
      Country
      IATA
      ICAO
      Latitude
      Longitude
      Altitude
      Timezone
      DST
      Tz
      Type
      Source

      3212
      3411
      Barter Island LRRS Airport
      Barter Island
      United States
      BTI
      PABA
      70.134003
      -143.582001
      2
      -9
      A
      America/Anchorage
      airport
      OurAirports

      3213
      3412
      Wainwright Air Station
      Fort Wainwright
      United States
      \N
      PAWT
      70.613403
      -159.860001
      35
      -9
      A
      America/Anchorage
      airport
      OurAirports

      3214
      3413
      Cape Lisburne LRRS Airport
      Cape Lisburne
      United States
      LUR
      PALU
      68.875099
      -166.110001
      16
      -9
      A
      America/Anchorage
      airport
      OurAirports

      3215
      3414
      Point Lay LRRS Airport
      Point Lay
      United States
      PIZ
      PPIZ
      69.732903
      -163.005005
      22
      -9
      A
      America/Anchorage
      airport
      OurAirports

      3216
      3415
      Hilo International Airport
      Hilo
      United States
      ITO
      PHTO
      19.721399
      -155.048004
      38
      -10
      N
      Pacific/Honolulu
      airport
      OurAirports

Next, let's load up the routes. These routes include a source airport, which is where the flight began, and a destination airport, which is where the plane was flying to, as well as some other information that we can ignore.

```python
names = 'Airline', 'Airline ID', 'Source ', 'Source ID', 'Destination', 'Destination ID', 'Codeshare', 'Stops', 'Equipment'
routes = pandas.read_csv("routes.csv", header=None, names=names)
routes = routes.replace("\\N", numpy.nan).dropna()
routes['Source ID'] = routes['Source ID'].astype(int)
routes['Destination ID'] = routes['Destination ID'].astype(int)
routes = routes.dropna()
routes.head()
```

      Airline
      Airline ID
      Source
      Source ID
      Destination
      Destination ID
      Codeshare
      Stops
      Equipment

      187
      2P
      897
      GES
      2402
      MNL
      2397
      Y
      0
      320

      197
      2P
      897
      MNL
      2397
      GES
      2402
      Y
      0
      320

      1059
      4M
      3201
      DFW
      3670
      EZE
      3988
      Y
      0
      777

      1060
      4M
      3201
      EZE
      3988
      DFW
      3670
      Y
      0
      777

      1061
      4M
      3201
      EZE
      3988
      JFK
      3797
      Y
      0
      777

### Airport Visualization

We can start off by visualizing the location of each airport in our data set. There are a few airports that are in other countries that we can safely ignore because they are unlikely to be commercial.

```python
plt.figure(figsize=(14, 7))
plt.scatter(airports['Longitude'], airports['Latitude'], s=2, color='c')
plt.xlim(-200, -50)
plt.ylim(10, 80)

plt.axis('off')
plt.show()
```

That shape looks familiar.

Next, we can perform two table joins in order to get the latitude and longitude of the sorce and destination airports for each flight. This will let us visualize the routes.

```python
routes_reduced = routes[['Source ID', 'Destination ID']].drop_duplicates()
airports_reduced = airports[['Airport ID', 'Latitude', 'Longitude']]

routes_merged = pandas.merge(routes_reduced, airports_reduced, left_on='Source ID', right_on='Airport ID')
routes_merged = pandas.merge(routes_merged, airports_reduced, left_on='Destination ID', right_on='Airport ID', suffixes=('_source', '_destination'))
routes_merged = routes_merged.drop(['Airport ID_destination', 'Airport ID_source'], axis=1)
routes_merged.head()
```

      Source ID
      Destination ID
      Latitude_source
      Longitude_source
      Latitude_destination
      Longitude_destination

      0
      3670
      3718
      32.896801
      -97.038002
      32.411301
      -99.681900

      1
      3670
      3700
      32.896801
      -97.038002
      31.611300
      -97.230499

      2
      3670
      3852
      32.896801
      -97.038002
      31.327400
      -92.549797

      3
      3682
      3852
      33.636700
      -84.428101
      31.327400
      -92.549797

      4
      3550
      3852
      29.984400
      -95.341400
      31.327400
      -92.549797

Now let's add the routes to the plot as thin lines.

```python
plt.figure(figsize=(14, 7))
plt.scatter(airports['Longitude'], airports['Latitude'], s=2, color='c')
for i, (_, _, la_x, lo_x, la_y, lo_y) in routes_merged.iterrows():
    plt.plot([lo_x, lo_y], [la_x, la_y], color='k', linewidth=0.15)

plt.xlim(-200, -50)
plt.ylim(10, 80)

plt.axis('off')
plt.show()
```

We can already see some hubs forming. It looks like Atlanta is a clear hub near the east coast, Chicago is a clear hub north of it, Houston is a hub in the south, and Los Angeles is a clear hub on the west coast.

### Airport selection using the most routes

As a baseline, let's see how many routes are covered by those airports that are connected to the most routes. In this case, we're saying that an airport covers a route if it is either the source or the destination of the routes in the data set. We can do this by creating a matrix where each row and column are airports and a 1 indicates that there is a route connected the two airports. This will result in a square symmetric matrix.

```python
n = len(airports)
mapping = {airport: i for i, airport in enumerate(airports['Airport ID'])}

route_map = numpy.zeros((n, n))
for _, (source, destination) in routes_reduced.iterrows():
    if source in mapping and destination in mapping:
        x, y = mapping[source], mapping[destination]
        route_map[x, y] = 1
        route_map[y, x] = 1

route_map.sum()
```

    3404.0

Looks like there are 3404 total routes in our data base.

What are the 6 airports connected to the most routes?

```python
airports_w_routes = airports.copy()
airports_w_routes['# Routes'] = [route_map[mapping[airport]].sum() for airport in airports['Airport ID']]
airports_w_routes.sort_values("# Routes", ascending=False).head(6)
```

      Airport ID
      Name
      City
      Country
      IATA
      ICAO
      Latitude
      Longitude
      Altitude
      Timezone
      DST
      Tz
      Type
      Source
      # Routes

      3482
      3682
      Hartsfield Jackson Atlanta International Airport
      Atlanta
      United States
      ATL
      KATL
      33.636700
      -84.428101
      1026
      -5
      A
      America/New_York
      airport
      OurAirports
      146.0

      3630
      3830
      Chicago O'Hare International Airport
      Chicago
      United States
      ORD
      KORD
      41.978600
      -87.904800
      672
      -6
      A
      America/Chicago
      airport
      OurAirports
      117.0

      3551
      3751
      Denver International Airport
      Denver
      United States
      DEN
      KDEN
      39.861698
      -104.672997
      5431
      -7
      A
      America/Denver
      airport
      OurAirports
      107.0

      3658
      3858
      Minneapolis-St Paul International/Wold-Chamber...
      Minneapolis
      United States
      MSP
      KMSP
      44.882000
      -93.221802
      841
      -6
      A
      America/Chicago
      airport
      OurAirports
      79.0

      3470
      3670
      Dallas Fort Worth International Airport
      Dallas-Fort Worth
      United States
      DFW
      KDFW
      32.896801
      -97.038002
      607
      -6
      A
      America/Chicago
      airport
      OurAirports
      76.0

      3350
      3550
      George Bush Intercontinental Houston Airport
      Houston
      United States
      IAH
      KIAH
      29.984400
      -95.341400
      97
      -6
      A
      America/Chicago
      airport
      OurAirports
      71.0

These airports are fairly expected. Atlanta is the busiest airport in the United States, so it's not surprising that it comes up first. Further, Delta has hubs in Atlanta and Minneapolis-St Paul (among other cities), United has hubs in Chicago, Denver, and Houston. Lastly, Dallas is a hub for American. Thus, all of the top 6 airports here currently serve as hubs for some airline.

What do these airports look like if we plotted their location as well as all the routes that are connected to them?

```python
airports_ = airports_w_routes.sort_values("# Routes", ascending=False).head(6)
airport_idxs = airports_['Airport ID'].values
d = {}

plt.figure(figsize=(14, 7))
plt.scatter(airports['Longitude'], airports['Latitude'], s=2, color='c')
plt.scatter(airports_['Longitude'], airports_['Latitude'], color='r')

for i, (sid, did, la_x, lo_x, la_y, lo_y) in routes_merged.iterrows():
    if (sid, did) in d:
        continue

    if int(sid) in airport_idxs or int(did) in airport_idxs:
        d[(sid, did)] = True
        plt.plot([lo_x, lo_y], [la_x, la_y], color='r', linewidth=0.15)

plt.xlim(-200, -50)
plt.ylim(10, 80)

plt.axis('off')
plt.show()
```

The selected airports seem to be concentrated in the mid-west and ~east. This makes sense, because airports in the middle of the country are well suited for supporting flights to either coast. However, you'll notice that these airports do not serve any of the airports in Alaska or lie at all on the west coast.

Let's evaluate this choice of airports by summing the total number of unique airports that they are connected to. This means that if both Atlanta and Chicago have flights to Seattle that the pair only gets 1 point.

```python
most_routes = numpy.array([mapping[airport] for airport in airport_idxs[:6]])
route_map[most_routes].max(axis=0).sum()
```

    256.0

It looks like this set of airport hubs service 256 unique airports across the United States. Can we do better with submodular optimization?

### Airport selection by optimizing a facility location function

There are many submodular functions that one could optimize for this problem. However, the problem of placing facilities is such a canonical use of submodular optimization that there is a function named the "facility location" function. The functiont takes the form

\begin{equation}
f(X) = \sum\limits_{x} \max\limits_{y} \phi(x, y)
\end{equation}
where $x$ is an example from your data set, or potential locations in this case, $y$ is a facility that has already been selected, and $\phi$ is a similarity measure. Potentially similarity is the physical distance between $x$ and $y$.

Now let's use facility location to select the set of six airports. We can directly use the route map matrix that we made early as a precomputed similarity matrix, with airports having a route between them being "similar" and those not having routes connecting them being "dissimilar."

```python
from apricot import FacilityLocationSelection

model = FacilityLocationSelection(6, pairwise_func='precomputed')
model.fit(route_map)

airports_w_routes.iloc[model.ranking].head(6)
```

      Airport ID
      Name
      City
      Country
      IATA
      ICAO
      Latitude
      Longitude
      Altitude
      Timezone
      DST
      Tz
      Type
      Source
      # Routes

      3482
      3682
      Hartsfield Jackson Atlanta International Airport
      Atlanta
      United States
      ATL
      KATL
      33.636700
      -84.428101
      1026
      -5
      A
      America/New_York
      airport
      OurAirports
      146.0

      3551
      3751
      Denver International Airport
      Denver
      United States
      DEN
      KDEN
      39.861698
      -104.672997
      5431
      -7
      A
      America/Denver
      airport
      OurAirports
      107.0

      3630
      3830
      Chicago O'Hare International Airport
      Chicago
      United States
      ORD
      KORD
      41.978600
      -87.904800
      672
      -6
      A
      America/Chicago
      airport
      OurAirports
      117.0

      3470
      3670
      Dallas Fort Worth International Airport
      Dallas-Fort Worth
      United States
      DFW
      KDFW
      32.896801
      -97.038002
      607
      -6
      A
      America/Chicago
      airport
      OurAirports
      76.0

      3399
      3599
      Bethel Airport
      Bethel
      United States
      BET
      PABE
      60.779800
      -161.837997
      126
      -9
      A
      America/Anchorage
      airport
      OurAirports
      17.0

      3377
      3577
      Seattle Tacoma International Airport
      Seattle
      United States
      SEA
      KSEA
      47.449001
      -122.308998
      433
      -8
      A
      America/Los_Angeles
      airport
      OurAirports
      53.0

Naturally, this approach will select Atlanta first, due in part to its greedy nature which begins by selecting the single most representative example. In this case, the most representative airport is the one connected to the most other airports.

The two major differences between selecting the airports with the most routes and optimizing a facility location function are differences in ranking, with Denver being selected this time ahead of Chicago, and the inclusion of Bethel and Seattle instead of Houston and Minneapolis-St. Paul. The inclusion of Bethel is interesting because, by itself, it is connected to few routes in comparison to the other selected airports. However, each of these routes is connected to a new airport, whereas other airports are connected to airports that likely alrady have flights to Atlanta, Denver, or Chicago. Finally, this approach selects Seattle to be a hub, which is so obviously correct that it needs no further elaboration here.

What do these airports look like on the map, along with the routes connected to them?

```python
airports_ = airports_w_routes.iloc[model.ranking]
airport_idxs = airports_['Airport ID'].values
d = {}

plt.figure(figsize=(14, 7))
plt.scatter(airports['Longitude'], airports['Latitude'], s=2, color='c')
plt.scatter(airports_['Longitude'], airports_['Latitude'], color='m')

for i, (sid, did, la_x, lo_x, la_y, lo_y) in routes_merged.iterrows():
    if (sid, did) in d:
        continue

    if int(sid) in airport_idxs or int(did) in airport_idxs:
        d[(sid, did)] = True
        plt.plot([lo_x, lo_y], [la_x, la_y], color='m', linewidth=0.15)

plt.xlim(-200, -50)
plt.ylim(10, 80)

plt.axis('off')
plt.show()
```

The airports appear to be more evenly spaced across the US. The largest visible difference is more activity on the west coast, particularly the inclusion of more airports from Alaska.

```python
most_routes = numpy.array([mapping[airport] for airport in airport_idxs[:6]])
route_map[most_routes].max(axis=0).sum()
```

    270.0

Looks like we're up to 270 from the 256 before. While it may not seem like much initially, keep in mind that there are many routes that most large airports will be connected to. For instance, Atlanta alone is connected to more than half of the airports that this set of hubs is connected to.

### Concluding remarks

The example here is clearly a toy example. In reality, one would want to factor in many more aspects of each location before deciding on hubs, such as the price of setting up a hub at each airport, the population served, the number of flights that they could reasonably fly out of each airport, and international or shipping routes. Further, the number of airports connected to a set of hub might not be the only metric one would use. However, it is a simple example of submodular optimization for placing facilities.

tutorials/Example B. Airport Selection.ipynb