# Problem set 6: Solving the Solow model

[](https://mybinder.org/v2/gh/NumEconCopenhagen/exercises-2019/master?urlpath=lab/tree/PS6/problem_set_6.ipynb)

```python
import numpy as np
from scipy import linalg
from scipy import optimize
import sympy as sm
import matplotlib.pyplot as plt
```

# Tasks

## Solving matrix equations I

```python
np.random.seed(1900)
n = 5
A = np.random.uniform(size=(n,n))
b = np.random.uniform(size=n)
c = np.random.uniform(size=n)
d = np.random.uniform(size=n)
```

**Question A:** Find the determinant of $[A \cdot A]^{-1}$

```python

linalg.det(linalg.inv(A@A))
```

    13132.548734461483

**Answer:**

```python
X = linalg.det(linalg.inv(A@A))
print(X)
```

    13132.548734461483

**Question B:** Solve the following equation systems directly using **scipy**.

$$
\begin{aligned}
Ax &= b \\
Ax &= c \\
Ax &= d
\end{aligned}
$$

```python
for matrix, name in zip([b,c,d],['b','c','d']):
    print(f'{name}:',linalg.solve(A,matrix))
```

    b: [-15.33189031 -24.00998148  40.02675108  15.24193293   4.89008792]
    c: [ -7.72469812 -11.6354722   20.86796131   7.93343031   1.55909328]
    d: [-2.57374569 -4.00256301  7.93409587  2.56007481  1.25709881]

**Answer:**

```python
xb = linalg.solve(A,b)
xc = linalg.solve(A,c)
xd = linalg.solve(A,d)
print('b:',xb)
print('c:',xc)
print('d:',xd)
```

    b: [-15.33189031 -24.00998148  40.02675108  15.24193293   4.89008792]
    c: [ -7.72469812 -11.6354722   20.86796131   7.93343031   1.55909328]
    d: [-2.57374569 -4.00256301  7.93409587  2.56007481  1.25709881]

**Question C:** Solve the same equation systems as above using `linalg.lu_factor()` and `linalg.lu_solve()`. What is the benefit of this approach?

```python
LU,piv = linalg.lu_factor(A) # only done once
xb = linalg.lu_solve((LU,piv),b) # much faster than regular solve
xc = linalg.lu_solve((LU,piv),c)
xd = linalg.lu_solve((LU,piv),d)
print('b:',xb)
print('c:',xc)
print('d:',xd)
```

    b: [-15.33189031 -24.00998148  40.02675108  15.24193293   4.89008792]
    c: [ -7.72469812 -11.6354722   20.86796131   7.93343031   1.55909328]
    d: [-2.57374569 -4.00256301  7.93409587  2.56007481  1.25709881]

## Solving matrix equations II

```python
F = np.array([[2.0, 1.0, -1.0], [-3.0, -1.0, 2], [-2.0, 1.0, 2.0]])
e = np.array([8.0, -11.0, -3.0])
```

**Question:** Use the function `gauss_jordan()` in the `numecon_linalg` module located in this folder to solve

$$
Fx = e
$$

```python
import numecon_linalg
```

```python
Y = np.column_stack((F,e))
numecon_linalg.gauss_jordan(Y)
Y[:,-1]
```

    array([ 2.,  3., -1.])

**Answer:**

```python
import numecon_linalg
Y = np.column_stack((F,e))
numecon_linalg.gauss_jordan(Y)
print('solution',Y[:,-1])
assert np.allclose(F@Y[:,-1],e)
```

    solution [ 2.  3. -1.]

## Symbolic

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

**Question A:** Find

$$
\lim_{x \rightarrow 0} \frac{\sin(x)}{x}
$$

and

$$
\frac{\partial\sin(2x)}{\partial x}
$$

```python
x = sm.symbols('x')
sm.limit((sm.sin(x)/x),x,0)
```

```python
sm.diff(sm.sin(2*x),x)
```

**Answer:**

```python
print('the limit is:')
x = sm.symbols('x')
sm.limit(sm.sin(x)/x,x,0)
```

```python
print('the derivative is')
x = sm.symbols('x')
sm.diff(sm.sin(2*x),x)
```

**Question B:** Solve the equation

$$
\frac{\sin(x)}{x} = 0
$$

```python
sm.solve(sm.sin(x)/x,x)[0]
```

**Answer:**

```python
sm.solve(sm.sin(x)/x)
```

# Problem: Solve the Solow model

## Introduction

Consider the **standard Solow-model** where:

1. $K_t$ is capital
* $L_t$ is labor (growing with a constant rate of $n$)
* $A_t$ is technology (growing with a constant rate of $g$)
* $Y_t = F(K_t,A_tL_t)$ is GDP

**Saving** is a constant fraction of GDP

$$
S_t = sY_t,\,s\in(0,1)
$$

such that **capital accumulates** according to

$$
K_{t+1}=S_{t}+(1-\delta)K_{t}=sF(K_{t},A_{t}L_{t})+(1-\delta)K_{t}, \delta \in (0,1)
$$

The **production function** has **constant-return to scale** such that

$$
\frac{Y_{t}}{A_{t}L_{t}}=\frac{F(K_{t},A_{t}L_{t})}{A_{t}L_{t}}=F(\tilde{k}_{t},1)\equiv f(\tilde{k}_{t})
$$

where $\tilde{k}_t = \frac{K_t}{A_{t}L_{t}}$ is the technology adjusted capital-labor ratio.

The **transition equation** then becomes

$$
\tilde{k}_{t+1}= \frac{1}{(1+n)(1+g)}[sf(\tilde{k}_{t})+(1-\delta)\tilde{k}_{t}]
$$

If the **production function** is **Cobb-Douglas** then

$$
F(K_{t},A_{t}L_{t})=K_{t}^{\alpha}(A_{t}L_{t})^{1-\alpha}\Rightarrow f(\tilde{k}_{t})=\tilde{k}_{t}^{\alpha}
$$

If it is **CES** (with $\beta < 1, \beta \neq 0$) then

$$
F(K_{t},A_{t}L_{t})=(\alpha K_{t}^{\beta}+(1-\alpha)(A_{t}L_{t})^{\beta})^{\frac{1}{\beta}}\Rightarrow f(\tilde{k}_{t})=(\alpha\tilde{k}_{t}^{\beta}+(1-\alpha))^{\frac{1}{\beta}}
$$

## Steady state

Assume the production function is **Cobb-Douglas**.

**Question A:** Use **sympy** to find an analytical expression for the steady state, i.e. solve

$$
\tilde{k}^{\ast}= \frac{1}{(1+n)(1+g)}[sf(\tilde{k}^{\ast})+(1-\delta)\tilde{k}^{\ast}]
$$

```python
k = sm.symbols('k')
alpha = sm.symbols('alpha')
delta = sm.symbols('delta')
s = sm.symbols('s')
g = sm.symbols('g')
n = sm.symbols('n')
```

```python
transition = sm.Eq(k,(s*k**alpha+(1-delta)*k)/((1+n)*(1+g)))
sm.solve(transition,k)
```

**Answer:**

```python
f = k**alpha
ss = sm.Eq(k,(s*f+(1-delta)*k)/((1+n)*(1+g)))
kss = sm.solve(ss,k)[0]
kss
```

```python
beta = sm.symbols('beta')
beta
```

```python
#beta =-0.2
f_CES = (alpha*k**beta+1-alpha)**(1/beta)
ss = sm.Eq(k,(s*f_CES+(1-delta)*k)/((1+n)*(1+g)))
ss
```

```python
ss1 = sm.Eq(k*((1+n)*(1+g)-1+delta),s*f_CES)
ss1
```

```python
ss2 = sm.Eq((k*((1+n)*(1+g)-1+delta)/s)**beta,(alpha*k**beta+1-alpha))
ss2
```

```python
kss2 = sm.solve(ss2,k)
kss2
```

```python
beta = sm.symbols('beta',negative=True)
f_CES = (alpha*k**beta+1-alpha)**(1/beta)
ss = sm.Eq(k,(s*f_CES+(1-delta)*k)/((1+n)*(1+g)))
kss = sm.solve(ss,k)
kss
```

```python
ss_func = sm.lambdify((s,g,n,delta,alpha,beta),kss2[0])
ss_func(0.2,0.02,0.01,0.1,1/3,-0.5)
```

```python
from sympy.solvers.solveset import solveset, solveset_real
from sympy.solvers.solveset import _solve_exponential as solve_expo
```

```python
f_CES = (alpha*k**beta+1-alpha)**(1/beta)
ss= (s*f_CES+(1-delta)*k)/((1+n)*(1+g))-k
solve_expo(ss2,0,k,sm.S.Reals)
```

```python
from sympy.solvers.solveset import nonlinsolve
```

```python
eq1 = (beta<1)
eq2 = (beta>0)

nonlinsolve([ss,eq1,eq2],k)
```

```python
ss
```

```python
ss_func = sm.lambdify((s,g,n,delta,alpha),kss)
ss_func(0.2,0.02,0.01,0.1,1/3)
```

```python
1.982233502409577
```

```python
new = sm.Eq(f_CES,1)
sm.solve(new,k)
```

```python
from sympy.solvers.solveset import _transolve as transolve
```

```python
f_CES = (alpha*k**0.5+1-alpha)**(1/0.5)
ss= (s*f_CES+(1-delta)*k)/((1+n)*(1+g))-k
transolve(ss, k,sm.S.Reals)
```

```python
help(sm.symbols)
```

    Help on function symbols in module sympy.core.symbol:

    symbols(names, **args)
        Transform strings into instances of :class:`Symbol` class.

        :func:`symbols` function returns a sequence of symbols with names taken
        from ``names`` argument, which can be a comma or whitespace delimited
        string, or a sequence of strings::

            >>> from sympy import symbols, Function

            >>> x, y, z = symbols('x,y,z')
            >>> a, b, c = symbols('a b c')

        The type of output is dependent on the properties of input arguments::

            >>> symbols('x')
            x
            >>> symbols('x,')
            (x,)
            >>> symbols('x,y')
            (x, y)
            >>> symbols(('a', 'b', 'c'))
            (a, b, c)
            >>> symbols(['a', 'b', 'c'])
            [a, b, c]
            >>> symbols({'a', 'b', 'c'})
            {a, b, c}

        If an iterable container is needed for a single symbol, set the ``seq``
        argument to ``True`` or terminate the symbol name with a comma::

            >>> symbols('x', seq=True)
            (x,)

        To reduce typing, range syntax is supported to create indexed symbols.
        Ranges are indicated by a colon and the type of range is determined by
        the character to the right of the colon. If the character is a digit
        then all contiguous digits to the left are taken as the nonnegative
        starting value (or 0 if there is no digit left of the colon) and all
        contiguous digits to the right are taken as 1 greater than the ending
        value::

            >>> symbols('x:10')
            (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9)

            >>> symbols('x5:10')
            (x5, x6, x7, x8, x9)
            >>> symbols('x5(:2)')
            (x50, x51)

            >>> symbols('x5:10,y:5')
            (x5, x6, x7, x8, x9, y0, y1, y2, y3, y4)

            >>> symbols(('x5:10', 'y:5'))
            ((x5, x6, x7, x8, x9), (y0, y1, y2, y3, y4))

        If the character to the right of the colon is a letter, then the single
        letter to the left (or 'a' if there is none) is taken as the start
        and all characters in the lexicographic range *through* the letter to
        the right are used as the range::

            >>> symbols('x:z')
            (x, y, z)
            >>> symbols('x:c')  # null range
            ()
            >>> symbols('x(:c)')
            (xa, xb, xc)

            >>> symbols(':c')
            (a, b, c)

            >>> symbols('a:d, x:z')
            (a, b, c, d, x, y, z)

            >>> symbols(('a:d', 'x:z'))
            ((a, b, c, d), (x, y, z))

        Multiple ranges are supported; contiguous numerical ranges should be
        separated by parentheses to disambiguate the ending number of one
        range from the starting number of the next::

            >>> symbols('x:2(1:3)')
            (x01, x02, x11, x12)
            >>> symbols(':3:2')  # parsing is from left to right
            (00, 01, 10, 11, 20, 21)

        Only one pair of parentheses surrounding ranges are removed, so to
        include parentheses around ranges, double them. And to include spaces,
        commas, or colons, escape them with a backslash::

            >>> symbols('x((a:b))')
            (x(a), x(b))
            >>> symbols(r'x(:1\,:2)')  # or r'x((:1)\,(:2))'
            (x(0,0), x(0,1))

        All newly created symbols have assumptions set according to ``args``::

            >>> a = symbols('a', integer=True)
            >>> a.is_integer
            True

            >>> x, y, z = symbols('x,y,z', real=True)
            >>> x.is_real and y.is_real and z.is_real
            True

        Despite its name, :func:`symbols` can create symbol-like objects like
        instances of Function or Wild classes. To achieve this, set ``cls``
        keyword argument to the desired type::

            >>> symbols('f,g,h', cls=Function)
            (f, g, h)

            >>> type(_[0])
            <class 'sympy.core.function.UndefinedFunction'>

**Question B:** Turn you solution into a Python function called as `ss_func(s,g,n,delta,alpha)`.

```python
ss_func = sm.lambdify((s,g,n,delta,alpha),sm.solve(transition,k)[0])
ss_func(0.2,0.02,0.01,0.1,1/3)
```

**Answer:**

```python
ss_func = sm.lambdify((s,g,n,delta,alpha),kss)
```

```python
ss_func(0.2,0.02,0.01,0.1,1/3)
```

**Question C**: Find the steady state numerically.

```python
s = 0.2
g = 0.02
n = 0.01
alpha = 1/3
delta = 0.1

params = {'s':0.2,'g':0.02,'n':0.01,'alpha':1/3,'delta':0.1}

transitioneq = lambda k,parms: (params['s']*k**params['alpha']+(1-params['delta'])*k)/((1+params['n'])*(1+params['g']))

def ss_num(transitioneq,params,guess=2,rtol=1.0000000000000001e-010,
           atol=1e-12,jump=1,max_iter=2000, printit=True):
    '''
    Finds the steady state(ss) for the transitionseq:
    args:
        transitioneq (function) : Transition equation to find ss for
        params           (dict) : A dictionary with parameters values for the transition equation
        guees           (float) : Inital guess
        rtol,atol       (float) : Relative and absolut tolerance for np.isclose
        jump            (float) : relative size of each jump when making a new guess
        max_iter          (int) : Maximum number of iterations
        prinit           (Bool) : Whether or not to print the results

    '''
    k1= transitioneq(guess,params)
    cond = np.isclose(k1,guess,rtol=rtol,atol=atol)
    i = 0
    while not cond:
        i+=1
        if i>max_iter:
            print('max iterations reached')
            break
            return None
    # Make new guess by guessing the way of the evolution of k, so if kt+1>kt the new guess of k becomes larger
        guess += jump*(k1-guess)
        k1= transitioneq(guess,params)
        cond = np.isclose(k1,guess,rtol=rtol,atol=atol) # Check if kt+1=kt

    if printit:
        print(f'K_star is {guess:.20f}')
        print(f'Check:    {k1:.20f}')

    return guess

kstar = ss_num(transitioneq,params)
```

    K_star is 1.90383154138895749341
    Check:    1.90383154120716469038

```python
#plot solution
ks = np.linspace(0,4,10000)
ks1 = [transitioneq(k,params) for k in ks]

fig = plt.figure()
ax = fig.add_subplot(1,1,1)

ax.plot(ks,ks1,label='$k_{t+1}(k_{t})$')
ax.plot(ks,ks,ls='--',color='black',label='$k_{t}=k_{t+1}$')
ax.plot(kstar,transitioneq(kstar,params),ls='',marker='o',color='blue',label='$k^{*}$')

ax.grid(True)
plt.legend(loc="upper left")
fig.tight_layout()
```

**Answer:**

```python
f = lambda k: k**alpha
obj_kss = lambda kss: kss - (s*f(kss) + (1-delta)*kss)/((1+g)*(1+n))
result = optimize.root_scalar(obj_kss,bracket=[0.1,100],method='brentq')

print('the steady state for k is',result.root)
```

    the steady state for k is 1.9038315392313185

**Question D:** Now assume the production function is CES. Find the steady state for $k$ for the various values of $\beta$ shown below.

$$
F(K_{t},A_{t}L_{t})=(\alpha K_{t}^{\beta}+(1-\alpha)(A_{t}L_{t})^{\beta})^{\frac{1}{\beta}}\Rightarrow f(\tilde{k}_{t})=(\alpha\tilde{k}_{t}^{\beta}+(1-\alpha))^{\frac{1}{\beta}}
$$

```python
betas = [-0.5,-0.25,-0.1,-0.05,0.05,0.1,0.25,0.5]

f = lambda k,params: (params['alpha']*k**params['beta']+(1-params['alpha']))**(1/params['beta'])
trans_eq = lambda kss,params:(params['s']*f(kss,params) + (1-params['delta'])*kss)/((1+params['g'])*(1+params['n']))

k_stars = np.empty(len(betas))
for i,beta in enumerate(betas):
    params['beta']=beta
    k_stars[i] = ss_num(trans_eq,params, printit=False)

    print(f'When beta= {beta:5.2f} the steady state for k is {k_stars[i]:.15f}')
```

    When beta= -0.50 the steady state for k is 1.847129701923042
    When beta= -0.25 the steady state for k is 1.873383264875734
    When beta= -0.10 the steady state for k is 1.891085641935206
    When beta= -0.05 the steady state for k is 1.897358104639712
    When beta=  0.05 the steady state for k is 1.910515975173142
    When beta=  0.10 the steady state for k is 1.917422135156361
    When beta=  0.25 the steady state for k is 1.939590275709855
    When beta=  0.50 the steady state for k is 1.982233502409577

```python

for beta,k_star in zip(betas,k_stars):
    params['beta']=beta
    ks = np.linspace(1,3,10000)
    ks1 = [trans_eq(k,params) for k in ks]

    fig = plt.figure()
    ax = fig.add_subplot(1,1,1)

    ax.plot(ks,ks1,label='$k_{t+1}(k_{t})$')
    ax.plot(ks,ks,ls='--',color='black',label='$k_{t}=k_{t+1}$')
    ax.plot(k_star,transitioneq(k_star,params),ls='',marker='o',color='blue',label='$k^{*}$')

    ax.grid(True)
    plt.title(r'$\beta=$'+f'{beta}')
    plt.legend(loc="upper left")
    fig.tight_layout()
```

```python
for beta in betas:
    f = lambda k: (alpha*k**beta + (1-alpha))**(1/beta)
    obj_kss = lambda kss: kss - (s*f(kss) + (1-delta)*kss)/((1+g)*(1+n))
    result = optimize.root_scalar(obj_kss,bracket=[0.1,100],method='brentq')
    print(f'for beta = {beta:.3f} the steady state for k is',result.root)
```

    for beta = -0.500 the steady state for k is 1.8471297000972984
    for beta = -0.250 the steady state for k is 1.873383262758588
    for beta = -0.100 the steady state for k is 1.8910856397655083
    for beta = -0.050 the steady state for k is 1.8973581025712736
    for beta = 0.050 the steady state for k is 1.9105159729244352
    for beta = 0.100 the steady state for k is 1.917422132817728
    for beta = 0.250 the steady state for k is 1.9395902733676993
    for beta = 0.500 the steady state for k is 1.9822334997701418


```python
# Install required packages.
%%capture
!pip install torch-scatter -f https://data.pyg.org/whl/torch-1.10.0+cu111.html
!pip install torch-sparse -f https://data.pyg.org/whl/torch-1.10.0+cu111.html
!pip install torch-geometric
!pip install -q git+https://github.com/snap-stanford/deepsnap.git
!pip install -U -q PyDrive
```

# Implementing a Recommender System using LightGCN

In this colab, we explain how to set up a graph recommender system using the [LighGCN](https://arxiv.org/abs/2002.02126) model. Specifically, we apply LightGCN to a movie recommendation task using [PyTorch](https://pytorch.org/) and [PyTorch Geometric](https://pytorch-geometric.readthedocs.io/en/latest/).

We use the [MovieLens](https://grouplens.org/datasets/movielens/) (*small*) dataset which has 100,000 ratings applied to 9,000 movies by 600 users.

```python
# import required modules
import random
from tqdm import tqdm
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

import torch
from torch import nn, optim, Tensor

from torch_sparse import SparseTensor, matmul

from torch_geometric.utils import structured_negative_sampling
from torch_geometric.data import download_url, extract_zip
from torch_geometric.nn.conv.gcn_conv import gcn_norm
from torch_geometric.nn.conv import MessagePassing
from torch_geometric.typing import Adj
```

# Loading the Dataset

We load the dataset and set ratings >=4 on a 0.5 ~ 5 scale as an edge between users and movies.

We split the edges of the graph using a 80/10/10 train/validation/test split.

```python
# download the dataset
url = 'https://files.grouplens.org/datasets/movielens/ml-latest-small.zip'
extract_zip(download_url(url, '.'), '.')

movie_path = './ml-latest-small/movies.csv'
rating_path = './ml-latest-small/ratings.csv'
```

    Downloading https://files.grouplens.org/datasets/movielens/ml-latest-small.zip
    Extracting ./ml-latest-small.zip

```python
# load user and movie nodes
def load_node_csv(path, index_col):
    """Loads csv containing node information

    Args:
        path (str): path to csv file
        index_col (str): column name of index column

    Returns:
        dict: mapping of csv row to node id
    """
    df = pd.read_csv(path, index_col=index_col)
    mapping = {index: i for i, index in enumerate(df.index.unique())}
    return mapping

user_mapping = load_node_csv(rating_path, index_col='userId')
movie_mapping = load_node_csv(movie_path, index_col='movieId')
```

```python
# load edges between users and movies
def load_edge_csv(path, src_index_col, src_mapping, dst_index_col, dst_mapping, link_index_col, rating_threshold=4):
    """Loads csv containing edges between users and items

    Args:
        path (str): path to csv file
        src_index_col (str): column name of users
        src_mapping (dict): mapping between row number and user id
        dst_index_col (str): column name of items
        dst_mapping (dict): mapping between row number and item id
        link_index_col (str): column name of user item interaction
        rating_threshold (int, optional): Threshold to determine positivity of edge. Defaults to 4.

    Returns:
        torch.Tensor: 2 by N matrix containing the node ids of N user-item edges
    """
    df = pd.read_csv(path)
    edge_index = None
    src = [src_mapping[index] for index in df[src_index_col]]
    dst = [dst_mapping[index] for index in df[dst_index_col]]
    edge_attr = torch.from_numpy(df[link_index_col].values).view(-1, 1).to(torch.long) >= rating_threshold

    edge_index = [[], []]
    for i in range(edge_attr.shape[0]):
        if edge_attr[i]:
            edge_index[0].append(src[i])
            edge_index[1].append(dst[i])

    return torch.tensor(edge_index)

edge_index = load_edge_csv(
    rating_path,
    src_index_col='userId',
    src_mapping=user_mapping,
    dst_index_col='movieId',
    dst_mapping=movie_mapping,
    link_index_col='rating',
    rating_threshold=4,
)
```

```python
# split the edges of the graph using a 80/10/10 train/validation/test split
num_users, num_movies = len(user_mapping), len(movie_mapping)
num_interactions = edge_index.shape[1]
all_indices = [i for i in range(num_interactions)]

train_indices, test_indices = train_test_split(
    all_indices, test_size=0.2, random_state=1)
val_indices, test_indices = train_test_split(
    test_indices, test_size=0.5, random_state=1)

train_edge_index = edge_index[:, train_indices]
val_edge_index = edge_index[:, val_indices]
test_edge_index = edge_index[:, test_indices]
```

```python
# convert edge indices into Sparse Tensors: https://pytorch-geometric.readthedocs.io/en/latest/notes/sparse_tensor.html
train_sparse_edge_index = SparseTensor(row=train_edge_index[0], col=train_edge_index[1], sparse_sizes=(
    num_users + num_movies, num_users + num_movies))
val_sparse_edge_index = SparseTensor(row=val_edge_index[0], col=val_edge_index[1], sparse_sizes=(
    num_users + num_movies, num_users + num_movies))
test_sparse_edge_index = SparseTensor(row=test_edge_index[0], col=test_edge_index[1], sparse_sizes=(
    num_users + num_movies, num_users + num_movies))
```

```python
# function which random samples a mini-batch of positive and negative samples
def sample_mini_batch(batch_size, edge_index):
    """Randomly samples indices of a minibatch given an adjacency matrix

    Args:
        batch_size (int): minibatch size
        edge_index (torch.Tensor): 2 by N list of edges

    Returns:
        tuple: user indices, positive item indices, negative item indices
    """
    edges = structured_negative_sampling(edge_index)
    edges = torch.stack(edges, dim=0)
    indices = random.choices(
        [i for i in range(edges[0].shape[0])], k=batch_size)
    batch = edges[:, indices]
    user_indices, pos_item_indices, neg_item_indices = batch[0], batch[1], batch[2]
    return user_indices, pos_item_indices, neg_item_indices
```

# Implementing LightGCN

## Light Graph Convolution
Between each layer, LightGCN uses the following propagation rule for user and item embeddings.

\begin{equation}
e_u^{(k+1)} = \sum_{i \in N_u} \frac{1}{\sqrt{|N_u|}\sqrt{|N_i|}} e_i^{(k)} \quad e_i^{(k+1)} = \sum_{u \in N_i} \frac{1}{\sqrt{|N_i|}\sqrt{|N_u|}} e_u^{(k)}
\end{equation}

$N_u$: the set of all neighbors of user $u$ (items liked by $u$)

$N_i$: the set of all neighbors of item $i$ (users who liked $i$)

$e_u^{(k)}$ : k-th layer user embedding

$e_i^{(k)}$ : k-th layer item embedding

## Layer Combination and Model Prediction
The only trainable parameters of LightGCN are the 0-th layer embeddings $e_u^{(0)}$ and $e_i^{(0)}$ for each user and item. We combine the embeddings obtained at each layer of propagation to form the final embeddings for all user and item, $e_u$ and $e_i$ via the follwing equation.

\begin{equation}
e_u = \sum_{k = 0}^K \alpha_k e_u^{(k)} \quad e_i = \sum_{k = 0}^K \alpha_k e_i^{(k)}
\end{equation}

$\alpha_k$ : hyperparameter which weights the contribution of the k-th layer embedding to the final embedding

The model prediction is obtained by taking the inner product of the final user and item embeddings.

\begin{equation}
\hat{y}_{ui} = e_u^Te_i
\end{equation}

## Matrix Form
In our implementation, we utilize the matrix form of LightGCN. We perform multi-scale diffusion to obtain the final embedding, which sums embeddings diffused across multi-hop scales.

\begin{equation}
E^{(K)} = \alpha_0 E^{(0)} + \alpha_1 \tilde{A}^1 E^{(0)} + \alpha_2 \tilde{A}^2 E^{(0)} + \cdot \cdot \cdot + \alpha_K \tilde{A}^K \tilde{A} E^{(0)}
\end{equation}

$E^{(0)} \in \mathcal{R}^{(M + N)} \times T$ : stacked initial item and user embeddings where $M$, $N$, and $T$ denote the number of users, number of items, and the dimension of each embedding respectively

$\tilde{A} = D^{-\frac{1}{2}}AD^{-\frac{1}{2}}$ : symmetrically normalized adjacency matrix

```python
# defines LightGCN model
class LightGCN(MessagePassing):
    """LightGCN Model as proposed in https://arxiv.org/abs/2002.02126
    """

    def __init__(self, num_users, num_items, embedding_dim=64, K=3, add_self_loops=False):
        """Initializes LightGCN Model

        Args:
            num_users (int): Number of users
            num_items (int): Number of items
            embedding_dim (int, optional): Dimensionality of embeddings. Defaults to 8.
            K (int, optional): Number of message passing layers. Defaults to 3.
            add_self_loops (bool, optional): Whether to add self loops for message passing. Defaults to False.
        """
        super().__init__()
        self.num_users, self.num_items = num_users, num_items
        self.embedding_dim, self.K = embedding_dim, K
        self.add_self_loops = add_self_loops

        self.users_emb = nn.Embedding(
            num_embeddings=self.num_users, embedding_dim=self.embedding_dim) # e_u^0
        self.items_emb = nn.Embedding(
            num_embeddings=self.num_items, embedding_dim=self.embedding_dim) # e_i^0

        nn.init.normal_(self.users_emb.weight, std=0.1)
        nn.init.normal_(self.items_emb.weight, std=0.1)

    def forward(self, edge_index: SparseTensor):
        """Forward propagation of LightGCN Model.

        Args:
            edge_index (SparseTensor): adjacency matrix

        Returns:
            tuple (Tensor): e_u_k, e_u_0, e_i_k, e_i_0
        """
        # compute \tilde{A}: symmetrically normalized adjacency matrix
        edge_index_norm = gcn_norm(
            edge_index, add_self_loops=self.add_self_loops)

        emb_0 = torch.cat([self.users_emb.weight, self.items_emb.weight]) # E^0
        embs = [emb_0]
        emb_k = emb_0

        # multi-scale diffusion
        for i in range(self.K):
            emb_k = self.propagate(edge_index_norm, x=emb_k)
            embs.append(emb_k)

        embs = torch.stack(embs, dim=1)
        emb_final = torch.mean(embs, dim=1) # E^K

        users_emb_final, items_emb_final = torch.split(
            emb_final, [self.num_users, self.num_items]) # splits into e_u^K and e_i^K

        # returns e_u^K, e_u^0, e_i^K, e_i^0
        return users_emb_final, self.users_emb.weight, items_emb_final, self.items_emb.weight

    def message(self, x_j: Tensor) -> Tensor:
        return x_j

    def message_and_aggregate(self, adj_t: SparseTensor, x: Tensor) -> Tensor:
        # computes \tilde{A} @ x
        return matmul(adj_t, x)

model = LightGCN(num_users, num_movies)
```

# Loss Function

We utilize a Bayesian Personalized Ranking (BPR) loss, a pairwise objective which encourages the predictions of positive samples to be higher than negative samples for each user.

\begin{equation}
L_{BPR} = -\sum_{u = 1}^M \sum_{i \in N_u} \sum_{j \notin N_u} \ln{\sigma(\hat{y}_{ui} - \hat{y}_{uj})} + \lambda ||E^{(0)}||^2
\end{equation}

$\hat{y}_{uj}$: predicted score of a positive sample

$\hat{y}_{uj}$: predicted score of a negative sample

$\lambda$: hyperparameter which controls the L2 regularization strength

```python
def bpr_loss(users_emb_final, users_emb_0, pos_items_emb_final, pos_items_emb_0, neg_items_emb_final, neg_items_emb_0, lambda_val):
    """Bayesian Personalized Ranking Loss as described in https://arxiv.org/abs/1205.2618

    Args:
        users_emb_final (torch.Tensor): e_u_k
        users_emb_0 (torch.Tensor): e_u_0
        pos_items_emb_final (torch.Tensor): positive e_i_k
        pos_items_emb_0 (torch.Tensor): positive e_i_0
        neg_items_emb_final (torch.Tensor): negative e_i_k
        neg_items_emb_0 (torch.Tensor): negative e_i_0
        lambda_val (float): lambda value for regularization loss term

    Returns:
        torch.Tensor: scalar bpr loss value
    """
    reg_loss = lambda_val * (users_emb_0.norm(2).pow(2) +
                             pos_items_emb_0.norm(2).pow(2) +
                             neg_items_emb_0.norm(2).pow(2)) # L2 loss

    pos_scores = torch.mul(users_emb_final, pos_items_emb_final)
    pos_scores = torch.sum(pos_scores, dim=-1) # predicted scores of positive samples
    neg_scores = torch.mul(users_emb_final, neg_items_emb_final)
    neg_scores = torch.sum(neg_scores, dim=-1) # predicted scores of negative samples

    loss = -torch.mean(torch.nn.functional.softplus(pos_scores - neg_scores)) + reg_loss

    return loss
```

# Evaluation Metrics

We evalaluate our model using the following metrics

\begin{equation}
\text{Recall} = \frac{TP}{TP + FP}
\end{equation}

\begin{equation}
\text{Precision} = \frac{TP}{TP + FN}
\end{equation}

**Dicounted Cumulative Gain (DCG)** at rank position p is defined as:

\begin{equation}
\text{DCG}_\text{p} = \sum_{i = 1}^p \frac{2^{rel_i} - 1}{\log_2{(i + 1)}}
\end{equation}

p: a particular rank position

$rel_i \in \{0, 1\}$ : graded relevance of the result at position $i$

**Idealised Dicounted Cumulative Gain (IDCG)**, namely the maximum possible DCG, at rank position $p$ is defined as:

\begin{equation}
\text{IDCG}_\text{p} = \sum_{i = 1}^{|REL_p|} \frac{2^{rel_i} - 1}{\log_2{(i + 1)}}
\end{equation}

$|REL_p|$ : list of items ordered by their relevance up to position p

**Normalized Dicounted Cumulative Gain (NDCG)** at rank position $p$ is defined as:

\begin{equation}
\text{nDCG}_\text{p} = \frac{\text{DCG}_p}{\text{nDCG}_p}
\end{equation}

Specifically, we use the metrics recall@K, precision@K, and NDCG@K. @K indicates that these metrics are computed on the top K recommendations.

```python
# helper function to get N_u
def get_user_positive_items(edge_index):
    """Generates dictionary of positive items for each user

    Args:
        edge_index (torch.Tensor): 2 by N list of edges

    Returns:
        dict: dictionary of positive items for each user
    """
    user_pos_items = {}
    for i in range(edge_index.shape[1]):
        user = edge_index[0][i].item()
        item = edge_index[1][i].item()
        if user not in user_pos_items:
            user_pos_items[user] = []
        user_pos_items[user].append(item)
    return user_pos_items
```

```python
# computes recall@K and precision@K
def RecallPrecision_ATk(groundTruth, r, k):
    """Computers recall @ k and precision @ k

    Args:
        groundTruth (list): list of lists containing highly rated items of each user
        r (list): list of lists indicating whether each top k item recommended to each user
            is a top k ground truth item or not
        k (intg): determines the top k items to compute precision and recall on

    Returns:
        tuple: recall @ k, precision @ k
    """
    num_correct_pred = torch.sum(r, dim=-1)  # number of correctly predicted items per user
    # number of items liked by each user in the test set
    user_num_liked = torch.Tensor([len(groundTruth[i])
                                  for i in range(len(groundTruth))])
    recall = torch.mean(num_correct_pred / user_num_liked)
    precision = torch.mean(num_correct_pred) / k
    return recall.item(), precision.item()
```

```python
# computes NDCG@K
def NDCGatK_r(groundTruth, r, k):
    """Computes Normalized Discounted Cumulative Gain (NDCG) @ k

    Args:
        groundTruth (list): list of lists containing highly rated items of each user
        r (list): list of lists indicating whether each top k item recommended to each user
            is a top k ground truth item or not
        k (int): determines the top k items to compute ndcg on

    Returns:
        float: ndcg @ k
    """
    assert len(r) == len(groundTruth)

    test_matrix = torch.zeros((len(r), k))

    for i, items in enumerate(groundTruth):
        length = min(len(items), k)
        test_matrix[i, :length] = 1
    max_r = test_matrix
    idcg = torch.sum(max_r * 1. / torch.log2(torch.arange(2, k + 2)), axis=1)
    dcg = r * (1. / torch.log2(torch.arange(2, k + 2)))
    dcg = torch.sum(dcg, axis=1)
    idcg[idcg == 0.] = 1.
    ndcg = dcg / idcg
    ndcg[torch.isnan(ndcg)] = 0.
    return torch.mean(ndcg).item()
```

```python
# wrapper function to get evaluation metrics
def get_metrics(model, edge_index, exclude_edge_indices, k):
    """Computes the evaluation metrics: recall, precision, and ndcg @ k

    Args:
        model (LighGCN): lightgcn model
        edge_index (torch.Tensor): 2 by N list of edges for split to evaluate
        exclude_edge_indices ([type]): 2 by N list of edges for split to discount from evaluation
        k (int): determines the top k items to compute metrics on

    Returns:
        tuple: recall @ k, precision @ k, ndcg @ k
    """
    user_embedding = model.users_emb.weight
    item_embedding = model.items_emb.weight

    # get ratings between every user and item - shape is num users x num movies
    rating = torch.matmul(user_embedding, item_embedding.T)

    for exclude_edge_index in exclude_edge_indices:
        # gets all the positive items for each user from the edge index
        user_pos_items = get_user_positive_items(exclude_edge_index)
        # get coordinates of all edges to exclude
        exclude_users = []
        exclude_items = []
        for user, items in user_pos_items.items():
            exclude_users.extend([user] * len(items))
            exclude_items.extend(items)

        # set ratings of excluded edges to large negative value
        rating[exclude_users, exclude_items] = -(1 << 10)

    # get the top k recommended items for each user
    _, top_K_items = torch.topk(rating, k=k)

    # get all unique users in evaluated split
    users = edge_index[0].unique()

    test_user_pos_items = get_user_positive_items(edge_index)

    # convert test user pos items dictionary into a list
    test_user_pos_items_list = [
        test_user_pos_items[user.item()] for user in users]

    # determine the correctness of topk predictions
    r = []
    for user in users:
        ground_truth_items = test_user_pos_items[user.item()]
        label = list(map(lambda x: x in ground_truth_items, top_K_items[user]))
        r.append(label)
    r = torch.Tensor(np.array(r).astype('float'))

    recall, precision = RecallPrecision_ATk(test_user_pos_items_list, r, k)
    ndcg = NDCGatK_r(test_user_pos_items_list, r, k)

    return recall, precision, ndcg
```

```python
# wrapper function to evaluate model
def evaluation(model, edge_index, sparse_edge_index, exclude_edge_indices, k, lambda_val):
    """Evaluates model loss and metrics including recall, precision, ndcg @ k

    Args:
        model (LighGCN): lightgcn model
        edge_index (torch.Tensor): 2 by N list of edges for split to evaluate
        sparse_edge_index (sparseTensor): sparse adjacency matrix for split to evaluate
        exclude_edge_indices ([type]): 2 by N list of edges for split to discount from evaluation
        k (int): determines the top k items to compute metrics on
        lambda_val (float): determines lambda for bpr loss

    Returns:
        tuple: bpr loss, recall @ k, precision @ k, ndcg @ k
    """
    # get embeddings
    users_emb_final, users_emb_0, items_emb_final, items_emb_0 = model.forward(
        sparse_edge_index)
    edges = structured_negative_sampling(
        edge_index, contains_neg_self_loops=False)
    user_indices, pos_item_indices, neg_item_indices = edges[0], edges[1], edges[2]
    users_emb_final, users_emb_0 = users_emb_final[user_indices], users_emb_0[user_indices]
    pos_items_emb_final, pos_items_emb_0 = items_emb_final[
        pos_item_indices], items_emb_0[pos_item_indices]
    neg_items_emb_final, neg_items_emb_0 = items_emb_final[
        neg_item_indices], items_emb_0[neg_item_indices]

    loss = bpr_loss(users_emb_final, users_emb_0, pos_items_emb_final, pos_items_emb_0,
                    neg_items_emb_final, neg_items_emb_0, lambda_val).item()

    recall, precision, ndcg = get_metrics(
        model, edge_index, exclude_edge_indices, k)

    return loss, recall, precision, ndcg
```

# Training

Your test set performance should be in line with the following (*K=20*):

*Recall@K: 0.13, Precision@K: 0.045, NDCG@K: 0.10*

```python
# define contants
ITERATIONS = 10000
BATCH_SIZE = 1024
LR = 1e-3
ITERS_PER_EVAL = 200
ITERS_PER_LR_DECAY = 200
K = 20
LAMBDA = 1e-6
```

```python
# setup
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device {device}.")

model = model.to(device)
model.train()

optimizer = optim.Adam(model.parameters(), lr=LR)
scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)

edge_index = edge_index.to(device)
train_edge_index = train_edge_index.to(device)
train_sparse_edge_index = train_sparse_edge_index.to(device)

val_edge_index = val_edge_index.to(device)
val_sparse_edge_index = val_sparse_edge_index.to(device)
```

    Using device cuda.

```python
# training loop

train_losses = []
val_losses = []

for iter in range(ITERATIONS):
    # forward propagation
    users_emb_final, users_emb_0, items_emb_final, items_emb_0 = model.forward(
        train_sparse_edge_index)

    # mini batching
    user_indices, pos_item_indices, neg_item_indices = sample_mini_batch(
        BATCH_SIZE, train_edge_index)
    user_indices, pos_item_indices, neg_item_indices = user_indices.to(
        device), pos_item_indices.to(device), neg_item_indices.to(device)
    users_emb_final, users_emb_0 = users_emb_final[user_indices], users_emb_0[user_indices]
    pos_items_emb_final, pos_items_emb_0 = items_emb_final[
        pos_item_indices], items_emb_0[pos_item_indices]
    neg_items_emb_final, neg_items_emb_0 = items_emb_final[
        neg_item_indices], items_emb_0[neg_item_indices]

    # loss computation
    train_loss = bpr_loss(users_emb_final, users_emb_0, pos_items_emb_final,
                          pos_items_emb_0, neg_items_emb_final, neg_items_emb_0, LAMBDA)

    optimizer.zero_grad()
    train_loss.backward()
    optimizer.step()

    if iter % ITERS_PER_EVAL == 0:
        model.eval()
        val_loss, recall, precision, ndcg = evaluation(
            model, val_edge_index, val_sparse_edge_index, [train_edge_index], K, LAMBDA)
        print(f"[Iteration {iter}/{ITERATIONS}] train_loss: {round(train_loss.item(), 5)}, val_loss: {round(val_loss, 5)}, val_recall@{K}: {round(recall, 5)}, val_precision@{K}: {round(precision, 5)}, val_ndcg@{K}: {round(ndcg, 5)}")
        train_losses.append(train_loss.item())
        val_losses.append(val_loss)
        model.train()

    if iter % ITERS_PER_LR_DECAY == 0 and iter != 0:
        scheduler.step()
```

    [Iteration 0/10000] train_loss: -0.69112, val_loss: -0.68374, val_recall@20: 0.00161, val_precision@20: 0.0009, val_ndcg@20: 0.00125
    [Iteration 200/10000] train_loss: -0.70072, val_loss: -0.6902, val_recall@20: 0.05163, val_precision@20: 0.01618, val_ndcg@20: 0.03836
    [Iteration 400/10000] train_loss: -0.89317, val_loss: -0.82984, val_recall@20: 0.14365, val_precision@20: 0.04005, val_ndcg@20: 0.10178
    [Iteration 600/10000] train_loss: -1.79995, val_loss: -1.54339, val_recall@20: 0.14435, val_precision@20: 0.04259, val_ndcg@20: 0.10322
    [Iteration 800/10000] train_loss: -3.49128, val_loss: -2.8681, val_recall@20: 0.14737, val_precision@20: 0.04476, val_ndcg@20: 0.10516
    [Iteration 1000/10000] train_loss: -5.57447, val_loss: -4.47435, val_recall@20: 0.14575, val_precision@20: 0.04485, val_ndcg@20: 0.10477
    [Iteration 1200/10000] train_loss: -7.60937, val_loss: -6.33453, val_recall@20: 0.14457, val_precision@20: 0.04512, val_ndcg@20: 0.10412
    [Iteration 1400/10000] train_loss: -9.72321, val_loss: -8.18129, val_recall@20: 0.14758, val_precision@20: 0.04548, val_ndcg@20: 0.10508
    [Iteration 1600/10000] train_loss: -12.35682, val_loss: -10.17684, val_recall@20: 0.14414, val_precision@20: 0.04476, val_ndcg@20: 0.10299
    [Iteration 1800/10000] train_loss: -14.44013, val_loss: -12.2623, val_recall@20: 0.14556, val_precision@20: 0.04503, val_ndcg@20: 0.1034
    [Iteration 2000/10000] train_loss: -17.09245, val_loss: -14.23878, val_recall@20: 0.14424, val_precision@20: 0.04485, val_ndcg@20: 0.10373
    [Iteration 2200/10000] train_loss: -19.21111, val_loss: -15.98005, val_recall@20: 0.14526, val_precision@20: 0.0453, val_ndcg@20: 0.10443
    [Iteration 2400/10000] train_loss: -22.51374, val_loss: -18.14113, val_recall@20: 0.14457, val_precision@20: 0.04494, val_ndcg@20: 0.10415
    [Iteration 2600/10000] train_loss: -24.43242, val_loss: -20.06738, val_recall@20: 0.14579, val_precision@20: 0.04512, val_ndcg@20: 0.10457
    [Iteration 2800/10000] train_loss: -26.27654, val_loss: -21.99173, val_recall@20: 0.14578, val_precision@20: 0.04521, val_ndcg@20: 0.10468
    [Iteration 3000/10000] train_loss: -30.05969, val_loss: -23.61742, val_recall@20: 0.14754, val_precision@20: 0.04539, val_ndcg@20: 0.10548
    [Iteration 3200/10000] train_loss: -30.67489, val_loss: -25.52533, val_recall@20: 0.14602, val_precision@20: 0.0453, val_ndcg@20: 0.1051
    [Iteration 3400/10000] train_loss: -34.77532, val_loss: -27.26008, val_recall@20: 0.14733, val_precision@20: 0.04521, val_ndcg@20: 0.10517
    [Iteration 3600/10000] train_loss: -35.58294, val_loss: -28.98993, val_recall@20: 0.1479, val_precision@20: 0.0453, val_ndcg@20: 0.1054
    [Iteration 3800/10000] train_loss: -37.35663, val_loss: -30.62263, val_recall@20: 0.14661, val_precision@20: 0.04539, val_ndcg@20: 0.10509
    [Iteration 4000/10000] train_loss: -39.28416, val_loss: -32.10266, val_recall@20: 0.14753, val_precision@20: 0.04539, val_ndcg@20: 0.10542
    [Iteration 4200/10000] train_loss: -41.50747, val_loss: -33.64888, val_recall@20: 0.14636, val_precision@20: 0.04539, val_ndcg@20: 0.10503
    [Iteration 4400/10000] train_loss: -42.93468, val_loss: -34.81876, val_recall@20: 0.1469, val_precision@20: 0.04548, val_ndcg@20: 0.10519
    [Iteration 4600/10000] train_loss: -45.86227, val_loss: -36.24535, val_recall@20: 0.15224, val_precision@20: 0.04593, val_ndcg@20: 0.10648
    [Iteration 4800/10000] train_loss: -45.33321, val_loss: -37.53897, val_recall@20: 0.14983, val_precision@20: 0.04575, val_ndcg@20: 0.10567
    [Iteration 5000/10000] train_loss: -47.07096, val_loss: -38.74474, val_recall@20: 0.14741, val_precision@20: 0.04575, val_ndcg@20: 0.1057
    [Iteration 5200/10000] train_loss: -50.03451, val_loss: -40.00531, val_recall@20: 0.14717, val_precision@20: 0.04566, val_ndcg@20: 0.10548
    [Iteration 5400/10000] train_loss: -50.09203, val_loss: -41.62997, val_recall@20: 0.14629, val_precision@20: 0.04557, val_ndcg@20: 0.10514
    [Iteration 5600/10000] train_loss: -53.07259, val_loss: -42.51913, val_recall@20: 0.14707, val_precision@20: 0.04566, val_ndcg@20: 0.10529
    [Iteration 5800/10000] train_loss: -52.63025, val_loss: -43.57563, val_recall@20: 0.14765, val_precision@20: 0.04593, val_ndcg@20: 0.10562
    [Iteration 6000/10000] train_loss: -52.31987, val_loss: -44.17801, val_recall@20: 0.14936, val_precision@20: 0.0462, val_ndcg@20: 0.10616
    [Iteration 6200/10000] train_loss: -54.28507, val_loss: -45.68126, val_recall@20: 0.14887, val_precision@20: 0.04584, val_ndcg@20: 0.10602
    [Iteration 6400/10000] train_loss: -58.80605, val_loss: -46.39964, val_recall@20: 0.14909, val_precision@20: 0.04602, val_ndcg@20: 0.10593
    [Iteration 6600/10000] train_loss: -58.40558, val_loss: -47.90079, val_recall@20: 0.14895, val_precision@20: 0.04593, val_ndcg@20: 0.10596
    [Iteration 6800/10000] train_loss: -58.89854, val_loss: -48.89202, val_recall@20: 0.14922, val_precision@20: 0.04611, val_ndcg@20: 0.1059
    [Iteration 7000/10000] train_loss: -61.2205, val_loss: -49.39697, val_recall@20: 0.14898, val_precision@20: 0.04593, val_ndcg@20: 0.10575
    [Iteration 7200/10000] train_loss: -62.91126, val_loss: -49.24725, val_recall@20: 0.14915, val_precision@20: 0.04602, val_ndcg@20: 0.10576
    [Iteration 7400/10000] train_loss: -62.67969, val_loss: -50.60665, val_recall@20: 0.14916, val_precision@20: 0.04602, val_ndcg@20: 0.10584
    [Iteration 7600/10000] train_loss: -61.43322, val_loss: -51.59992, val_recall@20: 0.1491, val_precision@20: 0.04602, val_ndcg@20: 0.10584
    [Iteration 7800/10000] train_loss: -62.50441, val_loss: -52.01427, val_recall@20: 0.1491, val_precision@20: 0.04602, val_ndcg@20: 0.10597
    [Iteration 8000/10000] train_loss: -62.64954, val_loss: -53.00801, val_recall@20: 0.14916, val_precision@20: 0.04602, val_ndcg@20: 0.10584
    [Iteration 8200/10000] train_loss: -63.46519, val_loss: -52.97116, val_recall@20: 0.14938, val_precision@20: 0.0462, val_ndcg@20: 0.10595
    [Iteration 8400/10000] train_loss: -62.74277, val_loss: -53.66493, val_recall@20: 0.1491, val_precision@20: 0.04602, val_ndcg@20: 0.10593
    [Iteration 8600/10000] train_loss: -68.88041, val_loss: -54.37319, val_recall@20: 0.14916, val_precision@20: 0.04602, val_ndcg@20: 0.10605
    [Iteration 8800/10000] train_loss: -69.47198, val_loss: -54.55242, val_recall@20: 0.14913, val_precision@20: 0.04602, val_ndcg@20: 0.10583
    [Iteration 9000/10000] train_loss: -70.36764, val_loss: -54.75371, val_recall@20: 0.14933, val_precision@20: 0.04611, val_ndcg@20: 0.10598
    [Iteration 9200/10000] train_loss: -68.07756, val_loss: -56.0104, val_recall@20: 0.14913, val_precision@20: 0.04602, val_ndcg@20: 0.10586
    [Iteration 9400/10000] train_loss: -70.01717, val_loss: -56.17115, val_recall@20: 0.14913, val_precision@20: 0.04602, val_ndcg@20: 0.10598
    [Iteration 9600/10000] train_loss: -71.47074, val_loss: -56.40413, val_recall@20: 0.14913, val_precision@20: 0.04602, val_ndcg@20: 0.10586
    [Iteration 9800/10000] train_loss: -69.44392, val_loss: -57.50562, val_recall@20: 0.14933, val_precision@20: 0.04611, val_ndcg@20: 0.10598

```python
iters = [iter * ITERS_PER_EVAL for iter in range(len(train_losses))]
plt.plot(iters, train_losses, label='train')
plt.plot(iters, val_losses, label='validation')
plt.xlabel('iteration')
plt.ylabel('loss')
plt.title('training and validation loss curves')
plt.legend()
plt.show()
```

```python
# evaluate on test set
model.eval()
test_edge_index = test_edge_index.to(device)
test_sparse_edge_index = test_sparse_edge_index.to(device)

test_loss, test_recall, test_precision, test_ndcg = evaluation(
            model, test_edge_index, test_sparse_edge_index, [train_edge_index, val_edge_index], K, LAMBDA)

print(f"[test_loss: {round(test_loss, 5)}, test_recall@{K}: {round(test_recall, 5)}, test_precision@{K}: {round(test_precision, 5)}, test_ndcg@{K}: {round(test_ndcg, 5)}")
```

    [test_loss: -51.86417, test_recall@20: 0.12678, test_precision@20: 0.04674, test_ndcg@20: 0.10205

# Make New Recommendatios for a Given User

```python
# def get_movie_title_mapping():
model.eval()
df = pd.read_csv(movie_path)
movieid_title = pd.Series(df.title.values,index=df.movieId).to_dict()
movieid_genres = pd.Series(df.genres.values,index=df.movieId).to_dict()

user_pos_items = get_user_positive_items(edge_index)
```

```python
USER_ID = 1
NUM_RECS = 10

user = user_mapping[USER_ID]
e_u = model.users_emb.weight[user]
scores = model.items_emb.weight @ e_u

values, indices = torch.topk(scores, k=len(user_pos_items[user]) + NUM_RECS)

movies = [index.cpu().item() for index in indices if index in user_pos_items[user]][:NUM_RECS]
movie_ids = [list(movie_mapping.keys())[list(movie_mapping.values()).index(movie)] for movie in movies]
titles = [movieid_title[id] for id in movie_ids]
genres = [movieid_genres[id] for id in movie_ids]

print(f"Here are some movies that user {USER_ID} rated highly")
for i in range(NUM_RECS):
    print(f"title: {titles[i]}, genres: {genres[i]} ")

print()

movies = [index.cpu().item() for index in indices if index not in user_pos_items[user]][:NUM_RECS]
movie_ids = [list(movie_mapping.keys())[list(movie_mapping.values()).index(movie)] for movie in movies]
titles = [movieid_title[id] for id in movie_ids]
genres = [movieid_genres[id] for id in movie_ids]

print(f"Here are some suggested movies for user {USER_ID}")
for i in range(NUM_RECS):
    print(f"title: {titles[i]}, genres: {genres[i]} ")
```

    Here are some movies that user 1 rated highly
    title: Forrest Gump (1994), genres: Comedy|Drama|Romance|War
    title: Matrix, The (1999), genres: Action|Sci-Fi|Thriller
    title: Silence of the Lambs, The (1991), genres: Crime|Horror|Thriller
    title: Star Wars: Episode IV - A New Hope (1977), genres: Action|Adventure|Sci-Fi
    title: Fight Club (1999), genres: Action|Crime|Drama|Thriller
    title: Schindler's List (1993), genres: Drama|War
    title: Star Wars: Episode V - The Empire Strikes Back (1980), genres: Action|Adventure|Sci-Fi
    title: Braveheart (1995), genres: Action|Drama|War
    title: American Beauty (1999), genres: Drama|Romance
    title: Usual Suspects, The (1995), genres: Crime|Mystery|Thriller

    Here are some suggested movies for user 1
    title: Shawshank Redemption, The (1994), genres: Crime|Drama
    title: Pulp Fiction (1994), genres: Comedy|Crime|Drama|Thriller
    title: Godfather, The (1972), genres: Crime|Drama
    title: Terminator 2: Judgment Day (1991), genres: Action|Sci-Fi
    title: Lord of the Rings: The Return of the King, The (2003), genres: Action|Adventure|Drama|Fantasy
    title: Lord of the Rings: The Fellowship of the Ring, The (2001), genres: Adventure|Fantasy
    title: Lord of the Rings: The Two Towers, The (2002), genres: Adventure|Fantasy
    title: Apollo 13 (1995), genres: Adventure|Drama|IMAX
    title: Aladdin (1992), genres: Adventure|Animation|Children|Comedy|Musical
    title: Sixth Sense, The (1999), genres: Drama|Horror|Mystery

```python

```


# Spectral Bandpass Dependence Correction

Reflectance spectrophotometers measuring energy at intervals larger than a single wavelength (5nm or 10nm steps for example) integrates the energy between $\lambda_{i-1}$ and $\lambda_{i+1}$ for a given wavelength $\lambda_{i}$.

The sampled spectral reflectance data $P^\prime$ needs to be corrected to retrieve the true spectral reflectance data $P$ if the spectrophotometer operating software has not applied spectral bandpass dependence correction. [1][2]

## Stearns & Stearns Method

Stearns and Stearns (1988) proposed the following equation for spectral bandpass dependence correction:

$$
\begin{equation}
P_i = -\alpha P^\prime_{i -1} + (1 + 2\alpha)P^\prime_{i} -\alpha P^\prime_{i+1}
\end{equation}
$$

where $\alpha = 0.083$ and if the wavelengths being corrected are the first or last one the equation for the first wavelength should be:

$$
\begin{equation}
P_i = (1 + \alpha)P^\prime_{i} - \alpha P^\prime_{i+1}
\end{equation}
$$

and for the last wavelength:

$$
\begin{equation}
P_i = (1 + \alpha)P^\prime_{i} - \alpha P^\prime_{i-1}
\end{equation}
$$

Implementation in [Colour](https://github.com/colour-science/colour/) is available through the `colour.bandpass_correction_stearns` definition or the generic `colour.bandpass_correction` definition with `method='Stearns'` argument:

```python
import colour

from colour.plotting import *
```

```python
colour_style();
```

```python
# Spectral bandpass dependence correction.
street_light_sd_data = {
    380: 8.9770000e-003,
    382: 5.8380000e-003,
    384: 8.3290000e-003,
    386: 8.6940000e-003,
    388: 1.0450000e-002,
    390: 1.0940000e-002,
    392: 8.4260000e-003,
    394: 1.1720000e-002,
    396: 1.2260000e-002,
    398: 7.4550000e-003,
    400: 9.8730000e-003,
    402: 1.2970000e-002,
    404: 1.4000000e-002,
    406: 1.1000000e-002,
    408: 1.1330000e-002,
    410: 1.2100000e-002,
    412: 1.4070000e-002,
    414: 1.5150000e-002,
    416: 1.4800000e-002,
    418: 1.6800000e-002,
    420: 1.6850000e-002,
    422: 1.7070000e-002,
    424: 1.7220000e-002,
    426: 1.8250000e-002,
    428: 1.9930000e-002,
    430: 2.2640000e-002,
    432: 2.4630000e-002,
    434: 2.5250000e-002,
    436: 2.6690000e-002,
    438: 2.8320000e-002,
    440: 2.5500000e-002,
    442: 1.8450000e-002,
    444: 1.6470000e-002,
    446: 2.2470000e-002,
    448: 3.6250000e-002,
    450: 4.3970000e-002,
    452: 2.7090000e-002,
    454: 2.2400000e-002,
    456: 1.4380000e-002,
    458: 1.3210000e-002,
    460: 1.8250000e-002,
    462: 2.6440000e-002,
    464: 4.5690000e-002,
    466: 9.2240000e-002,
    468: 6.0570000e-002,
    470: 2.6740000e-002,
    472: 2.2430000e-002,
    474: 3.4190000e-002,
    476: 2.8160000e-002,
    478: 1.9570000e-002,
    480: 1.8430000e-002,
    482: 1.9800000e-002,
    484: 2.1840000e-002,
    486: 2.2840000e-002,
    488: 2.5760000e-002,
    490: 2.9800000e-002,
    492: 3.6620000e-002,
    494: 6.2500000e-002,
    496: 1.7130000e-001,
    498: 2.3920000e-001,
    500: 1.0620000e-001,
    502: 4.1250000e-002,
    504: 3.3340000e-002,
    506: 3.0820000e-002,
    508: 3.0750000e-002,
    510: 3.2500000e-002,
    512: 4.5570000e-002,
    514: 7.5490000e-002,
    516: 6.6560000e-002,
    518: 3.9350000e-002,
    520: 3.3880000e-002,
    522: 3.4610000e-002,
    524: 3.6270000e-002,
    526: 3.6580000e-002,
    528: 3.7990000e-002,
    530: 4.0010000e-002,
    532: 4.0540000e-002,
    534: 4.2380000e-002,
    536: 4.4190000e-002,
    538: 4.6760000e-002,
    540: 5.1490000e-002,
    542: 5.7320000e-002,
    544: 7.0770000e-002,
    546: 1.0230000e-001,
    548: 1.6330000e-001,
    550: 2.3550000e-001,
    552: 2.7540000e-001,
    554: 2.9590000e-001,
    556: 3.2950000e-001,
    558: 3.7630000e-001,
    560: 4.1420000e-001,
    562: 4.4850000e-001,
    564: 5.3330000e-001,
    566: 7.3490000e-001,
    568: 8.6530000e-001,
    570: 7.8120000e-001,
    572: 6.8580000e-001,
    574: 6.6740000e-001,
    576: 6.9300000e-001,
    578: 6.9540000e-001,
    580: 6.3260000e-001,
    582: 4.6240000e-001,
    584: 2.3550000e-001,
    586: 8.4450000e-002,
    588: 3.5550000e-002,
    590: 4.0580000e-002,
    592: 1.3370000e-001,
    594: 3.4150000e-001,
    596: 5.8250000e-001,
    598: 7.2080000e-001,
    600: 7.6530000e-001,
    602: 7.5290000e-001,
    604: 7.1080000e-001,
    606: 6.5840000e-001,
    608: 6.0140000e-001,
    610: 5.5270000e-001,
    612: 5.4450000e-001,
    614: 5.9260000e-001,
    616: 5.4520000e-001,
    618: 4.4690000e-001,
    620: 3.9040000e-001,
    622: 3.5880000e-001,
    624: 3.3400000e-001,
    626: 3.1480000e-001,
    628: 2.9800000e-001,
    630: 2.8090000e-001,
    632: 2.6370000e-001,
    634: 2.5010000e-001,
    636: 2.3610000e-001,
    638: 2.2550000e-001,
    640: 2.1680000e-001,
    642: 2.0720000e-001,
    644: 1.9920000e-001,
    646: 1.9070000e-001,
    648: 1.8520000e-001,
    650: 1.7970000e-001,
    652: 1.7410000e-001,
    654: 1.7070000e-001,
    656: 1.6500000e-001,
    658: 1.6080000e-001,
    660: 1.5660000e-001,
    662: 1.5330000e-001,
    664: 1.4860000e-001,
    666: 1.4540000e-001,
    668: 1.4260000e-001,
    670: 1.3840000e-001,
    672: 1.3500000e-001,
    674: 1.3180000e-001,
    676: 1.2730000e-001,
    678: 1.2390000e-001,
    680: 1.2210000e-001,
    682: 1.1840000e-001,
    684: 1.1530000e-001,
    686: 1.1210000e-001,
    688: 1.1060000e-001,
    690: 1.0950000e-001,
    692: 1.0840000e-001,
    694: 1.0740000e-001,
    696: 1.0630000e-001,
    698: 1.0550000e-001,
    700: 1.0380000e-001,
    702: 1.0250000e-001,
    704: 1.0380000e-001,
    706: 1.0250000e-001,
    708: 1.0130000e-001,
    710: 1.0020000e-001,
    712: 9.8310000e-002,
    714: 9.8630000e-002,
    716: 9.8140000e-002,
    718: 9.6680000e-002,
    720: 9.4430000e-002,
    722: 9.4050000e-002,
    724: 9.2510000e-002,
    726: 9.1880000e-002,
    728: 9.1120000e-002,
    730: 8.9860000e-002,
    732: 8.9460000e-002,
    734: 8.8610000e-002,
    736: 8.9640000e-002,
    738: 8.9910000e-002,
    740: 8.7700000e-002,
    742: 8.7540000e-002,
    744: 8.5880000e-002,
    746: 8.1340000e-002,
    748: 8.8200000e-002,
    750: 8.9410000e-002,
    752: 8.9360000e-002,
    754: 8.4970000e-002,
    756: 8.9030000e-002,
    758: 8.7810000e-002,
    760: 8.5330000e-002,
    762: 8.5880000e-002,
    764: 1.1310000e-001,
    766: 1.6180000e-001,
    768: 1.6770000e-001,
    770: 1.5340000e-001,
    772: 1.1740000e-001,
    774: 9.2280000e-002,
    776: 9.0480000e-002,
    778: 9.0020000e-002,
    780: 8.8190000e-002}

street_light_sd = colour.SpectralDistribution(
    street_light_sd_data, name='Street Light')

bandpass_corrected_street_light_sd = street_light_sd.copy()
bandpass_corrected_street_light_sd.name = 'Street Light (Bandpass Corrected)'
bandpass_corrected_street_light_sd = colour.colorimetry.bandpass_correction(
    bandpass_corrected_street_light_sd, method='Stearns 1988')

plot_multi_sds([street_light_sd, bandpass_corrected_street_light_sd],
               title='Stearns Bandpass Correction');
```

We can then calculate the [$\Delta E^\star_{ab}$](http://en.wikipedia.org/wiki/Color_difference#Delta_E) between the two spectral distributions:

```python
cmfs = colour.colorimetry.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer']
street_light_XYZ = colour.sd_to_XYZ(
    street_light_sd.interpolate(
        colour.SpectralShape(interval=1)), cmfs)
bandpass_corrected_street_light_XYZ = colour.sd_to_XYZ(
    bandpass_corrected_street_light_sd.interpolate(
        colour.SpectralShape(interval=1)), cmfs)

# Converting the *CIE XYZ* colourspace values to *CIE Lab* colourspace
# and calculating *Delta E*.
colour.difference.delta_E_CIE2000(*colour.XYZ_to_Lab(
        [street_light_XYZ / 100, bandpass_corrected_street_light_XYZ / 100]))
```

    0.0052549406580512251

## Bibliography

1. ^ Westland, S., Ripamonti, C., & Cheung, V. (2012). Correction for Spectral Bandpass. In *Computational Colour Science Using MATLAB* (2nd ed., p. 38). ISBN:978-0-470-66569-5
2. ^ Stearns, E. I., & Stearns, R. E. (1988). An example of a method for correcting radiance data for Bandpass error. Color Research & Application, 13(4), 257–259. doi:10.1002/col.5080130410


# Combinando Modelos e dados: COVID-19

Ao se modelar uma epidemia real, a concordância do modelo e os dados observados é de extrema importância.

Neste Notebook vamos estudar o Modelo SEIAHR proposto para a COVID-19 por [Coelho et al](https://www.medrxiv.org/content/10.1101/2020.06.15.20132050v1). Neste modelo, temos os seguintes compartimentos:

Matemáticamente:
\begin{align}
\frac{dS}{dt}&=-\lambda [(1-\chi) S],\\
\frac{dE}{dt}&= \lambda [(1-\chi) S]-\alpha E,\\
\frac{dI}{dt}&= (1-p)\alpha E - \delta I -\phi I,\\
\frac{dA}{dt}&= p\alpha E - \gamma A,\\
\frac{dH}{dt}&= \phi I -(\rho+\mu) H,\\
\frac{dR}{dt}&= \delta I + \rho H+\gamma A,
\end{align}

onde $\lambda=\beta (I+A)$.

Para facilitar nosso trabalho com dados dentro do ambiente Sage, precisamos instalar o Pandas para isso
em um terminal use o seguinte comando:
```
sage -sh
```
este comando iniciará uma subsessão do sage onde você poderá instalar o pandas com o comando usual:
```
pip install pandas
pip install parameter-sherpa
exit
```
o comando `exit` após a instalação visa sair da subsessão do Sage.

```python
import numpy as np
import pandas as pd
# %display typeset
```

```python
def model(t, y, params):
    S, E, I, A, H, R, C, D = y
    chi, phi, beta, rho, delta, gamma, alpha, mu, p, q, r = params
    lamb = beta * (I + A)
    # Turns on Quarantine on day q and off on day q+r
    chi *= ((1 + np.tanh(t - q)) / 2) * ((1 - np.tanh(t - (q + r))) / 2)
    return [
        -lamb * ((1 - chi) * S),  # dS/dt
        lamb * ((1 - chi) * S) - alpha * E,  # dE/dt
        (1 - p) * alpha * E - delta * I - phi * I,  # dI/dt
        p * alpha * E - gamma * A,
        phi * I - (rho + mu) * H,  # dH/dt
        delta * I + rho * H + gamma * A,  # dR/dt
        phi * I,  # (1-p)*alpha*E+ p*alpha*E # Hospit. acumuladas
        mu * H  # Morte acumuladas
    ]
```

```python
chi = .3
phi = 0.012413633926076584
beta = 0.27272459855759813
rho = 0.2190519831830368
delta = 0.04168480042146949
gamma = 0.04
alpha =  0.3413355572047603
mu = 0.02359234606623134
p = 0.7693029079871165
q = 50
r = 55
```

```python
T = ode_solver()
T.function = model
T.algorithm='rk8pd'
inits = [.99, 0, 1e-4, 0, 0, 0, 0, 0]
tspan = [0,200]
T.ode_solve(tspan, inits, num_points=200, params=[chi,phi,beta,rho,delta,gamma,alpha,mu,p,q,r])
```

```python
def get_sim_array(sol):
    sim = np.array([y for t,y in sol])
    return sim
get_sim_array(T.solution).shape
```

    (201, 8)

```python
popRJ = 6.32e6
def plot_sol(sol):
    sim = get_sim_array(sol)*popRJ
    P = list_plot(sim[:,0],legend_label='S')
    colors = ['blue','red','pink','green','yellow','orange','black','purple']
    for i,var in enumerate(['E','I','A','H','R','C','D']):
        P += list_plot(sim[:,i+1],color=colors[i+1],legend_label=var)

    show(P)
plot_sol(T.solution)
```

```python
sims = get_sim_array(T.solution)
sims[-1,-2]*popRJ
```

    328307.38107624033

## Obtendo os dados
Os dados usados aqui foram obtidos do site [Brasil.io](https://brasil.io). Apenas os dados do estado do Rio de Janeiro foram extraídos e salvos em um CSV.

```python
def load_data(state):
    df = pd.read_csv(f'dados_{state}.csv')
    df['data'] = pd.to_datetime(df.data)
#     df.set_index('data', inplace=True)
    return df
```

```python
dfRJ = load_data('RJ')
ld = len(dfRJ)
html(dfRJ.tail().to_html())
```

      data
      date
      last_available_confirmed
      last_available_deaths
      incidencia_casos
      incidencia_morte
      ew

      170
      2020-08-22
      2020-08-22
      210464
      15267
      65.0
      3428.0
      34

      171
      2020-08-23
      2020-08-23
      210948
      15292
      25.0
      484.0
      35

      172
      2020-08-24
      2020-08-24
      211360
      15392
      100.0
      412.0
      35

      173
      2020-08-25
      2020-08-25
      214003
      15560
      168.0
      2643.0
      35

      174
      2020-08-26
      2020-08-26
      214003
      15560
      0.0
      0.0
      35

```python
subnot=1
dfRJ.set_index('data')[['last_available_confirmed','last_available_deaths']].plot();
```

```python
dfRJ['last_available_deaths'].plot()
```

## Ajustando o Modelo aos dados
Há muitas formas de se ajustar um modelo dinâmico aos dados disponíveis. Vamos começar por meio de otimização, buscando os valores de parâmetros que minimizam o desvio entre o modelo e os dados. O ajuste será feito simultaneamente para casos e mortes acumuladas. Abaixo vamos importar a biblioteca [Sherpa](https://parameter-sherpa.readthedocs.io/en/latest/gettingstarted/guide.html):

```python
!pip install parameter-sherpa
```

    Collecting parameter-sherpa
      Downloading parameter-sherpa-1.0.6.tar.gz (513 kB)
         |████████████████████████████████| 513 kB 4.5 MB/s eta 0:00:01
    Requirement already satisfied: pandas>=0.20.3 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from parameter-sherpa) (1.3.1)
    Collecting pymongo>=3.5.1
      Downloading pymongo-3.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (531 kB)
         |████████████████████████████████| 531 kB 7.4 MB/s eta 0:00:01
    Requirement already satisfied: numpy>=1.8.2 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from parameter-sherpa) (1.19.5)
    Requirement already satisfied: scipy>=1.0.0 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from parameter-sherpa) (1.5.4)
    Requirement already satisfied: scikit-learn>=0.19.1 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from parameter-sherpa) (0.24.2)
    Collecting flask>=0.12.2
      Downloading Flask-2.0.1-py3-none-any.whl (94 kB)
         |████████████████████████████████| 94 kB 2.8 MB/s  eta 0:00:01
    Collecting GPyOpt>=1.2.5
      Downloading GPyOpt-1.2.6.tar.gz (56 kB)
         |████████████████████████████████| 56 kB 4.8 MB/s  eta 0:00:01
    Collecting enum34
      Downloading enum34-1.1.10-py3-none-any.whl (11 kB)
    Requirement already satisfied: matplotlib in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from parameter-sherpa) (3.3.4)
    Collecting Jinja2>=3.0
      Using cached Jinja2-3.0.1-py3-none-any.whl (133 kB)
    Collecting itsdangerous>=2.0
      Downloading itsdangerous-2.0.1-py3-none-any.whl (18 kB)
    Requirement already satisfied: click>=7.1.2 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from flask>=0.12.2->parameter-sherpa) (8.0.1)
    Collecting Werkzeug>=2.0
      Downloading Werkzeug-2.0.1-py3-none-any.whl (288 kB)
         |████████████████████████████████| 288 kB 12.2 MB/s eta 0:00:01
    Collecting GPy>=1.8
      Downloading GPy-1.10.0.tar.gz (959 kB)
         |████████████████████████████████| 959 kB 12.8 MB/s eta 0:00:01
    Requirement already satisfied: six in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from GPy>=1.8->GPyOpt>=1.2.5->parameter-sherpa) (1.15.0)
    Collecting paramz>=0.9.0
      Downloading paramz-0.9.5.tar.gz (71 kB)
         |████████████████████████████████| 71 kB 7.1 MB/s  eta 0:00:01
    Requirement already satisfied: cython>=0.29 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from GPy>=1.8->GPyOpt>=1.2.5->parameter-sherpa) (0.29.21)
    Collecting MarkupSafe>=2.0
      Downloading MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (30 kB)
    Requirement already satisfied: python-dateutil>=2.7.3 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from pandas>=0.20.3->parameter-sherpa) (2.8.0)
    Requirement already satisfied: pytz>=2017.3 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from pandas>=0.20.3->parameter-sherpa) (2020.4)
    Requirement already satisfied: decorator>=4.0.10 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from paramz>=0.9.0->GPy>=1.8->GPyOpt>=1.2.5->parameter-sherpa) (4.4.2)
    Requirement already satisfied: threadpoolctl>=2.0.0 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from scikit-learn>=0.19.1->parameter-sherpa) (2.2.0)
    Requirement already satisfied: joblib>=0.11 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from scikit-learn>=0.19.1->parameter-sherpa) (1.0.1)
    Requirement already satisfied: pillow>=6.2.0 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from matplotlib->parameter-sherpa) (8.1.2)
    Requirement already satisfied: kiwisolver>=1.0.1 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from matplotlib->parameter-sherpa) (1.0.1)
    Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.3 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from matplotlib->parameter-sherpa) (2.4.7)
    Requirement already satisfied: cycler>=0.10 in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from matplotlib->parameter-sherpa) (0.10.0)
    Requirement already satisfied: setuptools in /home/fccoelho/Downloads/SageMath/local/lib/python3.9/site-packages (from kiwisolver>=1.0.1->matplotlib->parameter-sherpa) (51.1.1)
    Building wheels for collected packages: parameter-sherpa, GPyOpt, GPy, paramz
      Building wheel for parameter-sherpa (setup.py) ... done
      Created wheel for parameter-sherpa: filename=parameter_sherpa-1.0.6-py2.py3-none-any.whl size=542118 sha256=8836fcef51a6e46b135b20539b48b4a7e87ce274b6bb8f630bf39378f80edbfd
      Stored in directory: /home/fccoelho/.cache/pip/wheels/5e/e2/ac/eb3e175761289cb9a9c5d86045009b9e192ed0df15554f637d
      Building wheel for GPyOpt (setup.py) ... done
      Created wheel for GPyOpt: filename=GPyOpt-1.2.6-py3-none-any.whl size=83621 sha256=173cc00fed6f5f8590501dafed53f5f21635365d0961027867aae75d14e8bddb
      Stored in directory: /home/fccoelho/.cache/pip/wheels/11/b8/44/0282cdff2277bc12f04266de6f104099dec02411879a0ac19f
      Building wheel for GPy (setup.py) ... done
      Created wheel for GPy: filename=GPy-1.10.0-cp39-cp39-linux_x86_64.whl size=3286527 sha256=7b5f179a6788cd0a1275f12e328eaaa1309580d1119e93858e863c88422f4bc4
      Stored in directory: /home/fccoelho/.cache/pip/wheels/78/fd/57/7c1e4a6f9a5380e2536af9809075ba085b1bb8d38ee84ea183
      Building wheel for paramz (setup.py) ... done
      Created wheel for paramz: filename=paramz-0.9.5-py3-none-any.whl size=102550 sha256=cb0e77e7644d17d8ea46d040c3b03b55de262e17a4fb8106b97b3cabc1efea73
      Stored in directory: /home/fccoelho/.cache/pip/wheels/9c/5f/9b/c4273ae8f869387214be2b99598d1b71dbf00672576cb85e74
    Successfully built parameter-sherpa GPyOpt GPy paramz
    Installing collected packages: paramz, MarkupSafe, Werkzeug, Jinja2, itsdangerous, GPy, pymongo, GPyOpt, flask, enum34, parameter-sherpa
      Attempting uninstall: MarkupSafe
        Found existing installation: MarkupSafe 1.1.1
        Uninstalling MarkupSafe-1.1.1:
          Successfully uninstalled MarkupSafe-1.1.1
      Attempting uninstall: Jinja2
        Found existing installation: Jinja2 2.11.2
        Uninstalling Jinja2-2.11.2:
          Successfully uninstalled Jinja2-2.11.2
    Successfully installed GPy-1.10.0 GPyOpt-1.2.6 Jinja2-3.0.1 MarkupSafe-2.0.1 Werkzeug-2.0.1 enum34-1.1.10 flask-2.0.1 itsdangerous-2.0.1 parameter-sherpa-1.0.6 paramz-0.9.5 pymongo-3.12.0
    WARNING: You are using pip version 21.0.1; however, version 21.2.4 is available.
    You should consider upgrading via the '/home/fccoelho/Downloads/SageMath/local/bin/python3 -m pip install --upgrade pip' command.

```python
import sherpa
```

Começamos por definir o tipo de variável de cada parâmetro, e o tipo de algoritmo de busca que utilizaremos.

```python
parameters = [
    sherpa.Continuous(name='e',range=[0,1]),
    sherpa.Continuous(name='chi',range=[0,.3]),
    sherpa.Continuous(name='phi',range=[0,1]),
    sherpa.Continuous(name='beta',range=[0.1,2]),
    sherpa.Continuous(name='rho',range=[0.2,1]),
    sherpa.Continuous(name='delta',range=[0.01,1]),
    sherpa.Continuous(name='gamma',range=[0.01,1]),
    sherpa.Continuous(name='alpha',range=[0.01,1]),
    sherpa.Continuous(name='mu',range=[0.01,.7]),
    sherpa.Continuous(name='p',range=[0.01,.7]),
    sherpa.Discrete(name='q',range=[1,30]),
    sherpa.Discrete(name='r',range=[1,12]),
    sherpa.Discrete(name='t0',range=[0,25]),
]
algorithm = sherpa.algorithms.RandomSearch(max_num_trials=1000)
# algorithm = sherpa.algorithms.GPyOpt(model_type='GP',max_num_trials=150)
```

```python
study = sherpa.Study(parameters=parameters,
                     algorithm=algorithm,
                     lower_is_better=True,
                     disable_dashboard=True)

```

Uma vez definido o problema (`study`) podemos gerar uma sugestão valores para ver como funciona:

```python
trial = study.get_suggestion()
trial.parameters
```

    {'e': 0.6633981790812898,
     'chi': 0.20806102643110655,
     'phi': 0.1675576065501,
     'beta': 1.6334929266739133,
     'rho': 0.4983366877248262,
     'delta': 0.3151485461881289,
     'gamma': 0.5319575393066565,
     'alpha': 0.3685410748547585,
     'mu': 0.5881872108774452,
     'p': 0.39349886479238544,
     'q': 22,
     'r': 1,
     't0': 19}

Então podemos executar a busca de parâmetros em um simples loop

```python
for trial in study:
    pars = [trial.parameters[n] for n in ['chi', 'phi', 'beta', 'rho', 'delta', 'gamma', 'alpha', 'mu', 'p', 'q', 'r']]
    t0 = trial.parameters['t0']
    T.ode_solve(tspan, inits, num_points=200, params=pars)
    sim = get_sim_array(T.solution)
    H = sim[:ld+t0,-2]*popRJ*trial.parameters['e']
    D = sim[:ld+t0,-1]*popRJ*trial.parameters['e']
    loss = sum((dfRJ.last_available_confirmed-H[t0:t0+ld])**2) +sum((dfRJ.last_available_deaths-D[t0:t0+ld])**2)/2*ld
    study.add_observation(trial=trial,
                          objective=loss,
                          )
    study.finalize(trial)
```

```python
res = study.get_best_result()
res
```

    {'Trial-ID': 475,
     'Iteration': 1,
     'alpha': 0.9394810724951352,
     'beta': 0.5308498112450182,
     'chi': 0.12780938386529786,
     'delta': 0.5923550985639527,
     'e': 0.9084210097968136,
     'gamma': 0.2498206877232359,
     'mu': 0.04066936638497329,
     'p': 0.3337257898942823,
     'phi': 0.09298989032857552,
     'q': 21,
     'r': 1,
     'rho': 0.3594731742120626,
     't0': 20,
     'Objective': 75443865634.05386}

```python
def plot_results(pars):
    T.ode_solve(tspan, inits, num_points=200, params=list(pars[:-1]))
    t0=pars[-1]
    sim = get_sim_array(T.solution)*popRJ
    h = list_plot(sim[:ld+t0,-2],color='red',legend_label='Cum. cases', plotjoined=True)
    d = list_plot(sim[:ld+t0,-1],color='purple', legend_label='Cum. Deaths', plotjoined=True)
    cc = list_plot(list(zip(range(t0,ld+t0),dfRJ.last_available_confirmed)), color='black',legend_label='cases (obs)')
    cd = list_plot(list(zip(range(t0,ld+t0),dfRJ.last_available_deaths)), color='orange',legend_label='deaths(obs)')
    show(h+d+cc+cd)
```

```python
plot_results([res['chi'],
              res['phi'],
              res['beta'],
              res['rho'],
              res['delta'],
              res['gamma'],
              res['alpha'],
              res['mu'],
              res['p'],
              res['q'],
              res['r'],
              res['t0']
             ])
```

# Passo a passo da segunda parte
## Resultados
### Ter o modelo pronto e adimensionalizado
### Encontrar os equilíbrios: ELD (eq. livre de doença) EE (eq. endêmicos)
### Por meio de metodo de linearização local, caracterizar as estabilidades dos equilíbrios
### Calculo do RO. Analitico e depois substituir os valores para obter uma estimativa numérica.
### Gerar simulações e compara com os dados. Descrevam a adequação do modelo para representar a epidemia no país escolhido
### Análise de sensibilidade
### Otimização dos parâmetros
### Estimação bayesiana dos parâmetros.
## Discussão
### Contextualizar cada resultado
### Discutir toda a análise feita de um ponto de vista geral defendendo a aplicabilidade e originalidade dos resultados obtidos.
### Implicações do modelo para o controle da COVID
## Conclusão

Adicionalmente é necessário escrever um resumo, no início de artigo.

```python
from scipy.interpolate import interp1d
from matplotlib import pyplot as plt
```

```python
d = [2,6,5,3,8,7,0]
f = interp1d(range(7),d,kind='quadratic')
```

```python
plt.plot(d, 'o')
t2 = [0, 1, 1.5,2.5,3,3.1,6]
plt.plot(t2,f(t2),'-*' )
```

```python
plot(f(t2))
```

```python

```

Planilhas Sage/Suplemento 2 - o Modelo SEIAHR.ipynb


```python
from coppertop.core import ensurePath, unload
'/Users/david/repos/github/DangerMouseB/coppertop/src/std' >> ensurePath       # <= set this to your path
'/Users/david/repos/github/DangerMouseB/coppertop/|src/dm' >> ensurePath       # <= set this to your path
from importlib import reload
```

See README.md for notes on usage of coppertop.

```python
%matplotlib inline
import matplotlib.pyplot as plt, numpy as np
```

```python
from coppertop.pipe import *
from coppertop.core import PP as _PP, RR, DD, HH, LL, TT
from bones.metatypes import BType
from bones.core.types import T, T1, T2, T3, num, pystr
from coppertop.std import format, fvs, join, values, _, inject, each, fields, values, typeOf, at
from coppertop.std.maths import max, min
from coppertop.std.types import adhoc
from coppertop.dm.pmf import pmfMul, normalise, uniform, sequence, mean, PMF, L, values, fields, to
from coppertop.dm.utils import formatStruct
```

```python
formatPmf = formatStruct(_, 'PMF', '.3f', '.3f', ', ')
formatL = formatStruct(_, 'L', '.3f', '.3f', ', ')

@coppertop
def PP(x):
    return x >> _PP

@coppertop
def PP(x, f):
    f(x) >> _PP
    return x

@coppertop
def PP(x:L):
    x >> formatL >> _PP
    return x

@coppertop
def PP(x:PMF):
    x >> formatPmf >> _PP
    return x
```

#### Bayes Refresher

from PROP 3
$$
\begin{align}
\mathbf{P}\left(B \cap A\right) = \mathbf{P}\left(B\mathbin{\vert}A\right)\cdot \mathbf{P}\left(A\right)\\
\end{align}
$$

and obviously
$$\mathbf{P}(A \cap B) = \mathbf{P}(B \cap A)$$

so
$$
\begin{align}
\mathbf{P}( A\mathbin{\vert}B) \cdot \mathbf{P}(B)=\mathbf{P}(B\mathbin{\vert}A)\cdot \mathbf{P}(A)
\end{align}
$$

aka
$$
\begin{align}
\mathbf{P}( hypothesis\mathbin{\vert}data) \cdot \mathbf{P}(data)=\mathbf{P}(data\mathbin{\vert}hypothesis)\cdot \mathbf{P}(hypothesis)
\end{align}
$$

Comtemporaneous (look it up) version, i.e. after some data is known
$$
\begin{align}
posterior =likelihood\cdot prior \cdot constant
\end{align}
$$

#### 1.6 The M&M Problem

M&M’s are small candy-coated chocolates that come in a variety of colors.
Mars, Inc., which makes M&M’s, changes the mixture of colors from time
to time.

In 1995, they introduced blue M&M’s. Before then, the color mix in a bag
of plain M&M’s was 30% Brown, 20% Yellow, 20% Red, 10% Green, 10%
Orange, 10% Tan. Afterward it was 24% Blue , 20% Green, 16% Orange,
14% Yellow, 13% Red, 13% Brown.

Suppose a friend of mine has two bags of M&M’s, and he tells me that one
is from 1994 and one from 1996. He won’t tell me which is which, but he
gives me one M&M from each bag. One is yellow and one is green. What is
the probability that the yellow one came from the 1994 bag?

```python
bag1994 = PMF(Brown=30, Yellow=20, Red=20, Green=10, Orange=10, Tan=10)
bag1996 = PMF(Brown=13, Yellow=14, Red=13, Green=20, Orange=16, Blue=24)
[bag1994, bag1996] >> each >> (lambda pmf: pmf >> PP);
```

    PMF(Brown=0.300, Yellow=0.200, Red=0.200, Green=0.100, Orange=0.100, Tan=0.100)
    PMF(Brown=0.130, Yellow=0.140, Red=0.130, Green=0.200, Orange=0.160, Blue=0.240)

hypA -> yellow is from 1994, green is from 1996\
hypB -> green is from 1994, yellow is from 1996

```python
prior = PMF(hypA=0.5, hypB=0.5) >> PP

likelihood = L(
    hypA=bag1994.Yellow * bag1996.Green,
    hypB=bag1994.Green * bag1996.Yellow
) >> PP

post = prior >> pmfMul >> likelihood >> normalise
post >> PP

20/27
```

    PMF(hypA=0.500, hypB=0.500)
    L(hypA=0.040, hypB=0.014)
    PMF(hypA=0.741, hypB=0.259)

    0.7407407407407407

#### 1.7 The Monty Hall problem

Monty Hall was the original host of the game show Let’s Make a Deal. The
Monty Hall problem is based on one of the regular games on the show. If
you are on the show, here’s what happens:

• Monty shows you three closed doors and tells you that there is a prize
behind each door: one prize is a car, the other two are less valuable
prizes like peanut butter and fake finger nails. The prizes are arranged
at random.

• The object of the game is to guess which door has the car. If you guess
right, you get to keep the car.

• You pick a door, which we will call Door A. We’ll call the other doors
B and C.

• Before opening the door you chose, Monty increases the suspense by
opening either Door B or C, whichever does not have the car. (If the
car is actually behind Door A, Monty can safely open B or C, so he
chooses one at random.)

• Then Monty offers you the option to stick with your original choice or
switch to the one remaining unopened door.

The question is, should you “stick” or “switch” or does it make no difference?

**Minor reframe**

Let A be the door we initially choose at random \
Let B be the door Monty selects to show us to be without a car \
Let C be the other door we can choose after the fact

```python
prior = PMF(A=1, B=1, C=1)
likelihood = L(   # i.e. likelihood of monty opening B given that the car is behind each, i.e. p(data|hyp)
    A=0.5,      # prob of opening B if behind A - he can choose at random so 50:50
    B=0,        # prob of opening B if behind B - Monty can't open B else he'd reveal the car, so cannot open B => 0%
    C=1,        # prob of opening B if behind C - Monty can't open C else he'd reveal the car, so must open B => 100%
)
posterior = prior >> pmfMul >> likelihood >> normalise
posterior >> PP;
```

    PMF(A=0.333, B=0.000, C=0.667)

#### 1.8 Discussion
If the Monty Hall problem is your idea of fun, I have collected a num- ber of similar problems in an article called “All your Bayes are belong to us,” which you can read at http://allendowney.blogspot.com/2011/10/ all-your-bayes-are-belong-to-us.html.

#### 2.8 Exercises

**Exercise 2.1.** In Section 2.3 I said that the solution to the cookie problem generalizes to the case where we draw multiple cookies with replacement.

But in the more likely scenario where we eat the cookies we draw, the likelihood of each draw depends on the previous draws.

Modify the solution in this chapter to handle selection without replacement. Hint: add instance variables to Cookie to represent the hypothetical state of the bowls, and modify Likelihood accordingly. You might want to define a Bowl object.

```python
@coppertop
def withAttr(s, name, value):
    s = adhoc(s)
    s[name] = value
    return s
```

```python
@coppertop
def jarLikelihood(jarsStates, flavour):
    return jarsStates >> each >> (lambda j: (j >> at(_,'tag'), j[flavour])) >> to(_,L)

@coppertop
def jarUpdate(jarsStateAndPrior, flavour):
    jarsState, prior = jarsStateAndPrior
    posterior = prior >> pmfMul >> jarLikelihood(jarsState, flavour) >> normalise
    jarsState = jarsState >> each >> (lambda s: s >> withAttr(_, flavour, s[flavour] - 1))
    f'{flavour}: {posterior >> formatPmf} ({jarsState})' >> PP
    return (jarsState, posterior)

modelState = [adhoc(V=30, C=10, tag='J1'), adhoc(V=20, C=20, tag='J2')]
data = ['V', 'C']

(modelState, PMF(J1=0.5, J2=0.5)) >> inject(data, _, _) >> jarUpdate;
```

    V: PMF(J1=0.600, J2=0.400) ([adhoc(V=29, C=10, tag='J1'), adhoc(V=19, C=20, tag='J2')])
    C: PMF(J1=0.429, J2=0.571) ([adhoc(V=29, C=9, tag='J1'), adhoc(V=19, C=19, tag='J2')])

#### 3.1 The dice problem

Suppose I have a box of dice that contains a 4-sided die, a 6-sided die, an 8-sided die, a 12-sided die, and a 20-sided die. If you have ever played Dungeons & Dragons, you know what I am talking about.

Suppose I select a die from the box at random, roll it, and get a 6. What is the probability that I rolled each die?

```python
normalise >> sig
```

    ['({adhoc+numEtAl+L})->PMF <unary1>  :   in coppertop.dm.pmf.normalise',
     '(dict)->PMF <unary1>  :   in coppertop.dm.pmf.normalise',
     '(((T2**T1)[adhoc])[T3])->((T2**T1)[adhoc])[T3] <unary1>  :   in coppertop.dm.pmf.normalise']

```python
@coppertop
def diceLikelihood(rvs, val):
    tagLikePairs = rvs >> each >> (lambda rv: (rv.tag, rv._get(val, 0.0)))
    return L(tagLikePairs)

d4 = (sequence(1, 4) >> uniform)(tag='d4')
d6 = (sequence(1, 6) >> uniform)(tag='d6')
d8 = (sequence(1, 8) >> uniform)(tag='d8')
d12 = (sequence(1, 12) >> uniform)(tag='d12')
d20 = (sequence(1, 20) >> uniform)(tag='d20')

modelState = [d4, d6, d8, d12, d20]
prior = PMF(d4=1, d6=1, d8=1, d12=1, d20=1)
prior >> PP(_,formatPmf) >> pmfMul >> (diceLikelihood(modelState, 6) >> PP(_,formatL)) >> normalise >> PP(_,formatPmf);
```

    PMF(d4=0.200, d6=0.200, d8=0.200, d12=0.200, d20=0.200)
    L(d4=0.000, d6=0.167, d8=0.125, d12=0.083, d20=0.050)
    PMF(d4=0.000, d6=0.392, d8=0.294, d12=0.196, d20=0.118)

```python
@coppertop
def diceUpdate(prior, modelState, data):
    posterior = prior >> pmfMul >> diceLikelihood(modelState, data) >> normalise
    return posterior >> PP(_, formatPmf)

[6, 6, 8, 7, 7, 5, 4] >> inject(_, prior, _) >> diceUpdate(_, modelState, _);
```

    PMF(d4=0.000, d6=0.392, d8=0.294, d12=0.196, d20=0.118)
    PMF(d4=0.000, d6=0.526, d8=0.296, d12=0.131, d20=0.047)
    PMF(d4=0.000, d6=0.000, d8=0.735, d12=0.218, d20=0.047)
    PMF(d4=0.000, d6=0.000, d8=0.818, d12=0.161, d20=0.021)
    PMF(d4=0.000, d6=0.000, d8=0.876, d12=0.115, d20=0.009)
    PMF(d4=0.000, d6=0.000, d8=0.916, d12=0.080, d20=0.004)
    PMF(d4=0.000, d6=0.000, d8=0.943, d12=0.055, d20=0.002)

```python
@coppertop
def toSteps(s:PMF+adhoc, **kwargs) -> tuple:
    return asSteps(s >> fields, s >> values, **kwargs)

def asSteps(xs, ys, align='center', width=None):
    #xMin, xMax = min(xs), max(xs)
    if width is None:
        width = np.diff(xs).min()
    points = []
    lastx = np.nan
    lasty = np.nan
    for x, y in zip(xs, ys):
        if (x - lastx) > 1e-5:
            points.append((lastx, 0))
            points.append((x, 0))
        if not np.isnan(lasty):
            points.append((x, lasty))
        points.append((x, y))
        points.append((x + width, y))
        lastx = x + width
        lasty = y
    points.append((lastx, lasty))
    pxs, pys = zip(*points)
    if align == 'center':
        pxs = np.array(pxs) - width / 2.0
    elif align == 'right':
        pxs = np.array(pxs) - width
    return pxs, np.array(pys)
```

#### 3.2 The locomotive problem

A railroad numbers its locomotives in order 1..N. One day you see a locomotive with the number 60. Estimate how many locomotives the railroad has.

**Notes**

The railroad has more than 1000 locomotives \
If the railroad has N locomotives then the chance of seeing a particular locomotive is uniformly distributed with P = 1/N \
See http://en.wikipedia.org/wiki/Minimum_mean_square_error

```python
prior = (sequence(1, 1000) >> uniform)
data = 60

@coppertop
def railroadLikehood(N, ob):
    return (sequence(1, N) >> each >> (lambda hyp:
        (hyp, 0) if hyp < ob else (hyp, 1 / hyp)) >> to(_,adhoc)
    ) | L

likelihood = railroadLikehood(1000, data)
posterior = prior >> pmfMul >> likelihood >> normalise

[posterior \
     >> values \
     >> max, posterior[60], posterior[59],
     posterior \
     >> mean
]
```

    [0.005905417875729855, 0.005905417875729855, 0.0, 333.41989326370776]

```python
fig = plt.figure(figsize=(5, 5), dpi= 80, facecolor='w', edgecolor='k')
plt.plot(*(posterior >> toSteps));
```

hmmm... that's quite a heavy tail...

```python
[250, 500, 1000, 2000, 4000] >> each >> (lambda N:
    (sequence(1, N) >> uniform)
        >> pmfMul
        >> railroadLikehood(N, data)
        >> normalise
        >> mean
)
```

    [132.8721992622349,
     207.0792279834104,
     333.41989326370776,
     552.1790171646376,
     936.5079113977753]

#### 3.4 An alternative prior

In fact, the distribution of company sizes tends to follow a power law, as Robert Axtell reports in Science (see http://www.sciencemag.org/content/293/5536/1818.full.pdf)

```python
@coppertop
def powerLawPrior(n, alpha):
    return sequence(1, 1000) >> each >> (lambda hyp: (hyp, hyp**(-alpha))) >> to(_,adhoc) >> normalise

fig = plt.figure(figsize=(5, 5), dpi= 80, facecolor='w', edgecolor='k')
plt.plot(*(1000 >> powerLawPrior(_, 1) >> toSteps));
```

```python
observations = [30, 60, 90]
Ns = [250, 500, 1000, 2000, 4000]

Ns >> each >> (lambda N:
    powerLawPrior(N, 0.9)
        >> inject(observations, _, _) >> (lambda prior, ob:
            prior
                >> pmfMul
                >> railroadLikehood(N, ob)
                >> normalise
        )
        >> mean
)
```

    [123.52471977274632,
     132.31912378592563,
     135.34103113413445,
     135.34103113413445,
     135.34103113413445]

#### 3.5 Credible intervals

```python
from coppertop.std.stats import logisticCDF
from coppertop.dm.pmf import toCmf
```

```python
d8 >> toCmf
```

    adhoc(1.0=0.125, 2.0=0.25, 3.0=0.375, 4.0=0.5, 5.0=0.625, 6.0=0.75, 7.0=0.875, 8.0=1.0, tag='d8', _cmf=array([[1.   , 0.125],
           [2.   , 0.25 ],
           [3.   , 0.375],
           [4.   , 0.5  ],
           [5.   , 0.625],
           [6.   , 0.75 ],
           [7.   , 0.875],
           [8.   , 1.   ]]))

```python

```