#Linear Regression

In this tutorial, we will take a look and implement the classic linear regression algorithm

$$y = a + bx + \varepsilon$$

Linear Regression involves creating a best fit linear line such the distance between the y' points on line and real values is minimum (the summation of the distance is known as our loss)

## Gradient Descent

### Step 1: Compute the loss

$$MSE = \frac{1}{N} \sum_{i=1}^N (y_i - \hat{y}_i)^2 $$

$$MSE = \frac{1}{N} \sum_{i=1}^N (y_i - a - bx_i )^2 $$

Mean Square Error (MSE)

### Step 2: Compute the gradients

\begin{equation}
\frac{\partial MSE}{\partial a} = \frac{\partial MSE}{\partial \hat{y}_i} \frac{\partial  \hat{y}_i}{\partial a}  = \frac{1}{N} \sum_{i=1}^N 2(y_i  - a - bx_i) (-1) = -2\frac{1}{N} \sum_{i=1}^N (y_i - \hat{y}_i)
\end{equation}

\begin{equation}
\frac{\partial MSE}{\partial a} = \frac{\partial MSE}{\partial \hat{y}_i} \frac{\partial  \hat{y}_i}{\partial a}  = \frac{1}{N} \sum_{i=1}^N 2(y_i  - a - bx_i) (-1) (x_i)= -2\frac{1}{N} \sum_{i=1}^N x_i(y_i - \hat{y}_i)
\end{equation}

Computing gradients w.r.t. parameters

### Step 3: Update the parameters

\begin{equation}
a = a - \alpha \frac{\partial MSE}{\partial a}
\end{equation}

\begin{equation}
b = b - \alpha \frac{\partial MSE}{\partial b}
\end{equation}

### Step 4: Loop for training

Go back to **Step 1** and restart the process. Repeating the process over and over, for many epochs is training a model

##Data generation

```
import numpy as np
from IPython.display import clear_output
import matplotlib.pyplot as plt
%matplotlib inline

# Data Generation
np.random.seed(42)
x = np.random.rand(100, 1)
y = 1 + 2 * x + .1 * np.random.randn(100, 1)

# Shuffles the indices
idx = np.arange(100)
np.random.shuffle(idx)

# Uses first 80 random indices for train
train_idx = idx[:80]
# Uses the remaining indices for validation
val_idx = idx[80:]

# Generates train and validation sets
x_train, y_train = x[train_idx], y[train_idx]
x_val, y_val = x[val_idx], y[val_idx]

plt.plot(x_train, y_train, 'bo')
plt.title('Data-Train')
plt.xlabel('x')
plt.xlabel('y')
plt.show()

plt.plot(x_val, y_val, 'ro')
plt.title('Data-Validation')
plt.xlabel('x')
plt.xlabel('y')
plt.show()

```

## Numpy

```
# Initializes parameters "a" and "b" randomly
np.random.seed(42)
a = np.random.randn(1)
b = np.random.randn(1)

print(' a and b after initialization')
print(a, b)

# Sets learning rate
lr = 1e-1
# Defines number of epochs
n_epochs = 1000

for epoch in range(n_epochs):
    # Computes our model's predicted output
    yhat = a + b * x_train

    # How wrong is our model? That's the error!
    error = (y_train - yhat)
    # It is a regression, so it computes mean squared error (MSE)
    loss = (error ** 2).mean()

    # Computes gradients for both "a" and "b" parameters
    a_grad = -2 * error.mean()
    b_grad = -2 * (x_train * error).mean()

    # Updates parameters using gradients and the learning rate
    a = a - lr * a_grad
    b = b - lr * b_grad

print(' a and b after gradient descent')
print(a, b)

# Sanity Check: do we get the same results as our gradient descent?
from sklearn.linear_model import LinearRegression
linr = LinearRegression()
linr.fit(x_train, y_train)
print(' a and b by scikit-learn')
print(linr.intercept_, linr.coef_[0])
```

     a and b after initialization
    [0.49671415] [-0.1382643]
     a and b after gradient descent
    [1.02354094] [1.96896411]
     a and b by scikit-learn
    [1.02354075] [1.96896447]

## Pytorch!

```
!pip install torchviz  # package for dynamic computation Graph
```

    Collecting torchviz
      Downloading https://files.pythonhosted.org/packages/8f/8e/a9630c7786b846d08b47714dd363a051f5e37b4ea0e534460d8cdfc1644b/torchviz-0.0.1.tar.gz (41kB)
         |████████████████████████████████| 51kB 3.4MB/s
    Requirement already satisfied: torch in /usr/local/lib/python3.6/dist-packages (from torchviz) (1.1.0)
    Requirement already satisfied: graphviz in /usr/local/lib/python3.6/dist-packages (from torchviz) (0.10.1)
    Requirement already satisfied: numpy in /usr/local/lib/python3.6/dist-packages (from torch->torchviz) (1.16.3)
    Building wheels for collected packages: torchviz
      Building wheel for torchviz (setup.py) ... done
      Stored in directory: /root/.cache/pip/wheels/2a/c2/c5/b8b4d0f7992c735f6db5bfa3c5f354cf36502037ca2b585667
    Successfully built torchviz
    Installing collected packages: torchviz
    Successfully installed torchviz-0.0.1

```
import torch
import torch.optim as optim
import torch.nn as nn
from torchviz import make_dot

print(torch.__version__)
torch.cuda.is_available()
```

    1.1.0

    True

###Loading Data, Devices and CUDA

```
# switch to False to use CPU
use_cuda = True

use_cuda = use_cuda and torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu");

# Our data was in Numpy arrays, but we need to transform them into PyTorch's Tensors
# and then we send them to the chosen device
x_train_tensor = torch.from_numpy(x_train).float().to(device)
y_train_tensor = torch.from_numpy(y_train).float().to(device)

# Here we can see the difference - notice that .type() is more useful
# since it also tells us WHERE the tensor is (device)
print(type(x_train), type(x_train_tensor), x_train_tensor.type())
```

    <class 'numpy.ndarray'> <class 'torch.Tensor'> torch.cuda.FloatTensor

### Dynamic computation graph

The [PyTorchViz](https://github.com/szagoruyko/pytorchviz) package and its make_dot(variable) method allows us to easily visualize a graph associated with a given Python variable.

```
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)

yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()

make_dot(yhat)
```

```
make_dot(error)
```

```
make_dot(loss)
```

Let’s take a closer look at its components:

* blue boxes: these correspond to the tensors we use as parameters, the ones we’re asking PyTorch to compute gradients for;
* gray box: a Python operation that involves a gradient-computing tensor or its dependencies;
* green box: the same as the gray box, except it is the starting point for the computation of gradients (assuming the backward()method is called from the variable used to visualize the graph)— they are computed from the bottom-up in a graph.

### Autograd

*(No more manual computation of gradients!)*

[Autograd](https://pytorch.org/docs/stable/autograd) is PyTorch’s automatic differentiation package. Thanks to it, we don’t need to worry about partial derivatives, chain rule or anything like it.

* backward()
* grad()
* no.grad()
* grad.zero()

```
lr = 1e-1
n_epochs = 1000

torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)

print(' a and b after initialization')
print(a, b)

for epoch in range(n_epochs):
    yhat = a + b * x_train_tensor
    error = y_train_tensor - yhat
    loss = (error ** 2).mean()

    # No more manual computation of gradients!
    # a_grad = -2 * error.mean()
    # b_grad = -2 * (x_tensor * error).mean()

    # We just tell PyTorch to work its way BACKWARDS from the specified loss!
    loss.backward()
    # Let's check the computed gradients...
    #print(a.grad)
    #print(b.grad)

    # What about UPDATING the parameters? Not so fast...

    # FIRST ATTEMPT
    # AttributeError: 'NoneType' object has no attribute 'zero_'
    # a = a - lr * a.grad
    # b = b - lr * b.grad
    # print(a)

    # SECOND ATTEMPT
    # RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.
    # a -= lr * a.grad
    # b -= lr * b.grad

    # THIRD ATTEMPT
    # We need to use NO_GRAD to keep the update out of the gradient computation
    # Why is that? It boils down to the DYNAMIC GRAPH that PyTorch uses...
    with torch.no_grad():
        a -= lr * a.grad
        b -= lr * b.grad

    # PyTorch is "clingy" to its computed gradients, we need to tell it to let it go...
    a.grad.zero_()
    b.grad.zero_()

print(' a and b after gradient descent using Pytorch ')
print(a, b)
```

     a and b after initialization
    tensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)
     a and b after gradient descent using Pytorch
    tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)

### Optimizer

So far, we’ve been manually updating the parameters using the computed gradients. That’s probably fine for two parameters… but what if we had a whole lot of them?! We use one of PyTorch’s [optimizers](https://pytorch.org/docs/stable/optim.html#module-torch.optim), like [SGD](https://pytorch.org/docs/stable/optim.html#torch.optim.SGD) or [Adam](https://pytorch.org/docs/stable/optim.html#torch.optim.Adam).

* step()  # No more manual update!
* zero_grad() # No more telling PyTorch to let gradients go!

```
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)

print(' a and b after initialization')
print(a, b)

lr = 1e-1
n_epochs = 1000

# Defines a SGD optimizer to update the parameters
optimizer = optim.SGD([a, b], lr=lr)

for epoch in range(n_epochs):
    yhat = a + b * x_train_tensor
    error = y_train_tensor - yhat
    loss = (error ** 2).mean()

    loss.backward()

    # No more manual update!
    # with torch.no_grad():
    #     a -= lr * a.grad
    #     b -= lr * b.grad
    optimizer.step()

    # No more telling PyTorch to let gradients go!
    # a.grad.zero_()
    # b.grad.zero_()
    optimizer.zero_grad()

print(' a and b after gradient descent using Pytorch')
print(a, b)
```

     a and b after initialization
    tensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)
     a and b after gradient descent using Pytorch
    tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)

### Loss

We now tackle the loss computation. As expected, PyTorch got us covered once again. There are many  [loss](https://pytorch.org/docs/stable/nn.html#loss-functions) functions to choose from, depending on the task at hand. Since ours is a regression, we are using the Mean Square Error (MSE) loss.

Notice that **nn.MSELoss** actually creates a loss function for us — it is NOT the loss function itself. Moreover, you can specify a reduction method to be applied, that is, how do you want to aggregate the results for individual points — you can average them (reduction=’mean’) or simply sum them up (reduction=’sum’)

* nn.MSELoss   # No more manual loss!

```
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)

print(' a and b after initialization')
print(a, b)

lr = 1e-1
n_epochs = 1000

# Defines a MSE loss function
loss_fn = nn.MSELoss(reduction='mean')

optimizer = optim.SGD([a, b], lr=lr)

for epoch in range(n_epochs):
    yhat = a + b * x_train_tensor

    # No more manual loss!
    # error = y_tensor - yhat
    # loss = (error ** 2).mean()
    loss = loss_fn(y_train_tensor, yhat)

    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

print(' a and b after gradient descent')
print(a, b)
```

     a and b after initialization
    tensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)
     a and b after gradient descent
    tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)

### Model

*No more manual prediction!  forward*

In PyTorch, a model is represented by a regular Python class that inherits from the  [Module](https://pytorch.org/docs/stable/nn.html#torch.nn.Module) class.

The most fundamental methods it needs to implement are:

*  **__init__(self)**: it defines the parts that make up the model —in our case, two parameters, a and b.

> You are not limited to defining parameters, though… models can contain other models (or layers) as its attributes as well, so you can easily nest them. We’ll see an example of this shortly as well.

* **forward(self, x)**: it performs the actual computation, that is, it outputs a prediction, given the input x.

> You should NOT call the forward(x) method, though. You should call the whole model itself, as in model(x) to perform a forward pass and output predictions.

```
class ManualLinearRegression(nn.Module):
    def __init__(self):
        super().__init__()
        # To make "a" and "b" real parameters of the model, we need to wrap them with nn.Parameter
        self.a = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))
        self.b = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))

    def forward(self, x):
        # Computes the outputs / predictions
        return self.a + self.b * x
```

**IMPORTANT**: we need to **send our model to the same device where the data is**. If our data is made of GPU tensors, our model must “live” inside the GPU as well.

```
torch.manual_seed(42)

# Now we can create a model and send it at once to the device
model = ManualLinearRegression().to(device)
# We can also inspect its parameters using its state_dict
print(model.state_dict())

lr = 1e-1
n_epochs = 1000

loss_fn = nn.MSELoss(reduction='mean')
optimizer = optim.SGD(model.parameters(), lr=lr)

for epoch in range(n_epochs):
    # What is this?!?
    model.train()

    # No more manual prediction!
    # yhat = a + b * x_tensor
    yhat = model(x_train_tensor)

    loss = loss_fn(y_train_tensor, yhat)
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

print(model.state_dict())
```

    OrderedDict([('a', tensor([0.3367], device='cuda:0')), ('b', tensor([0.1288], device='cuda:0'))])
    OrderedDict([('a', tensor([1.0235], device='cuda:0')), ('b', tensor([1.9690], device='cuda:0'))])

**What is this?!?**

In PyTorch, models have a **train() method which, somewhat disappointingly, does NOT perform a training step. Its only purpose is to set the model to training mode. Why is this important? Some models may use mechanisms like Dropout, for instance, which have distinct behaviors in training and evaluation phases.**

### Nested Models

In our model, we manually created two parameters to perform a linear regression. Let’s use PyTorch’s [Linear](https://pytorch.org/docs/stable/nn.html#torch.nn.Linear) model as an attribute of our own, thus creating a nested model.

Even though this clearly is a contrived example, as we are pretty much wrapping the underlying model without adding anything useful (or, at all!) to it, it illustrates well the concept.

In the ** __init__ ** method, we created an attribute that contains our nested Linear model.

In the **forward(**) method, we call the nested model itself to perform the forward pass (notice, we are not *calling self.linear.forward(x)*).

```
class LayerLinearRegression(nn.Module):
    def __init__(self):
        super().__init__()
        # Instead of our custom parameters, we use a Linear layer with single input and single output
        self.linear = nn.Linear(1, 1)

    def forward(self, x):
        # Now it only takes a call to the layer to make predictions
        return self.linear(x)
```

### Sequential Models

For straightforward models, that use run-of-the-mill layers, where the output of a layer is sequentially fed as an input to the next, we can use a  [Sequential](https://pytorch.org/docs/stable/nn.html#torch.nn.Sequential) model

```
torch.manual_seed(42)
# Alternatively, you can use a Sequential model
model = nn.Sequential(nn.Linear(1, 1)).to(device)
# We can also inspect its parameters using its state_dict
print(model.state_dict())
```

    OrderedDict([('0.weight', tensor([[0.7645]], device='cuda:0')), ('0.bias', tensor([0.8300], device='cuda:0'))])

### Training Step

So far, we’ve defined an **optimizer**, a **loss function** and a **model**. Scroll up a bit and take a quick look at the code inside the loop. Would it change if we were using a different optimizer, or loss, or even model? If not, how can we make it more generic?

Well, I guess we could say all these lines of code **perform a training step**, given those **three elements (optimizer, loss and model)**,the **features** and the **labels**.

```
torch.manual_seed(42)
# Alternatively, you can use a Sequential model
model = nn.Sequential(nn.Linear(1, 1)).to(device)
# We can also inspect its parameters using its state_dict
print(model.state_dict())

lr = 1e-1
n_epochs = 1000

loss_fn = nn.MSELoss(reduction='mean')
optimizer = optim.SGD(model.parameters(), lr=lr)

def make_train_step(model, loss_fn, optimizer):
    # Builds function that performs a step in the train loop
    def train_step(x, y):
        # Sets model to TRAIN mode
        model.train()
        # Makes predictions
        yhat = model(x)
        # Computes loss
        loss = loss_fn(y, yhat)
        # Computes gradients
        loss.backward()
        # Updates parameters and zeroes gradients
        optimizer.step()
        optimizer.zero_grad()
        # Returns the loss
        return loss.item()

    # Returns the function that will be called inside the train loop
    return train_step

# Creates the train_step function for our model, loss function and optimizer
train_step = make_train_step(model, loss_fn, optimizer)
losses = []

# For each epoch...
for epoch in range(n_epochs):
    # Performs one train step and returns the corresponding loss
    loss = train_step(x_train_tensor, y_train_tensor)
    losses.append(loss)

# Checks model's parameters
print(model.state_dict())
```

    OrderedDict([('0.weight', tensor([[0.7645]], device='cuda:0')), ('0.bias', tensor([0.8300], device='cuda:0'))])
    OrderedDict([('0.weight', tensor([[1.9690]], device='cuda:0')), ('0.bias', tensor([1.0235], device='cuda:0'))])


# A Detailed RBC Model Example

Consider the equilibrium conditions for a basic RBC model without labor:

\begin{align}
C_t^{-\sigma} & = \beta E_t \left[C_{t+1}^{-\sigma}(\alpha A_{t+1} K_{t+1}^{\alpha-1} + 1 - \delta)\right]\\
Y_t & = A_t K_t^{\alpha}\\
I_t & = K_{t+1} - (1-\delta)K_t\\
Y_t & = C_t + I_t\\
\log A_t & = \rho_a \log A_{t-1} + \epsilon_t
\end{align}

In the nonstochastic steady state, we have:

\begin{align}
K & = \left(\frac{\alpha A}{1/\beta+\delta-1}\right)^{\frac{1}{1-\alpha}}\\
Y & = AK^{\alpha}\\
I & = \delta K\\
C & = Y - I
\end{align}

Given values for the parameters $\beta$, $\sigma$, $\alpha$, $\delta$, and $A$, steady state values of capital,  output, investment, and consumption are easily computed.

## Import requisite modules

```python
# Import numpy, pandas, linearsolve, matplotlib.pyplot
import numpy as np
import pandas as pd
import linearsolve as ls
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
```

## Initializing the model in `linearsolve`

To initialize the model, we need to first set the model's parameters. We do this by creating a Pandas Series variable called `parameters`:

```python
# Input model parameters
parameters = pd.Series()
parameters['alpha']  = .35
parameters['beta']  = 0.99
parameters['delta']   = 0.025
parameters['rhoa'] = .9
parameters['sigma'] = 1.5
parameters['A'] = 1
```

Next, we need to define a function that returns the equilibrium conditions of the model. The function will take as inputs two vectors: one vector of "current" variables and another of "forward-looking" or one-period-ahead variables. The function will return an array that represents the equilibirum conditions of the model. We'll enter each equation with all variables moved to one side of the equals sign. For example, here's how we'll enter the produciton fucntion:

`production_function = technology_current*capital_current**alpha - output_curent`

Here the variable `production_function` stores the production function equation set equal to zero. We can enter the equations in almost any way we want. For example, we could also have entered the production function this way:

`production_function = 1 - output_curent/technology_current/capital_current**alpha`

One more thing to consider: the natural log in the equation describing the evolution of total factor productivity will create problems for the solution routine later on. So rewrite the equation as:

\begin{align}
A_{t+1} & =  A_{t}^{\rho_a}e^{\epsilon_{t+1}}\\
\end{align}

So the complete system of equations that we enter into the program looks like:

\begin{align}
C_t^{-\sigma} & = \beta E_t \left[C_{t+1}^{-\sigma}(\alpha Y_{t+1} /K_{t+1}+ 1 - \delta)\right]\\
Y_t & = A_t K_t^{\alpha}\\
I_t & = K_{t+1} - (1-\delta)K_t\\
Y_t & = C_t + I_t\\
A_{t+1} & =  A_{t}^{\rho_a}e^{\epsilon_{t+1}}
\end{align}

Now let's define the function that returns the equilibrium conditions:

```python
# Define function to compute equilibrium conditions
def equilibrium_equations(variables_forward,variables_current,parameters):

    # Parameters
    p = parameters

    # Variables
    fwd = variables_forward
    cur = variables_current

    # Household Euler equation
    euler_eqn = p.beta*fwd.c**-p.sigma*(p.alpha*fwd.y/fwd.k+1-p.delta) - cur.c**-p.sigma

    # Production function
    production_fuction =  cur.a*cur.k**p.alpha - cur.y

    # Capital evolution
    capital_evolution = fwd.k - (1-p.delta)*cur.k - cur.i

    # Goods market clearing
    market_clearing = cur.c + cur.i - cur.y

    # Exogenous technology
    technology_proc = cur.a**p.rhoa- fwd.a

    # Stack equilibrium conditions into a numpy array
    return np.array([
            euler_eqn,
            production_fuction,
            capital_evolution,
            market_clearing,
            technology_proc
        ])
```

Notice that inside the function we have to define the variables of the model form the elements of the input vectors `variables_forward` and `variables_current`. It is *essential* that the predetermined or state variables are ordered first.

## Initializing the model

To initialize the model, we need to specify the number of state variables in the model, the names of the endogenous varaibles in the same order used in the `equilibrium_equations` function, and the names of the exogenous shocks to the model.

```python
# Initialize the model
rbc = ls.model(equations = equilibrium_equations,
               n_states=2,
               var_names=['a','k','c','y','i'],
               shock_names=['e_a','e_k'],
               parameters=parameters)
```

The solution routine solves the model as if there were a separate exogenous shock for each state variable and that's why I initialized the model with two exogenous shocks `e_a` and `e_k` even though the RBC model only has one exogenous shock.

## Steady state

Next, we need to compute the nonstochastic steady state of the model. The `.compute_ss()` method can be used to compute the steady state numerically. The method's default is to use scipy's `fsolve()` function, but other scipy root-finding functions can be used: `root`, `broyden1`, and `broyden2`. The optional argument `options` lets the user pass keywords directly to the optimization function. Check out the documentation for Scipy's nonlinear solvers here: http://docs.scipy.org/doc/scipy/reference/optimize.html

```python
# Compute the steady state numerically
guess = [1,1,1,1,1]
rbc.compute_ss(guess)
print(rbc.ss)
```

    a     1.000000
    k    34.398226
    c     2.589794
    y     3.449750
    i     0.859956
    dtype: float64

Note that the steady state is returned as a Pandas Series. Alternatively, you could compute the steady state directly and then sent the `rbc.ss` attribute:

```python
# Steady state solution
p = parameters
K = (p.alpha*p.A/(1/p.beta+p.delta-1))**(1/(1-p.alpha))
C = p.A*K**p.alpha - p.delta*K
Y = p.A*K**p.alpha
I = Y - C

rbc.set_ss([p.A,K,C,Y,I])
print(rbc.ss)
```

    a     1.000000
    k    34.398226
    c     2.589794
    y     3.449750
    i     0.859956
    dtype: float64

## Log-linearization and solution

Now we use the `.log_linear()` method to find the log-linear appxoximation to the model's equilibrium conditions. That is, we'll transform the nonlinear model into a linear model in which all variables are expressed as log-deviations from the steady state. Specifically, we'll compute the matrices $A$ and $B$ that satisfy:

\begin{align}
A E_t\left[ x_{t+1} \right] & = B x_t + \left[ \begin{array}{c} \epsilon_{t+1} \\ 0 \end{array} \right],
\end{align}

where the vector $x_{t}$ denotes the log deviation of the endogenous variables from their steady state values.

```python
# Find the log-linear approximation around the non-stochastic steady state
rbc.log_linear_approximation()

print('The matrix A:\n\n',np.around(rbc.a,4),'\n\n')
print('The matrix B:\n\n',np.around(rbc.b,4))
```

    The matrix A:

     [[ 0.00000e+00 -8.30000e-03 -3.59900e-01  8.30000e-03  0.00000e+00]
     [ 0.00000e+00  0.00000e+00  0.00000e+00  0.00000e+00  0.00000e+00]
     [ 0.00000e+00  3.43982e+01  0.00000e+00  0.00000e+00  0.00000e+00]
     [ 0.00000e+00  0.00000e+00  0.00000e+00  0.00000e+00  0.00000e+00]
     [-1.00000e+00  0.00000e+00  0.00000e+00  0.00000e+00  0.00000e+00]]

    The matrix B:

     [[-0.     -0.     -0.3599 -0.     -0.    ]
     [-3.4497 -1.2074 -0.      3.4497 -0.    ]
     [-0.     33.5383 -0.     -0.      0.86  ]
     [-0.     -0.     -2.5898  3.4497 -0.86  ]
     [-0.9    -0.     -0.     -0.     -0.    ]]

Finally, we need to obtain the *solution* to the log-linearized model. The solution is a pair of matrices $F$ and $P$ that specify:

1. The current values of the non-state variables $u_{t}$ as a linear function of the previous values of the state variables $s_t$.
1. The future values of the state variables $s_{t+1}$ as a linear function of the previous values of the state variables $s_t$ and the future realisation of the exogenous shock process $\epsilon_{t+1}$.

\begin{align}
u_t  &  = Fs_t\\
s_{t+1} & = Ps_t + \epsilon_{t+1}.
\end{align}

We use the `.klein()` method to find the solution.

```python
# Solve the model
rbc.solve_klein(rbc.a,rbc.b)

# Display the output
print('The matrix F:\n\n',np.around(rbc.f,4),'\n\n')
print('The matrix P:\n\n',np.around(rbc.p,4))
```

    The matrix F:

     [[ 0.2297  0.513 ]
     [ 1.      0.35  ]
     [ 3.3197 -0.1408]]

    The matrix P:

     [[0.9    0.    ]
     [0.083  0.9715]]

## Impulse responses

One the model is solved, use the `.impulse()` method to compute impulse responses to exogenous shocks to the state. The method creates the `.irs` attribute which is a dictionary with keys equal to the names of the exogenous shocks and the values are Pandas DataFrames with the computed impulse respones. You can supply your own values for the shocks, but the default is 0.01 for each exogenous shock.

```python
# Compute impulse responses and plot
rbc.impulse(T=41,t0=1,shocks=None,percent=True)

print('Impulse responses to a 0.01 unit shock to A:\n\n',rbc.irs['e_a'].head())
```

    Impulse responses to a 0.01 unit shock to A:

        e_a      a         k         c         y         i
    0  0.0  0.000  0.000000  0.000000  0.000000  0.000000
    1  1.0  1.000  0.000000  0.229718  1.000000  3.319739
    2  0.0  0.900  0.082993  0.249318  0.929048  2.976083
    3  0.0  0.810  0.155321  0.265744  0.864362  2.667127
    4  0.0  0.729  0.218116  0.279348  0.805341  2.389390

Plotting is easy.

```python
rbc.irs['e_a'][['a','k','c','y','i']].plot(lw='5',alpha=0.5,grid=True).legend(loc='upper right',ncol=2)
rbc.irs['e_a'][['e_a','a']].plot(lw='5',alpha=0.5,grid=True).legend(loc='upper right',ncol=2)
```

## Stochastic simulation

Creating a stochastic simulation of the model is straightforward with the `.stoch_sim()` method. In the following example, I create a 151 period (including t=0) simulation by first simulating the model for 251 periods and then dropping the first 100 values. The variance of the shock to $A_t$ is set to 0.001 and the variance of the shock to $K_t$ is set to zero because there is not capital shock in the model. The seed for the numpy random number generator is set to 0.

```python
rbc.stoch_sim(T=121,drop_first=100,cov_mat=np.array([[0.00763**2,0],[0,0]]),seed=0,percent=True)
rbc.simulated[['k','c','y','i']].plot(lw='5',alpha=0.5,grid=True).legend(loc='upper right',ncol=4)
rbc.simulated[['a']].plot(lw='5',alpha=0.5,grid=True).legend(ncol=4)
rbc.simulated[['e_a','e_k']].plot(lw='5',alpha=0.5,grid=True).legend(ncol=4)
```


```python
import numpy as np
import matplotlib.pyplot as plt
```

---

# Item XII

Let $\ddot{y}(t)-\mu (1-y^2(t)) \dot{y}(t) + y(t) = 0$, with $y(0)=2$ , $\dot{y}(0)=0$, and $\mu = 1234$.
1. Approximate the solution by means of a Taylor series expansion about $t=0$.
2. Implement a numerical solver for it. *You may not use scipy but you can use numpy*.
3. Compare both solutions and comment on the comparison.
---

## Part 1

We do the approximation:
\begin{align}
y(t) &= y(0) + t \dot{y}(0) + \frac{1}{2} t^2 \ddot{y}(0)
\\ &= 2 + \frac{1}{2}t^2\ddot{y}(0) \,.
\end{align}

In order to compute $\ddot{y}(0)$ we use the equation given, at $t=0$:
\begin{align}
\ddot{y}(0) - \mu(1-y^2(0)) \dot{y}(0) + y(0) &= 0
\\ \ddot{y}(0) &= -y(0)
\\ &= -2 \, .
\end{align}

And the approximation is $$y(t) = 2-t^2 \,.$$

## Part 2

```python
# Euler's method as a higher order function:
def euler(yp=lambda y,t:1,yi=0,tmin=0,tmax=1.0,steps=400):
    delta = (tmax-tmin)/float(steps)
    t = tmin
    ys = []
    ys.append(yi)
    for i in range(steps):
        t = tmin+delta*i
        yp_next = yp(ys[-1],t)
        y_next = ys[-1]+delta*yp_next
        ys.append(y_next)
    return ys
```

We use it in the following way:

$$
\begin{bmatrix} y_{t+1} \\ \dot{y}_{y+1} \end{bmatrix}
=
\begin{bmatrix} \dot{y}_{t} \\ \ddot{y}(t) \end{bmatrix} \Delta t
+
\begin{bmatrix} y_{t} \\ \dot{y}_t \end{bmatrix} \Delta t
$$

remembering that
$$
\ddot{y}(t) = \mu (1-y^2(t)) \dot{y}(t) - y(t) = 0
$$

```python
mu = 1234
N = 40000
YF = 4.0
xs = np.linspace(0,YF,num=N+1)
# --- Approximation with Taylor
y_tay = 2-xs**2.0
# --- Solution:
fn = lambda ys,t: np.array([ys[1], mu*(1-ys[0]**2)*ys[1] - ys[0]])
yini = np.array([2,0])
result = euler(fn,yini,tmin=0,tmax=YF,steps=N)
y_sol = [a[0] for a in result]
yp_sol = [a[1] for a in result]
# --- Print values at some points:
NS = (N//4,N//2,N*3//4,N)
print("Taylor:")
for i in NS:
    print("\ty(%f) = %f"%(xs[i],y_tay[i]))
print("Euler's:")
for i in NS:
    print("\ty(%f) = %f"%(xs[i],y_sol[i]))
```

    Taylor:
    	y(1.000000) = 1.000000
    	y(2.000000) = -2.000000
    	y(3.000000) = -7.000000
    	y(4.000000) = -14.000000
    Euler's:
    	y(1.000000) = 1.999460
    	y(2.000000) = 1.998919
    	y(3.000000) = 1.998378
    	y(4.000000) = 1.997837

## Part 3

Plot both solutions:

```python
plt.plot(xs,y_tay,label="Taylor approx.")
plt.plot(xs,y_sol,label="Euler's method")
plt.grid()
plt.legend()
plt.show()
```

It can be seen that the Talor's approximation is bad as it doens't consider that $y''(t)$ is really small except on $t=0$ (due the large value of $\mu$).

```python
plt.plot(xs,yp_sol,label="Euler's y'(t)")
plt.grid()
plt.legend()
plt.show()
```

```python

```


# Likevektskonsentrasjoner (Eksamen høst 2020, oppgave 33(c))
Fosgen (COCl$_2$) er en svært giftig gass som ble brukt som stridsgass under første verdenskrig. I dag er fosgen
et viktig utgangskjemikalie som kan brukes til å fremstille polykarbonat som bl.a. brukes til å lage "uknuselige"
butikkvinduer.

Fosgen dekomponerer etter følgende reaksjonslignin:

$$\text{COCl}_2(\text{g}) + \rightleftharpoons \text{CO} (\text{g}) + \text{Cl}_2 (\text{g}),$$

der likevektskonstanten ved 1100 K er $K_{p} = 1.6 \times 10^{2}.$

**Hvis rent fosgen med et initielt trykk på 1.0 atm dekomponerer ved 1100 K, hva blir trykket av de ulike gassene ved likevekt?**

Vi skal løse denne oppgaven ved å bruke Python. For å kunne regne symbolsk skal vi bruke et bibliotek som heter [SymPy](https://www.sympy.org/):

```python
import sympy as sym  # Importer SymPy
```

```python
# Definer størrelsene vi kjenner
START_COCl2 = 1.0
KP = 1.6e2
```

Over har vi listet opp hva vi kjenner. La oss også liste opp alle de ukjente trykkene som vi skal bestemme:
- $P_{\text{COCl}_2}$
- $P_{\text{CO}}$
- $P_{\text{Cl}_{2}}$

Vi har altså tre ukjente. La oss definere de som størrelser (spesifikt som [SymPy-symboler](https://docs.sympy.org/latest/tutorial/intro.html#a-more-interesting-example)) slik at vi kan regne med de (dette blir litt som når vi introduserer $x$ osv. for ukjente størrelser i ligninger vi skriver for hånd):

```python
# Vi definerer de ukjente størrelsene:
p_COCl2, p_CO, p_Cl2 = sym.symbols('p_COCl2 p_CO p_Cl2')
```

Vi har nå definert konsentrasjonene. Disse er foreløpig ukjente. For å bestemme de, så trenger vi noen ligninger som relaterer de til hverandre. Mulige slike ligninger er:
- likevektskonstanten
- massebalanser

```python
# La oss begynne med likevektskonstanten:
ligning1 = sym.Eq((p_CO * p_Cl2)/p_COCl2, KP)
```

Vi kan be SymPy skrive ut hva denne ligningen er, for å sjekke at den ser ut som vi hadde tenkt:

```python
ligning1
```

Her har vi tre grunnstoffer i forbindelsene som er involvert i reaksjonen, C, O og Cl. Vi kan derfor lage tre molbalanser for disse grunnstoffene. Her bruker vi prinsippet om at grunnstoffer ikke forsvinner i kjemiske reaksjoner. Dvs. hvis vi vet at vi har $n$ mol av grunnstoff $X$ før likevekt innstilles, så må vi fortsatt ha $n$ mol av grunnstoffet $X$ når likevekten er innstilt.

Vi lager tre molbalanser:
- En for karbon:
  $$n_{\text{COCl}_2,{\text{start}}} = n_{\text{COCl}_2,{\text{likevekt}}} + n_{\text{CO},{\text{likevekt}}}$$

- En for oksygen:
  $$n_{\text{COCl}_2,{\text{start}}} = n_{\text{COCl}_2,{\text{likevekt}}} + n_{\text{CO},{\text{likevekt}}}$$

- En for klor:
  $$2 n_{\text{COCl}_2,{\text{start}}} = 2 n_{\text{COCl}_2,{\text{likevekt}}} + 2n_{\text{Cl}_2,{\text{likevekt}}}$$

Her ser vi at ligningen for karbon og oksygen er identisk. Det betyr at de tre molbalansene i virkeligheten bare gir
to uavhengige ligninger (f.eks. den for karbon og den for klor).

Over skrev vi molbalanser, men her bruker vi trykk. Vi kan regne om ved å bruke ideell gasslov: $P = \frac{nRT}{v}$. Her blir dette bare en felles faktor som vi ganger ligningene over med. Med andre ord, vi skal skrive de direkte med trykk. La oss gjøre det:

```python
ligning2 = sym.Eq(START_COCl2,  p_COCl2 + p_CO)  # for karbon
ligning3 = sym.Eq(START_COCl2, p_COCl2 + p_Cl2)  # for oksygen/klor
```

```python
# Skriv ut for dobbeltsjekk:
ligning2
```

```python
ligning3
```

Vi har nå tre ligninger og vi har tre ukjente. Dette kan vi (eller i dette tilfellet, SymPy) løse:

```python
løsninger = sym.solve([ligning1, ligning2, ligning3], [p_COCl2, p_CO, p_Cl2], dict=True)
```

```python
løsninger
```

En av løsningene SymPy fant gir negative trykk. Dette er en ugyldig løsning og vi beholder bare den som har kun positive løsninger:

```python
# Vis gyldige løsninger:
gyldige = []
for løsning in løsninger:
    if all(i > 0 for i in løsning.values()):
        gyldige.append(løsning)
        print('Gyldig løsning:')
        print(f'- P(COCl2) = {løsning.get(p_COCl2):.3g} atm')
        print(f'- P(CO) = {løsning.get(p_CO):.3g} atm')
        print(f'- P(Cl2) = {løsning.get(p_Cl2):.3g} atm')
```

Her kan vi sammenlikne med løsningsforslaget:
- $P_{\text{COCl}_2} = 0.006$ atm,
- $P_{\text{CO}} = 0.994$ atm,
- $P_{\text{Cl}_{2}} = 0.994$ atm,


This tutorial covers the basics of python programming. It introduces some of the built-in python module and how to use them.

Author: Collab.

# The Numpy Array Object

Numpy is a fundamental scientific package (module) for multi-dimensional arrays. Arrays are similar to lists, but the elements of an array must be of the same type, e.g, float, int or string. To begin with, we need to import the numpy package. In most cases np is used as an abbreviation for numpy.

```python
import numpy as np
```

## Creating Numpy Arrays

Numpy arrays can be created in various fashions. They can be created manually or initialised using the in-built functions, e.g np.array(), np.zeros(), np.ones(), np.arange(), np.linspace, np.indices. The in-built function np.array does not require any parameter and it returns an empty array. Given the desired shape, np.zeros returns an array filled with zeros. For instance, np.zeros((2,2)) returns a 2×2 array filled with zeros. Other parameters also can be included such as type and order; they are optional paremters. Same applies to np.ones, but now the array is filled with ones instead. np.arange takes start, stop and step values as parameters. It returns an array with regularly specified incrementing values, the default start is 0 and the default increment is 1. np.linspace returns an array with evenly spaced elements over a specified interval. np.indices() acts like a grid, it returns an array representing the indices of a grid.

Let us give it a try!

```python
A= np.array([[2,3,4],[6,7,8]],dtype='int')
A
```

```python
B = np.zeros((2,3))
B
```

```python
C = np.ones((2,4),dtype='float')
C
```

```python
D = np.arange(2,10)
D
```

```python
E = np.linspace(0,1,30)
E
```

```python
F = np.indices((3,3))
F
```

Once we have initialised an array, we can get hold of its shape, dimension and type of the elements.

A.shape: shape of array A

A.ndim: dimension of array A

A.dtype: type of the elements in array A

```python
A.shape, A.ndim, A.dtype
```

## Array Manipulation

### Basic Operations

Numpy arrays basically behave similar to matrices but they are not matrices. The basic operations such as addition, subtraction and multiplication can be easily performed on them. Note that if it is a binary operation, then the numpy arrays involved must be of the same shape.

```python
A = np.array([(1,7,3),(4,5,6)]) ;B = np.array([(8,12,1),(6,9,11)])
A ** 2 #Squaring the elements in the array
```

```python
A + B # Adding two numpy array
```

```python
A - B # Subtracting two numpy arrays
```

```python
A * B # Elementwise multiplication of two numpy arrays
```

```python
np.dot(A,B.T) # matrix multiplication; B.T returns the transpose of B
```

### Shape Manipulation

The shape of a numpy array is given by the number of elements along the individual axis and it can be manipulated through various commands.

```python
A = np.arange(8) # one dimensional array of shape (8,)
```

```python
B = np.arange(2,10) # one dimensional array of shape (8,)
```

```python
A.reshape(4,2) #changing the shape of A to (4,2)
```

```python
np.concatenate((A,B),axis=0) #concatening two one-dimensional numpy arrays
```

```python
np.hstack((A,B)) #stack arrays in sequence horizontally (column wise); similar to concatenate
```

```python
np.vstack((A,B)) #stack arrays in sequence horizontally (row wise).
```

```python
np.dstack((A,B)) #stack arrays in sequence depth wise (along third dimension)
```

```python
C = A.reshape(2,4) #reshaping the array
print (C)
np.vsplit(C,2) # spliting the array into subarrays
```

```python
np.append(A,17) #adding a value (17) at the end of array A
```

```python
np.append(17,A) #adding a value (17) at the beginning of array A
```

```python
np.append(C,10) #appending an element to a (4,2) array. This returns a 1-dimensional array
```

### Indexing and Slicing

Indexing refers to the process of assigning indinces or positions of an elemnt in the array. Slicing is taking out a desired portion or subarray of the array. The basic slice syntax is $i:j:k$ where $i$ is the starting index, $j$ is the stopping index, and $k$ is the step ($k≠0$). The starting index is zero and the end index is $n−1$ for an n-dimensional array.

```python
A = np.array([(2,4,5,6),(7,10,23,13),(45,0,23,5)])
```

```python
A[0] # returns the first row of the array
```

```python
A[0,:] #returns the first row of the array
```

```python
A[:,1] #returns the second column of the array
```

```python
A[:,-1] #returns the last column of the array
```

```python
A[0,0] #returns the first element of the array
```

```python
A[0:2,1:3] # returns subarray ranging from first to second row and second to third column
```

The dimension of the array can be increased by using new axis.

```python
a = A[np.newaxis,:]
b = A[:,np.newaxis]
a.shape, b.shape
```

### Statistical Functions

```python
np.average(A) #returns the average value
```

```python
np.mean(A) #returns the mean value
```

```python
np.median(A) #returns the median
```

```python
np.var(A) # returns the variance
```

```python
np.std(A) #returns the standard deviation
```

```python
np.cov(A) #returns the covariance
```

### Linear Algebra Functions

```python
a = np.array([(2,3),(4,5)])
b = np.array([(7,1),(3,9)])
```

```python
np.inner(a,b) #returns the inner product of two arrays
```

```python
np.kron(a,b) #Kronecker product of two array
```

```python
np.linalg.eig(a) #returns the eingenvalues and right eigenvectors of a square array
```

```python
np.linalg.eigh(a) #returns the eigenvalues and eigenvectors of a Hermitian or symmetric matrix
```

```python
np.linalg.eigvals(a) #returns the eigenvalues of a matrix
```

```python
np.linalg.eigvalsh(a) #returns the eigenvalues of a Hermitian or symmetric matrix
```

```python
np.linalg.det(a) #computes the determinant of the matrix
```

```python
np.trace(a) #returns the trace of the matrix
```

```python
np.linalg.inv(a) #returns the inverse of the matrix
```

### Other Functions

```python
A = np.array([3,6,3,1,7,4,2,8])
np.copy(A) # returns a copy of the array
```

```python
np.sort(A) # sorting the array in ascending order
```

```python
np.sort(A)[::-1] # sorting the array in descending order
```

```python
np.argsort(A) #returns the indices that would sort an array.
```

```python
np.min(A) #returns the maximum element
```

```python
np.max(A) #returns the maximum element
```

```python
np.argmax(A) #returns the index of the maximum element
```

```python
np.argwhere(A>3) #returns all the indices whose values are greater than 3
```

```python
np.mat(A) #returns matrix format of the array
```

```python
np.sum(A) #returns the sum of all the elements
```

```python
np.cumsum(A) #returns the sum of all the elements
```

```python
np.prod(A) #returns the product of all the elements
```

# Matplotlib Pyplot- 2D Plotting

Matplotlib is a python plotting library with excellent 2D and 3D graphics.

We need to import matplotlib library to be able to access the library.

```python
%matplotlib inline
import matplotlib.pyplot as plt
```

## Simple Plots

```python
x = np.linspace(0,3,100)
y = np.sin(x)
plt.xlabel('x')
plt.ylabel('sin x')
plt.plot(x,y)
plt.show()
```

The sin curve can be enhanced in various fashion. By default, the color is blue and the marker is a straight line. The basic in-built colors in matplotlib are: b-blue, g-green, r-red, c-cyan, m-magenta, y-yellow, k-black, w-white. For more information, visit http://matplotlib.org/api/colors_api.html.

```python
# Comparing the distance covered by Mark,and Sam over an interval of time
time = np.arange(0,50,2)
dMark = np.array([0,20,40,59,76,93,109,125,139,152,162,172,181,189,197,205,213,220,227,233,239,243,247,251,253])
dSam = np.array([0,25,49,71,91,110,129,150,169,188,205,222,237,249,261,271,281,290,298,306,313,320,325,329,331])
plt.plot(time,dMark,'b*',label='Mark')
plt.plot(time,dSam,'r*',label='Sam')
plt.xlabel('Time in minutes')
plt.ylabel('Distance covered in metres')
plt.legend(loc='upper left')
plt.xlim(0,50)
plt.ylim(0,350)
plt.show()
```

## Scatter Plots

```python
# Making a scatter plot from random distributions
x = np.arange(20)
y = np.random.rand(20)
z = np.random.rand(20)
plt.scatter(x,y,s=50,facecolor='b',edgecolor='k',alpha=0.5,marker='d',label='Battlefield1')
plt.scatter(x,z,s=120,facecolor='r',edgecolor='k',alpha=0.3,marker='d',label='Battlefield2')
plt.legend(loc='upper left')
plt.ylim(-0.2,1.2)
plt.show()
```

```python
N = 10
data = np.random.random((10,4))
labels = ['{0}'.format(i) for i in range(N)]
plt.subplots_adjust(bottom = 0.1)
plt.scatter(
    data[:, 0], data[:, 1], marker = 'o', c = data[:, 2], s = data[:, 3]*1500,
    cmap = plt.get_cmap('cool'))

for label, x, y in zip(labels, data[:, 0], data[:, 1]):
    plt.annotate(label, xy = (x, y), xytext = (-10, 10),
        textcoords = 'offset points', ha = 'right', va = 'bottom',
        bbox=dict(boxstyle='round,pad=0.3',fc='red', alpha=0.4),arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'))

plt.show()
```

## Histograms

```python
from numpy.random import normal,poisson,uniform
f, ax = plt.subplots(3)
gaussian_numbers = normal(size=1000)
poisson_numbers = poisson(4,1000)
uniform_numbers = uniform(low=-2, high=2,size=1000)
ax[0].hist(gaussian_numbers,bins=50,facecolor='b',alpha=0.7)
ax[1].hist(poisson_numbers,bins=50,facecolor='g',alpha=0.7)
ax[2].hist(uniform_numbers,bins=50,facecolor='r',alpha=0.7)
ax[0].set_title("Gaussian Histogram")
ax[1].set_title("Poisson Histogram")
ax[2].set_title("Uniform Histogram")
ax[0].set_ylabel("Frequency")
ax[1].set_ylabel("Frequency")
ax[2].set_ylabel("Frequency")
plt.tight_layout()
plt.show()
```

When we increase the value for lambda in the poisson distribution, the histrogram approaches the shape of the gaussion. Give it a try!

Now let us merge the gaussian and uniform distribution since their values lie in the same range.

```python
plt.hist(gaussian_numbers,bins=20,facecolor='b',alpha=0.7,histtype='stepfilled', normed=True,label='Gaussian')
plt.hist(uniform_numbers,bins=20,facecolor='r',alpha=0.5,histtype='stepfilled', normed=True,label='Uniform')
plt.title("Gaussian/Uniform Histogram")
plt.xlabel("Value")
plt.ylabel("Frequency")
plt.legend()
plt.show()
```

## Bar Charts

```python
# Showing the average percentage scores success for the past 5 years at Hudson Park Primary School by each gender
MScores = [72.2,78.9,75.3,80,81.4]
FScores = [77.2,80.9,78.1,84,86.2]
MStd = [3,5,4,3,5]
FStd = [5,4,3,6,4]
ind = np.arange(5)
year = ['2009','2010','2011','2012','2013','2014']
plt.bar(ind,MScores,width=0.3,color='y',yerr=MStd,edgecolor='r',linewidth=2,label='Male')
plt.bar(ind+0.3,FScores,width=0.3,color='c',yerr=FStd,edgecolor='r',linewidth=2,label='Female')
plt.xticks(ind,year,rotation=45)
plt.title("Average passing rate for past 5 years")
plt.ylabel("Percentage Success ($\%$)")
plt.ylim(0,120)
plt.legend()
plt.show()
```

# SciPy Module

SciPy is an extension of the Numpy package. It is collection of mathematical algorithms. Here we will have a look at the complex scipy functions that are not availble in the Numpy package. Similar to numpy, we need to import scipy.

```python
import scipy
```

## Numerical Integration and ODEs

Suppose we want to compute the integral
\begin{equation}
I = \int_0^1 2e^{-2\pi i t}~dx.
\end{equation}
First, we need to import the function quad from a sub-module in scipy.

```python
from scipy.integrate import quad
quad(lambda t:2*np.exp(-2*1j*np.pi*t),0,1)
```

The result is 8.326672684688674e-17 and the second value is the error associated with the computation.

Consider the following ordinary differential equation (ode):
    \begin{equation}
    y = \frac{dy}{dt}.
    \end{equation}
The initial condition is 1 and the points over which the ode needs to be solved is 10.

```python
from scipy.integrate import odeint
odeint(lambda y, t: y, 1, np.linspace(0,1,10))
```

## Interpolation

There are in-built functions in scipy that allow us to perform univariate and multivariate interpolation easily.

```python
from scipy import interpolate
x = np.arange(4, 20,)
y = np.exp(-x/3.0)
f = interpolate.interp1d(x, y) # interpolating a 1D function

xnew = np.linspace(4,19,1000)
plt.plot(x,y,'ro',label='data point')
plt.plot(xnew,f(xnew),'g--',label='interpolated') # f(xnew) gives the interpolated points
plt.legend()
plt.show()

```

Next, we are going to do a univariate spline interpolation. Spline interpolation is a special type of piecewise function which is continuous at the intervals and at the interpolating nodes.

```python
from scipy.interpolate import UnivariateSpline
x = np.linspace(-4, 4, 100)
y = np.sin(x**2) + np.random.randn(100)/10
s = UnivariateSpline(x, y, s=1)
xnew = np.linspace(-4, 4, 1000)
plt.plot(x, y, '.',label='data points')
plt.plot(xnew, s(xnew),'g',label='spline interpolated')
plt.ylim(-1.5,2.0)
plt.legend()
plt.show()
```

# Fast Fourier Transform (FFT)

A fast Fourier transform (FFT) is an algorithm to compute the discrete Fourier transform (DFT) and its inverse. The fft function in numpy computes the 1-dimensional fourier transform of n discrete points.

Let us compute the Fourier inverse of $f(t)=2cos(t)$.
\begin{equation}
f(t) = \int_{-\infty}^{\infty} f(t)e^{-2 \pi i t}
\end{equation}

```python
t = np.arange(100)
sp = np.fft.fft(np.cos(2*t))
freq = np.fft.fftfreq(t.shape[0])
plt.plot(freq, sp.real,label='real') # frequency bins
plt.plot(freq,sp.imag,label='imaginary')
plt.legend()
plt.show()
```

```python
# 2-dimensional FFT
a = np.mgrid[0:4, 0:4][0]
np.fft.fft2(a)
```

# Curve Fitting

Curve fitting is very commom when dealing with astronomical data. The most popular fiiting algorithm is least squares, but we are going to get acquainted to other ways as well.

```python
x = np.arange(10)
y = np.array([0.2,0.3,0.5,0.8,0.95,0.9,0.7,0.5,0.3,0.1])
# constructing polynomial
z = np.polyfit(x,y,3)
p = np.poly1d(z)

#generting new x's and y's
xnew = np.linspace(x[0],x[-1],30)
ynew = p(xnew)

plt.plot(x,y,'go')
plt.plot(xnew,ynew,'r-')
plt.show()
```

```python
# using least square to fit a straight line
x = np.arange(10)
y = np.array([1.2,1.0,0.95,0.9,0.8,0.7,0.5,0.3,0.1,0.09])

A = np.vstack([x, np.ones(len(x))]).T
m, c = np.linalg.lstsq(A, y)[0]
plt.plot(x,y,'bo')
plt.plot(x,m*x+c,'r-')
plt.show()
```

Another way of fitting data is curve_fit found in scipy library.

```python
from scipy.optimize import curve_fit

# Defining the function to be fit to the data
def fitFunc(x,m,c):
    return m*x + c

params, cov = curve_fit(fitFunc, x, y) # returns the fitting paramters

plt.plot(x,y,'go')
plt.plot(x,fitFunc(x,*params),'r-')
plt.show()
```

This cheat sheet walks us through the basics of numpy, scipy and matplotlib libraries. For more details, have a look at
http://docs.scipy.org/doc .

### COMPLETED!

```python

```


# Rasmus
YOUR PROJECT TITLE

> **Note the following:**
> 1. This is *not* meant to be an example of an actual **model analysis project**, just an example of how to structure such a project.
> 1. Remember the general advice on structuring and commenting your code from [lecture 5](https://numeconcopenhagen.netlify.com/lectures/Workflow_and_debugging).
> 1. Remember this [guide](https://www.markdownguide.org/basic-syntax/) on markdown and (a bit of) latex.
> 1. Turn on automatic numbering by clicking on the small icon on top of the table of contents in the left sidebar.
> 1. The `modelproject.py` file includes a function which could be used multiple times in this notebook.

Imports and set magics:

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

# autoreload modules when code is run
%load_ext autoreload
%autoreload 2

# local modules
import modelproject
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

The general Solow model consist of the followning equations:

\begin{align}
(1)&\:\:\:Y_t = K_t^\alpha (A_t L_t)^{1-\alpha}\\
(2)&\:\:\:K_{t+1} = s Y_t +(1+\delta)K_t\\
(3)&\:\:\:L_{t+1} = (1+n)L_t\\
(4)&'\:\:\:A_{t+1} = (1+g)A_t$
\end{align}

$Y_t$, $K_t$, $A_t$, and $L_t$ are the production, capital, technology level, and population in year $t$ respectively. $s$ are the saving rate in physical and human capital respectively. $\alpha$ contribution variables of production and $n$ and $g$ are growth in tecnology and population respectively. Finally we have $\delta$ as a paramater for the depreciation.

Since we have that capital is both accumulating and depreciating, there exist a stable level of capital, known as the steady state. But because the tecknology level and population is constantly growing, the steady state exist in a tecnology adjusted capital per capita. This translate into a steady state in technology adjusted BNP per capita. We define these terms as

\begin{align}
\tilde{y}\equiv \frac{Y_t}{A_t L_t}\\
\tilde{k}\equiv \frac{K_t}{A_t L_t}
\end{align}

By dividing both sides of $Y_t = K_t^\alpha (A_t L_t)^{1-\alpha}$ with $A_t L_t$ we get that $\tilde{y}_t = \tilde{k}^\alpha_t$. If we insert equation (1),(3), and (4) into (2), we get a transistion equation for $\tilde{k}$:

\begin{equation}
    \tilde{k}_{t+1} = \frac{s \tilde{k}_t^\alpha + (1-\delta) \tilde{k}_t}{(1+n)(1+g)}
\end{equation}

I start out by solving the steady states for $\tilde{y}$ and $\tilde{k}$. First symbolically

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

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

$\displaystyle \left(\frac{\delta + g n + g + n}{s}\right)^{\frac{1}{\alpha - 1}}$

```python
ytilde = ktilde**alpha
yss =ytilde.subs(ktilde,kss)
yss
```

$\displaystyle \left(\left(\frac{\delta + g n + g + n}{s}\right)^{\frac{1}{\alpha - 1}}\right)^{\alpha}$

```python

```

```python
ss_ktilde_func = sm.lambdify((s,g,n,delta,alpha),kss)
ss_ytilde_func = sm.lambdify((s,g,n,delta,alpha),yss)
ktilde1_func = sm.lambdify((ktilde,s,g,n,delta,alpha),ktilde1)
```

We could also have solved steady states it numerically using a rootfinder

```python
alpha =0.3
delta =0.05
s = 0.2
n = 0.01
g = 0.02
x0 = 1

def ssk_num_func(s,g,n,delta,alpha):
    def obj(ktilde):
        return ktilde-(s*ktilde**(alpha) + (1-delta)*ktilde)/((1+n)*(1+g))
    result = optimize.root(obj, x0)
    return result.x[0]

def ssy_num_func(s,g,n,delta,alpha):
    return ssk_num_func(s,g,n,delta,alpha)**alpha
print(ssk_num_func(s,g,n,delta,alpha))
print(ss_ktilde_func(s,g,n,delta,alpha))
print(ssy_num_func(s,g,n,delta,alpha))
print(ss_ytilde_func(s,g,n,delta,alpha))

```

    3.689237466703929
    3.689237466703934
    1.4793842241482769
    1.4793842241482773

# transition

```python
ktilde_vec = np.linspace(0,5,100)

trans = ktilde1_func(ktilde_vec,s,g,n,delta,alpha)
plt.plot(ktilde_vec, trans)
plt.plot(ktilde_vec,ktilde_vec)
```

```python

```

```python
ktilde_vec = np.linspace(0,7,10)
trans = ktilde1fun(ktilde_vec,0.2,0.3,0.05,0.01,0.02)
print(trans)
plt.plot(ktilde_vec,trans)
plt.plot(ktilde_vec,ktilde_vec)
plt.axvline(x=ss_ktilde_func(0.2,0.02,0.01,0.05,0.3))
```

```python
def tjeckconc(alpha):
    def plot_ss_func(n,g,delta,s):
        ktilde_vec = np.linspace(0,ss_ktilde_func(s,g,n,delta,alpha),100)
        def depriciation(ktilde,n,g,delta):
            return (n+g+delta+n*g)*ktilde
        def growthk(ktilde,s,alpha):
            return s*ktilde**alpha
        s = 0.2
        depri = depriciation(ktilde_vec,n,g,delta)
        grow = growthk(ktilde_vec,s,alpha)
        plt.plot(ktilde_vec,depri)
        plt.plot(ktilde_vec, grow)
    widgets.interact(plot_ss_func,
    n=widgets.FloatSlider(description="n", min=0.005, max=0.6, step=0.005, value=0.01),
    g=widgets.FloatSlider(description="g", min=0.005, max=0.6, step=0.005, value=0.02),
    delta=widgets.FloatSlider(description="$\delta$", min=0.01, max=0.6, step=0.01, value=0.05),
    s=widgets.FloatSlider(description="$s$", min=0.05, max=0.6, step=0.01, value=0.2),
    );

tjeckconc(alpha)
```

    interactive(children=(FloatSlider(value=0.01, description='n', max=0.6, min=0.005, step=0.005), FloatSlider(va…

```python
def plot_ss(g,n,delta,alpha):
    def plot_ss_func(s,new_s):
        max_x = max(ss_ktilde_func(s,g,n,delta,alpha),ss_ktilde_func(new_s,g,n,delta,alpha))
        ktilde_vec = np.linspace(0,max_x*1.2,100)
        def depriciation(ktilde,n,g,delta):
            return (n+g+delta+n*g)*ktilde
        def growthk(ktilde,s,alpha):
            return s*ktilde**alpha
        depri = depriciation(ktilde_vec,n,g,delta)
        grow = growthk(ktilde_vec,s,alpha)
        plt.plot(ktilde_vec,depri)
        plt.plot(ktilde_vec, grow)
        plt.axvline(x=ss_ktilde_func(s,g,n,delta,alpha), linestyle='--', color = 'grey')

        grow = growthk(ktilde_vec,new_s,alpha)
        plt.plot(ktilde_vec, grow)
        plt.axvline(x=ss_ktilde_func(new_s,g,n,delta,alpha), linestyle='--', color = 'black')

    widgets.interact(plot_ss_func,
    s=widgets.FloatSlider(description="s", min=0.05, max=0.6, step=0.02, value=0.15),
    new_s=widgets.FloatSlider(description="New s", min=0.05, max=0.6, step=0.02, value=0.25),
    );

plot_ss(0.02,n,delta,alpha)
```

    interactive(children=(FloatSlider(value=0.15, description='s', max=0.6, min=0.05, step=0.02), FloatSlider(valu…

```python

```

```python

```

# Increase in saving rate

As $\tilde{y}^*$ is constant, we have that $y^*$ is growing at at contanst rate along with $A_t$, $g$, $\tilde{y}^* = A_t y^* = A_0(1+g)^t y^*$ insteady state. If some of the parameter chance, the growth in $y$ will change until the economy again has found its steady state. If

```python

```

```python

```

```python
#initial parameters
alpha =0.3
delta =0.05
s = 0.2
n = 0.01
g = 0.02
A0 = 10
def plotconvergens(new_s,new_s_period=5,T=50):

    y_vec = []
    ktilde0 = ss_ktilde_func(s,g,n,delta,alpha)
    y0 = ss_ytilde_func(s,g,n,delta,alpha)*A0

    for i in range(0,new_s_period):
        y_vec.append(y0*(1+g)**i)

    ktilde = ktilde0
    for i in range(new_s_period,T):
        ktilde = ktilde1_func(ktilde,new_s,g,n,delta,alpha)
        y = ktilde**alpha*A0*(1+g)**i
        y_vec.append(y)

    ln_y = np.log(y_vec)
    g_y_vec = np.diff(ln_y)

    fig = plt.figure(dpi = 130)
    ax = fig.add_subplot(2,1,1)
    ax.plot(ln_y)

    ax = fig.add_subplot(2,1,2)
    plt.plot(g_y_vec)
    plt.show()
```

```python
widgets.interact(plotconvergens,
    #alphas=widgets.fixed(new_s_period=5,T=100),
    new_s=widgets.FloatSlider(description="New s", min=0.01, max=0.99, step=0.05, value=0.25),
    new_s_period=widgets.fixed(5),
    T=widgets.fixed(50)
);
```

    interactive(children=(FloatSlider(value=0.25, description='New s', max=0.99, min=0.01, step=0.05), Output()), …

# Model description

The Solow model with human capital consist of the followning equations:

\begin{align}
Y_t &= K_t^\alpha  H_t^\varphi (A_t L_t)^{1-\alpha-\varphi}\\
K_{t+1} &= s_K Y_t +(1+\delta)K_t\\
H_{t+1} &= s_H Y_t + (1+\delta)H_t\\
L_{t+1} &= (1+n)L_t\\
A_{t+1} &= (1+g)A_t
\end{align}

$Y_t$, $K_t$, $H_t$, $A_t$, and $L_t$ are the production, physical capital, human capital, technology level, and population in year $t$ respectively. $s_K$ and $s_H$ are the saving rate in physical and human capital respectively. $\alpha$ and $\varphi$ are contribution variables of production and $n$ and $g$ are growth in tecnology and population respectively. Finally we have $\delta$ as a paramater for the depreciation in both physical and human capital.

$\tilde{y}\equiv \frac{Y_t}{A_t L_t}$              $\tilde{k}\equiv \frac{K_t}{A_t L_t}$              $\tilde{h}\equiv \frac{H_t}{A_t L_t}$

## Analytical solution

If your model allows for an analytical solution, you should provide here.

You may use Sympy for this. Then you can characterize the solution as a function of a parameter of the model.

To characterize the solution, first derive a steady state equation as a function of a parameter using Sympy.solve and then turn it into a python function by Sympy.lambdify. See the lecture notes for details.

```python
# Defining symbols for sympy

# period t
K = sm.symbols("K_t")
H = sm.symbols("H_t")
A = sm.symbols("A_t")
L = sm.symbols("L_t")

phi = sm.symbols("varphi")
sK = sm.symbols("s_K")
sH = sm.symbols("s_H")
alpha = sm.symbols('alpha')
delta = sm.symbols('delta')
varphi = sm.symbols('varphi')
g = sm.symbols('g')
n = sm.symbols('n')

kstar = sm.symbols("ktilde^*")
hstar = sm.symbols("htilde^*")
ystar = sm.symbols("ytilde^*")

ktilde = sm.symbols("ktilde_t")
htilde = sm.symbols("htilde_t")
ytilde = sm.symbols("ytilde_t")
```

```python
Y = K**alpha*H**phi*(A*L)**(1-alpha-phi)

# Physical capital
Kt1 = sK*Y +(1-delta)*K

# Human capital
Ht1 = sH*Y +(1-delta)*H

# Technology level and labor force
At1 = (1+g)*A
Lt1 = (1+n)*L
Y
```

$\displaystyle H_{t}^{\varphi} K_{t}^{\alpha} \left(A_{t} L_{t}\right)^{- \alpha - \varphi + 1}$

transition equations

```python

ktilde1 = ( sK*ktilde**alpha*htilde**varphi + (1-delta)*ktilde) /((1+n)*(1+g))
htilde1 = ( sH*ktilde**alpha*htilde**varphi + (1-delta)*htilde) /((1+n)*(1+g))
```

```python

```

```python
redundant = ((g+1)*(n+1))
ksolow = sm.simplify(ktilde1-ktilde)
ksolowss = ksolow*redundant
ssk = sm.simplify(sm.Eq(ksolowss,0))
ssk
```

$\displaystyle - \tilde{h}_{t}^{\varphi} \tilde{k}_{t}^{\alpha} s_{K} + \tilde{k}_{t} \left(\delta - 1\right) + \tilde{k}_{t} \left(g + 1\right) \left(n + 1\right) = 0$

```python
hsolow = sm.simplify(htilde1-htilde)
hsolowss = hsolow*redundant
ssh = sm.Eq(hsolowss,0)
ssh
```

$\displaystyle - \tilde{h}_{t} \left(\delta - 1\right) - \tilde{h}_{t} \left(g + 1\right) \left(n + 1\right) + \tilde{h}_{t}^{\varphi} \tilde{k}_{t}^{\alpha} s_{H} = 0$

```python

# Finding steady state for physical capital
ktildesub = sm.solve(ssh, ktilde)[0]
ktildesub
#kSS = sm.Eq(kstar, sm.solve(ssk, ktilde)[0])
#kSS
```

$\displaystyle \left(\frac{\tilde{h}_{t}^{1 - \varphi} \left(\delta + g n + g + n\right)}{s_{H}}\right)^{\frac{1}{\alpha}}$

```python
hss1 = ssh.subs(ktilde, ktildesub)
sm.simplify(hss1)
n
```

$\displaystyle \tilde{h}_{t} \left(\delta - 1\right) + \tilde{h}_{t} \left(g + 1\right) \left(n + 1\right) - \tilde{h}_{t}^{\varphi} s_{H} \left(\left(\frac{\tilde{h}_{t}^{1 - \varphi} \left(\delta + g n + g + n\right)}{s_{H}}\right)^{\frac{1}{\alpha}}\right)^{\alpha} = 0$

```python
sej = sm.solve(hss1)
sej
scipy.optimize.root
```

## Numerical solution

You can always solve a model numerically.

Define first the set of parameters you need.

Then choose one of the optimization algorithms that we have gone through in the lectures based on what you think is most fitting for your model.

Are there any problems with convergence? Does the model converge for all starting values? Make a lot of testing to figure these things out.

```python
alpha =0.3
varphi = 0.3
delta =0.05
sH = 0.2
sK = 0.3
n = 0.01
g = 0.02

htilde = 13.3

def obj(ktilde, alpha, sK, sH, varphi, delta, n, g):
    return ktilde-(sK*ktilde**alpha*htilde**varphi + (1-delta)*ktilde) /((1+n)*(1+g))

```

# Further analysis

```python

x_vec = np.linspace(0.0000000001,20,100)
obj(x_vec, alpha, sK, sH, varphi, delta, n, g)
res = optimize.root(obj,20,args=(alpha, sK, sH, varphi, delta, n, g),method='hybr')
res.x
```

    array([19.95934363])

```python

```

Make detailed vizualizations of how your model changes with parameter values.

Try to make an extension of the model.

# Conclusion

Add concise conclusion.

```python
# The complete model

def funYt(Kt,Ht,At,Lt, alpha, phi):
    Yt = Kt**alpha * Ht**phi * (At*Lt)**(1-alpha-phi)
    return Yt

def funKt1(sK, Yt, delta, Kt):
    Kt1 = sK*Yt - delta*Kt + Kt
    return Kt1

def funHt1(sH, Yt, delta, Ht):
    Ht1 = sH*Yt - delta*Ht + Ht
    return Ht1

def funLt1(Lt,n):
    Lt1 = (1+n)*Lt
    return Lt1

def funAt1(At, g):
    At1 = (1+g)*At
    return At1
```

```python
def funkttilde(Kt,At,Lt):
    kttilde = Kt/(At*Lt)
    return kttilde

def funhttilde(Ht,At,Lt):
    httilde = Ht/(At*Lt)
    return httilde

def funyttilde(kttilde, httilde, alpha, phi):
    yttilde = kttilde**alpha * httilde**phi
    return yttilde

def funkttilde1(yttilde, sK, delta, ktilde, n, g):
    kttilde1 = (sK*yttilde + (1-delta)*kttilde) / ((1+n)+(1+g))
    return kttilde1

def funhttilde1(yttilde, sH, delta, htilde, n, g):
    httilde1 = (sH*yttilde + (1-delta)*httilde) / ((1+n)+(1+g))
    return httilde1

def funobj_sskttilde(kttilde, kttilde1):
    res = kttilde-kttilde1
    return res
```

```python
Yt = 10
Kt = 10
Ht = 20
At = 8
Lt = 100
alpha =0.3
phi = 0.3
delta =0.05
sH = 0.2
sK = 0.3
n = 0.01
g = 0.02
```

```python
httilde = funkttilde(Kt,At,Lt)
kttilde = funhttilde(Ht,At,Lt)
yttilde = funyttilde(kttilde, httilde, alpha, phi)
```

```python
kttilde1 = funkttilde1(yttilde, sK, delta, kttilde, n, g)
funobj_sskttilde(kttilde, kttilde1)
```

    0.013744841152236894

```python
def deltak(sK, alpha, phi, n, g, delta, httilde):
    return ( (n+g+delta+n*g)/(sK*httilde**phi) )**(1/(alpha-1))

def deltah(sH, alpha, phi, n, g, delta, kttilde):
    return ( (n+g+delta+n*g)/(sH*kttilde**alpha) )**(1/(phi-1))
```

```python
httilde = np.linspace(0.01,30,1000)
kttilde = np.linspace(0.01,30,1000)
```

```python
h_vec = deltak(sK, alpha, phi, n, g, delta, httilde)
k_vec = deltah(sH, alpha, phi, n, g, delta, kttilde)
plt.plot(httilde, h_vec)
plt.plot(k_vec,httilde)
```

```python
kstar = ( (sK**(1-phi)*sH**(phi))/(n+g+delta+n*g))**(1/(1-alpha-phi))
hstar = ( (sK**(alpha)*sH**(1-alpha))/(n+g+delta+n*g))**(1/(1-alpha-phi))
print(hstar, kstar)
```

    13.310902819474649 19.966354229211966

Microtest

```python
from types import SimpleNamespace
par = SimpleNamespace()
par.alpha = 0.5
par.w = 10
par.L = 10
par.p1 = 5
par.beta = 0.8

def util(x1):
    x2 = budget(par)-par.p1*x1
    return -x1**par.alpha*x2**(1-par.alpha)

def budget(par):
    return par.w*par.L

def budgetcon(x1):
    x2 = budget(par)-par.p1*x1
    return budget(par)-par.p1*x1+-x2

```

```python
def demand(par):
    result = optimize.minimize(util,6,
                               method='Nelder-Mead',
                                )
    return result.x
demand(par)
```

    array([9.99997559])

```python
x1_vec = np.linspace(0,budget(par)/par.p1,1000)
plt.plot(x1_vec,-util(x1_vec))
```

```python
x2 = budget(par)-par.p1*x1
print(budget(par))
print(budgetcon(x1))
print(-util(x1))
```

    100
    0
    19.364916731037088

```python
def production(L):
    return par.L**par.beta*20

def profit(par):
    Pi = par.p1*demand(par)-par.L*par.w
    return Pi
```

```python
print(production(L))
profit(par)
```

    126.19146889603867

    array([-50.00012207])

```python

```


# Function

## Define function $$f(x) = x^2$$

```python
# Define function f(x) = x^2
def f(x):
    return x**2
```

```python
print(f(2))
```

    4

## Define function $$g(x) = x^3$$

```python
# Define function g(x) = x^3
def g(x, y):
    return 2*x + y
```

```python
print(g(1, 2))
```

    4

```python
# Load sympy module
from sympy import *
```

```python
# Define variables
x, y, z = symbols('x y z')
```

```python
# Substitute x = y and y = x
substitution = x**2 + 2*y + z
replacement = [(x**2, y**2), (2*y, 2*x)]
print(substitution.subs(replacement))
```

    2*x + y**2 + z

```python
# Subtitute x, y, and z with their values
number_subs = [(x**2, 2), (2*y, 2), (z, 2)]
print(substitution.subs(number_subs))
```

    6


#  Optimal Decisions for Discrete Stochastic Systems

We are ready to tackle our third example from this chapter's introduction:

```{panels}
Example: Binary Communication System
^^^
In a binary communication system, 0s and 1s are transmitted over a noisy channel.

If we know the probability that a given bit is a 0 and we know the probabilities associated with the noisy channel, what is the optimal decision given an observation at the output of the channel?
```

Let's start by considering one particular instantiation of this problem:

## Binary Communication System

Suppose we are given a system with that has binary inputs and ternary outputs. Let's use $A_0$ and $A_1$ to denote the input events and $B_0$, $B_1$, and $B_2$ to denote the output events. The channel is completely specified by giving the likelihoods, $P(B_j|A_i)$ for all $i \in \{0,1\}$ and $j \in \{0,1,2\}.

Let $p_{ij} = P(B_j|A_i)$. Note the order of the $i$ and $j$.  This is the probability of transitioning from input $i$ to output $j$, and these are also called *channel transition probabilities*. It is helpful to visualize the channel transition probabilities/likelihoods on a diagram, where we label the arrow connecting input event $A_i$ with output event $B_j$ by $p_{ij}$. An  example of such a diagram is shown in {numref}`example2to3`. For example, from this diagram, we can read that $p_{00}=5/8$, $p_{01}=1/4$, and $p_{12}=3/4$.

:::{figure-md} example2to3

Example channel transition diagram for channel with two inputs and three outputs.
:::

It is convenient to collect the channel transition probabilities into an array, such that the $(i,j)$th entry in the array is $p_{ij}$. Let's import numpy and create an array with these channel transition probabilities:

```python
import numpy as np

P=np.array([
    [5/8, 1/4, 1/8],
    [1/8, 1/8, 3/4]
])

print(P)
```

    [[0.625 0.25  0.125]
     [0.125 0.125 0.75 ]]

Note that the probabilities with the same conditioning should add to 1. In other words,

$$
\sum_{j \in \{0,1,2\} }  P(B_j|A_i) = 1, ~~~ i=0,1.
$$
In the diagram, this corresponds to the transition probabilities that emerge from the same input. In the matrix $\mathbf{P},$ this corresponds to all of the entries in a row. We can tell numpy to sum the array across the column by using the `np.sum` function and passing the `axis=1` argument to tell it to sum across the second axis (the columns):

```python
np.sum(P, axis=1)
```

    array([1., 1.])

Note that the probabilities for a particular $B_j$ (i.e., merging into a particular output) do not necessarily sum to 1.

$$
\sum_{i \in \{0,1\} }  P(B_j|A_i) = ?, ~~~ j=0,1,2.
$$

This corresponds to the column sums of the matrix $\mathbf{P}$, and we can get these column sums by using the `np.sum` function and passing the `axis=0` argument to tell numpy to sum across axis 0 (the rows):

```python
np.sum(P, axis=0)
```

    array([0.75 , 0.375, 0.875])

## Decision Problem and Decision Rules

The *decision problem* for the binary communication systems is concisely described as follows: given the observed output, determine which input was sent.  A *decision rule* tells how to choose an input given an observed output. In general, a decision rule may result in a randomized choice for the input,  but in this class, we will only consider deterministic decision rules.

We can turn this into an *optimal decision problem* if we specify criteria to be optimized. Here are two common criteria:
1. Maximize the likelihood of the input
2. Choose the input that minimizes the probability of error

The optimum decision under criterion 1 is called the *maximum likelihood (ML)* decision. In this problem, the likelihoods are of the form $P(B_j|A_i)$. Let $\hat{A}_i$ denote the event that the receiver decides that input was $i$. Then the ML decision rule given $B_j$ is observed is

$$
\hat{A}_i, \mbox{ where } i = \arg \max_{i \in \{0,1\}} P(B_j|A_i).
$$

The values of $P(B_j|A_i)$ are given in {numref}`example2to3`.  For each output, the ML rule selects the input that has the largest likelihood, which corresponds to the arrow with the largest probability merging into that output in {numref}`example2to3`. Similarly, the ML decision corresponds to the row number with the largest probability for each column of the transition probability matrix, $\mathbf{P}$. To get the index of the largest value in each column, we can use the `np.argmax` function and pass the `axis=0` keyword parameter to tell NumPy to maximize over the rows. (Note that `np.max` would return the maximum value, whereas `np.argmax` returns the index of the maximum value.)

Thus, the ML decisions are as follows:

```python
np.argmax(P, axis=0)
```

    array([0, 0, 1])

Given $B_0$ is received decide $A_0$

Given $B_1$ is received, decide $A_0$

Given $B_2$ is received, decide $A_1$

Unfortunately, the ML solution does not necessarily minimize the probability of error. For instance, suppose that we know that 0 is sent with probability 1; i.e., $P(A_0)=1$. Then the ML rule will make an error whenever $B_2$ is received. Let's use total probability to calculate the probability of each $B_j$:

$$
P(B_j) = \sum_{i \in \{0,1\}} P(B_j|A_i) P(A_i)
$$

We start by setting up a NumPy vector to hold the *a priori* probabilities:

$$
\left[ P(A_0), P(A_1) \right].
$$

Then we use a nested for loop to calculate $P(B_j)$ for each $j$ and within the loop for each $j$ we use a for loop to  carry out the sum for each $i$:

```python

aprioris = np.array([1,0])
for j in range(3):
    pBj=0
    for i in range(2):
        pBj+=P[i,j]*aprioris[i]

    print(f'P(B{j}) = {pBj}')
```

    P(B0) = 0.625
    P(B1) = 0.25
    P(B2) = 0.125

Let $E$ be the event that an error occurs (i.e., the decision differs from the transmitted symbol). Then for this simple example, $P(E) =  P(B_2) = 0.125$. We know that it is suboptimal, because we could just use the decision rule "Always decide $A_0$" and get error probability 0.

We can guess that there must be some value $q_0$ such that:
* if $P(A_0)<q_0$, the ML rule performs better, and
* if $P(A_0)>q_0$, always deciding $A_0$ performs better.

Let's build a simulation to test this. First, we will see how to efficiently generate the events $A_0$ and $A_1$ given any probabilities $P(A_0)$ and $P(A_1)$ such that $P(A_0)+P(A_1) = 1$. We will again use NumPy's `npr.choice()` function, but we will now pass it the probability information as a keyword parameter. Take a look at the help for `npr.choice()`:

```python
import numpy as np
import numpy.random as npr
```

```python
?npr.choice
```

    Docstring:
    choice(a, size=None, replace=True, p=None)

    Generates a random sample from a given 1-D array

    .. versionadded:: 1.7.0

    .. note::
        New code should use the ``choice`` method of a ``default_rng()``
        instance instead; please see the :ref:`random-quick-start`.

    Parameters
    ----------
    a : 1-D array-like or int
        If an ndarray, a random sample is generated from its elements.
        If an int, the random sample is generated as if it were ``np.arange(a)``
    size : int or tuple of ints, optional
        Output shape.  If the given shape is, e.g., ``(m, n, k)``, then
        ``m * n * k`` samples are drawn.  Default is None, in which case a
        single value is returned.
    replace : boolean, optional
        Whether the sample is with or without replacement. Default is True,
        meaning that a value of ``a`` can be selected multiple times.
    p : 1-D array-like, optional
        The probabilities associated with each entry in a.
        If not given, the sample assumes a uniform distribution over all
        entries in ``a``.

    Returns
    -------
    samples : single item or ndarray
        The generated random samples

    Raises
    ------
    ValueError
        If a is an int and less than zero, if a or p are not 1-dimensional,
        if a is an array-like of size 0, if p is not a vector of
        probabilities, if a and p have different lengths, or if
        replace=False and the sample size is greater than the population
        size

    See Also
    --------
    randint, shuffle, permutation
    Generator.choice: which should be used in new code

    Notes
    -----
    Setting user-specified probabilities through ``p`` uses a more general but less
    efficient sampler than the default. The general sampler produces a different sample
    than the optimized sampler even if each element of ``p`` is 1 / len(a).

    Sampling random rows from a 2-D array is not possible with this function,
    but is possible with `Generator.choice` through its ``axis`` keyword.

    Examples
    --------
    Generate a uniform random sample from np.arange(5) of size 3:

    >>> np.random.choice(5, 3)
    array([0, 3, 4]) # random
    >>> #This is equivalent to np.random.randint(0,5,3)

    Generate a non-uniform random sample from np.arange(5) of size 3:

    >>> np.random.choice(5, 3, p=[0.1, 0, 0.3, 0.6, 0])
    array([3, 3, 0]) # random

    Generate a uniform random sample from np.arange(5) of size 3 without
    replacement:

    >>> np.random.choice(5, 3, replace=False)
    array([3,1,0]) # random
    >>> #This is equivalent to np.random.permutation(np.arange(5))[:3]

    Generate a non-uniform random sample from np.arange(5) of size
    3 without replacement:

    >>> np.random.choice(5, 3, replace=False, p=[0.1, 0, 0.3, 0.6, 0])
    array([2, 3, 0]) # random

    Any of the above can be repeated with an arbitrary array-like
    instead of just integers. For instance:

    >>> aa_milne_arr = ['pooh', 'rabbit', 'piglet', 'Christopher']
    >>> np.random.choice(aa_milne_arr, 5, p=[0.5, 0.1, 0.1, 0.3])
    array(['pooh', 'pooh', 'pooh', 'Christopher', 'piglet'], # random
          dtype='<U11')
    Type:      builtin_function_or_method

Thus, if we want to output a 0 with probability $P(A_0)=0.75$ and a 1 with probability $P(A_1)=0.25$, we can simulate 1000 such events as follows:

```python
sim_values = npr.choice([0,1], 1000, p = [0.75,0.25])
```

We can check that we are achieving the desired probabilities by comparing the relative frequencies to the probabilities we passed as arguments:

```python
np.sum(sim_values==0)/1000
```

    0.759

```python
np.sum(sim_values==1)/1000
```

    0.241

The relative frequencies of 0.763, 0.237 are close to the probabilities 0.75, 0.25. Some variation is expected, since we only simulated 1000 events.

Now we are ready to build a function to carry out the simulation. I am going to introduced one new Python concept here: we can pass a function as an argument to another function. This will allow us to create one simulation and test it for multiple different decision rules.  Let's name the parameter for our decision_rule. Then the following code will simulate the error probability when passed a function for the decision rule that returns either 0 or 1 (corresponding to $A_0$ or $A_1$) when passed an integer 0, 1, or 2, representing one of the outputs ($B_0, B_1, B_2$):

```python
def sim2to3 (decision_rule, P, PA0, num_sims = 100_000, verbose = False):
    # Create all the input events at the same time:
    inputs = npr.choice([0, 1], num_sims, p = [PA0, 1-PA0])

    # Create an array to determine the channel outputs
    outputs = np.zeros(num_sims)

    # Create an array to store the decisions
    decisions = np.zeros(num_sims)

    # There are more efficient ways of doing this using NumPy, but
    # individually determining each output for each input should make
    # this easier to understand for most learners
    for sim in range(num_sims):
        input_bit = inputs[sim]
        # Choose observation according to transition probabilities for given input bit:
        observation = npr.choice([0, 1, 2], p = P[input_bit])

        # Now pass this observation to the decision rule function:
        decisions[sim] = decision_rule(observation, P, PA0)

    # Finally, calculate the error probability. An error occurs
    # whenever the decision is not equal to the true input
    errors = np.sum(inputs != decisions)

    error_prob = errors/num_sims
    if verbose:
        print( f'The error probability is approximately {error_prob:.2f}')
    return error_prob

```

Now let's create and test our decision rule functions. The easiest one is to always decide 0:

```python
def always_decide0 (observation, P, PA0):
    return 0
```

This should decision rule should result in zero errors when $P(A_0)=1$:

```python
sim2to3(always_decide0, P, 1, verbose=True)
```

    The error probability is approximately 0.00

    0.0

The error probability should increase as $P(A_0)$ decreases:

```python
sim2to3(always_decide0, P, 0.8, verbose=True)
```

    The error probability is approximately 0.20

    0.19986

Now let's implement out ML decision rule as a function:

```python
def ML (observation, P, PA0):
    return np.argmax(P[:,observation]) #Here I selected the  column and then did argmax, but the other way also works
```

Let's try this for a few values of $P(A_0)$:

```python
sim2to3(ML, P, 1, verbose = True)
```

    The error probability is approximately 0.13

    0.12527

```python
sim2to3(ML, P, 0.8, verbose = True)
```

    The error probability is approximately 0.15

    0.1513

The ML rule does worse than the "always decide $A_0$" rule for $P(A_0)=1$, as expected. The ML rule performs better when $P(A_0)=0.8$.

Let's create one more decision rule function: always decide $A_1$:

```python
def always_decide1 (observation, P, PA0):
    return 1
```

Let's generate a plot comparing the error probabilities of these decision rules as a function of $P(A_0)$:

```python
from tqdm import tqdm
import matplotlib.pyplot as plt

input_probs = np.linspace(0,1,21)

pe_always0 = []
pe_always1 = []
pe_ML = []

for PA0 in tqdm(input_probs):
    pe_always0 += [sim2to3(always_decide0, P, PA0)]
    pe_always1 += [sim2to3(always_decide1, P, PA0)]
    pe_ML += [sim2to3(ML, P, PA0)]

plt.plot(input_probs, pe_always0, label='Always decide 0')
plt.plot(input_probs, pe_always1, label='Always decide 1')
plt.plot(input_probs, pe_ML, label='ML')
plt.xlabel(f'$P(A_0)$')
plt.ylabel('Error probability')
plt.legend();

```

From the figure, we can see that each of these three different decision rules minimize the probability of error over some specific range of $P(A_0)$. For low $P(A_0)$, it is best to always decide $A_1$. For medium $P(A_0)$ (in the range $0.2 < P(A_0) < 0.85$), it appears that it is best to apply the ML rule, and for larger $P(A_0)$, it is best to always decide $A_0$. And other decision rules are possible. We need a principled way to determine the optimum decision rule to minimize the error probability.

## Optimal Decisions to Minimize the Probability of Error: The MAP Rule

As before, let $\hat{A}_i$ denote the event that the decision is $i$. Then an error occurs if the decision is different than the actual input event. Let $E$ denote the error event, so

$$
E = \left(\hat{A}_0 \cap A_1\right) \cup \left(\hat{A}_1 \cap A_0\right).
$$

Note that $\left(\hat{A}_0 \cap A_1\right)$ and $\left(\hat{A}_1 \cap A_0\right)$ are mutually exclusive (since $A_0$ and $A_1$ are complementary events$. Thus

$$
P(E) = P\left(\hat{A}_0 \cap A_1\right) + P \left(\hat{A}_1 \cap A_0\right).
$$

We can minimize the error probability if we minimize the error probability given the observation, $B_j$:

$$
P\left(E|B_j\right) = P\left(\hat{A}_0 \cap A_1 \left \vert B_j \right. \right) + P \left(\hat{A}_1 \cap A_0 \left \vert B_j \right. \right).
$$

In general, we can make a decision rule that is probabilistic, meaning that given $B_j$, we choose $\hat{A}_0$ with
some probability and $\hat{A}_1$ with some probability. However, that turns out to not be necessary: the optimum decision rule is deterministic. Thus, only one of the two terms in the summation will be retained; i.e., given $B_j$ only one of $\hat{A}_0$ and $\hat{A_1}$ will be occur. If given $B_j$, we always decide the most likely input was 0, then $\hat{A}_0$ occurs, and $P(E) = P\left(A_1 \left \vert B_j \right. \right)$; conversely, if given $B_j$, we always decide the most likely input was 1, then $\hat{A}_1$ occurs, and $P(E)=P \left( A_0 \left \vert B_j \right. \right).$

Since, we wish to minimize the $P(E)$, the decision rule given $B_j$ is received should be:
* $\hat{A}_0$ if $P\left(A_1 \left \vert B_j \right. \right) < P \left( A_0 \left \vert B_j \right. \right)$, and
* $\hat{A}_1$ if $P\left(A_1 \left \vert B_j \right. \right) > P \left( A_0 \left \vert B_j \right. \right)$, and
* either $\hat{A}_0$ or $\hat{A}_1$ if $P\left(A_1 \left \vert B_j \right. \right) = P \left( A_0 \left \vert B_j \right. \right)$.

```{note}

Although we didn't prove that the optimal decision rule is deterministic, it is not hard to see that any probabilistic decision rule would have an error probability that is  a linear combination of the probabilities $P\left(A_1 \left \vert B_j \right. \right)$ and  $P \left( A_0 \left \vert B_j \right. \right)$. The minimum value of a line on a closed interval is at one of the end points (i.e., one of the decisions has probability 1 and the other has probability 0), so the decision rule that minimizes the error probability is deterministic.
```

The decision rule that minimizes the error probability can be summarized as "choose the input that maximizes the *a posteriori* probability given the observation $B_j$. Mathematically, the minimum error probability rule is

$$
\hat{A}_i \mbox{where} j = \arg \max_{i \in \{0,1\} } P \left( A_i \left \vert B_j \right. \right)
$$

Since we are choosing the input that maximizes the *a posteriori* probability (APP), we call this a *maximum a posteriori (MAP)* decision rule. We also use the following notation when the decision rule is between two possible inputs:

$$
P\left(A_0 \left \vert B_j \right. \right)
\underset{1}{\overset{0}{\gtrless}}
 P \left( A_1 \left \vert B_j \right. \right).
 $$ (binary-map)

As in {doc}`bayes-rule`, the APPs are not given in the problem formulation, but we can find the APPs from the likelihoods and the *a priori* probabilities:

$$
P(A_i|B_j) = \frac{P(B_j|A_i)P(A_i)}{\sum_i P(B_j|A_i)P(A_i)}.
$$

If only the MAP decision is needed, then {eq}`binary-map` can be simplified:

\begin{align}
P\left(A_0 \left \vert B_j \right. \right)  & \underset{1}{\overset{0}{\gtrless}}  P \left( A_1 \left \vert B_j \right. \right)\\
\frac{P(B_j|A_0)P(A_0)}{\sum_i P(B_j|A_i)P(A_i)} & \underset{1}{\overset{0}{\gtrless}}
\frac{P(B_j|A_1)P(A_1)}{\sum_i P(B_j|A_i)P(A_i)} \\
{P(B_j|A_0)P(A_0)} & \underset{1}{\overset{0}{\gtrless}}
{P(B_j|A_1)P(A_1)}.
\end{align}

The APPs can be computed by implementing Bayes' rule in Python as follows for $P(A_0) = 1/5$ and $P(A_1)=4/5$:

```python

aprioris = np.array([1/5,4/5])
for j in range(3):
    pBj=0
    for i in range(2):
        pBj+=P[i,j]*aprioris[i]
    print(f'P(B{j}) = {pBj}')
    for i in range(2):
        print(f'P(A{i}|B{j}) = {P[i,j]*aprioris[i]/pBj}')
    print()

```

    P(B0) = 0.225
    P(A0|B0) = 0.5555555555555556
    P(A1|B0) = 0.4444444444444445

    P(B1) = 0.15000000000000002
    P(A0|B1) = 0.3333333333333333
    P(A1|B1) = 0.6666666666666666

    P(B2) = 0.6250000000000001
    P(A0|B2) = 0.039999999999999994
    P(A1|B2) = 0.96

The MAP rule for these *a priori* probabilities is **not any of the three rules previously introduced**! Let's create a MAP decision rule function:

```python
def MAP(observation, P, PA0):
    # Take the $j$th colum and multiply it elementwise by the a priori probability vector
    scaled_apps = P[:,observation] * np.array([PA0, 1-PA0])
    return np.argmax(scaled_apps)
```

Let's now run a simulation comparing the performance of all of these decision rules:

```python
input_probs = np.linspace(0,1,21)

pe_always0 = []
pe_always1 = []
pe_ML = []
pe_MAP = []

for PA0 in tqdm(input_probs):
    pe_always0 += [sim2to3(always_decide0, P, PA0)]
    pe_always1 += [sim2to3(always_decide1, P, PA0)]
    pe_ML += [sim2to3(ML, P, PA0)]
    pe_MAP += [sim2to3(MAP, P, PA0)]

plt.plot(input_probs, pe_always0, label='Always decide 0')
plt.plot(input_probs, pe_always1, label='Always decide 1')
plt.plot(input_probs, pe_ML, label='ML')
plt.plot(input_probs, pe_MAP, label='MAP')
plt.xlabel(f'$P(A_0)$')
plt.ylabel('Error probability')
plt.legend();

```

As seen in the figure, the MAP rule achieves the lowest error probability for all values of $P(A_0)$. However, this does require the receiver to know the *a priori* values of the inputs.  If the *a priori* probabilities are note known, then the ML decision rule is usually used.

## Terminology Review

```python
from jupytercards import display_flashcards

#display_flashcards('flashcards/'+'optimal-decisions.json')

github='https://raw.githubusercontent.com/jmshea/Foundations-of-Data-Science-with-Python/main/'
github+='07-bayesian-methods/flashcards/'
display_flashcards(github+'optimal-decisions.json')

```

<style>
:root{
--asparagus: #87a878ff;
--terra-cotta: #e26d5aff;
--cyan-process: #5bc0ebff;
--dark-blue-gray: #666a86ff;
--snow: #fffafbff;
--rich-black-fogra-39: #090c08ff;
}

/* entire container, keeps perspective */
.flip-container {
	perspective: 1000px;
  touch-action: pinch-zoom pan-y;
}

.flip-container.flip .flipper {
	transform: rotateY(180deg)  translateX(-10%);
}

.flip-container.slide .flipper.frontcard {
    filter: brightness(90%);
    position:absolute;
    z-index: -20;
}

.flip-container.prepare .flipper.backcard {
    opacity:1;
    transform:  translateX(20px);
    transition: display 0s;
}

.flip-container.slide .flipper.backcard {
    filter: brightness(115%);
    height: 360px;
    opacity:1;
	  transform:  rotateY(3deg)  translateX(700px) translateZ(20px) scale(1, 1.05) rotate(3deg);
    z-index: 20;
}

.flip-container.slideback .flipper.backcard {
    filter: brightness(100%);
    opacity: 1;
	  transform: translateX(0px) translateZ(20px) rotateY(0deg);
    z-index: 10;
}

.flip-container.slideback .flipper.frontcard {
    filter: brightness(90%);
    transform: translateX(0px) translateZ(-20px);
    z-index: -10;
}

.flip-container, .front, .back {
	  height: 300px;
	  width: 640px;
}

.flipper.frontcard {
    position:absolute;
	  transform: translateX(0px) rotateY(0deg) translateZ(20px);
    z-index: 10;
}

.flipper.backcard {
    filter: brightness(90%);
    opacity:0;
    position:absolute;
    transform: translateX(0px) translateZ(-20px);
    z-index: -10;
}

/* flip speed goes here */
.flipper {
    cursor:pointer;
	  height: 200px;
    margin-left:20px;
	  position: absolute;
    top: 0;
	  transform-style: preserve-3d;
	  transition: 0.6s;
    width: 600px;
}

/* hide back of pane during swap */
.front, .back {
    -webkit-backface-visibility: hidden;
	  backface-visibility: hidden;
	  left: 0;
	  position: absolute;
	  top: 0;
}

/* front pane, placed above back */
.front {
	  backface-visibility: hidden;
    background: var(--asparagus);
    transform: rotateY(0deg);
	  z-index: 2;
}

/* back, initially hidden pane */
.back {
	  backface-visibility: hidden;
    background: var(--dark-blue-gray);
	  transform: rotateY(180deg);
}

.next {
    color: var(--rich-black-fogra-39);
    cursor:pointer;
    font-size: 16pt;
    left:600px;
    opacity:0.3;
    position:relative;
    top:20px;
}

.next.flipped {
    opacity:1;
}

.next.hide {
    opacity:0;
}

.flashcard{
    border-radius: 10px;
    color: #fafafa;
    display:block;
    padding:10px;

}
.flashcardtext{
    color: var(--snow);
    font-size: 20pt;
    margin-left:3%;
    position:absolute;
    text-align:center;
    top: 50%;
    transform: translate(0, -50%);
    width:90%;
  }

</style>
        <div style="height:40px"></div><div class="flip-container" id="XRTjIaAmkWrV" onclick="flip(this)"></div><div style="height:40px"></div><div class="next" id="XRTjIaAmkWrV-next" onclick="checkFlip('XRTjIaAmkWrV')"> </div> <div style="height:40px"></div>

```python

```