```
import sys
try:
       import torch
except:
    import os
    os.environ['TCMALLOC_LARGE_ALLOC_REPORT_THRESHOLD']='2000000000'
    # http://pytorch.org/
    from os.path import exists
    from wheel.pep425tags import get_abbr_impl, get_impl_ver, get_abi_tag
    platform = '{}{}-{}'.format(get_abbr_impl(), get_impl_ver(), get_abi_tag())
    cuda_output = !ldconfig -p|grep cudart.so|sed -e 's/.*\.\([0-9]*\)\.\([0-9]*\)$/cu\1\2/'
    accelerator = cuda_output[0] if exists('/dev/nvidia0') else 'cpu'

    !{sys.executable} -m pip install -q http://download.pytorch.org/whl/{accelerator}/torch-0.4.1-{platform}-linux_x86_64.whl torchvision >/dev/null

import torch
from IPython.core.display import Image
```

```
# get the requred files
! wget https://raw.githubusercontent.com/udacity/deep-learning-v2-pytorch/master/intro-to-pytorch/fc_model.py >/dev/null 2>&1
! wget https://raw.githubusercontent.com/udacity/deep-learning-v2-pytorch/master/intro-to-pytorch/helper.py >/dev/null 2>&1
```

# Introduction to Deep Learning with PyTorch

In this notebook, you'll get introduced to [PyTorch](http://pytorch.org/), a framework for building and training neural networks. PyTorch in a lot of ways behaves like the arrays you love from Numpy. These Numpy arrays, after all, are just tensors. PyTorch takes these tensors and makes it simple to move them to GPUs for the faster processing needed when training neural networks. It also provides a module that automatically calculates gradients (for backpropagation!) and another module specifically for building neural networks. All together, PyTorch ends up being more coherent with Python and the Numpy/Scipy stack compared to TensorFlow and other frameworks.

## Neural Networks

Deep Learning is based on artificial neural networks which have been around in some form since the late 1950s. The networks are built from individual parts approximating neurons, typically called units or simply "neurons." Each unit has some number of weighted inputs. These weighted inputs are summed together (a linear combination) then passed through an activation function to get the unit's output.

Mathematically this looks like:

$$
\begin{align}
y &= f(w_1 x_1 + w_2 x_2 + b) \\
y &= f\left(\sum_i w_i x_i +b \right)
\end{align}
$$

With vectors this is the dot/inner product of two vectors:

$$
h = \begin{bmatrix}
x_1 \, x_2 \cdots  x_n
\end{bmatrix}
\cdot
\begin{bmatrix}
           w_1 \\
           w_2 \\
           \vdots \\
           w_n
\end{bmatrix}
$$

## Tensors

It turns out neural network computations are just a bunch of linear algebra operations on *tensors*, a generalization of matrices. A vector is a 1-dimensional tensor, a matrix is a 2-dimensional tensor, an array with three indices is a 3-dimensional tensor (RGB color images for example). The fundamental data structure for neural networks are tensors and PyTorch (as well as pretty much every other deep learning framework) is built around tensors.

With the basics covered, it's time to explore how we can use PyTorch to build a simple neural network.

```
# First, import PyTorch
import torch
```

```
def activation(x):
    """ Sigmoid activation function

        Arguments
        ---------
        x: torch.Tensor
    """
    return 1/(1+torch.exp(-x))
```

```
### Generate some data
torch.manual_seed(7) # Set the random seed so things are predictable

# Features are 5 random normal variables
features = torch.randn((1, 5))
# True weights for our data, random normal variables again
weights = torch.randn_like(features)
# and a true bias term
bias = torch.randn((1, 1))
```

Above I generated data we can use to get the output of our simple network. This is all just random for now, going forward we'll start using normal data. Going through each relevant line:

`features = torch.randn((1, 5))` creates a tensor with shape `(1, 5)`, one row and five columns, that contains values randomly distributed according to the normal distribution with a mean of zero and standard deviation of one.

`weights = torch.randn_like(features)` creates another tensor with the same shape as `features`, again containing values from a normal distribution.

Finally, `bias = torch.randn((1, 1))` creates a single value from a normal distribution.

PyTorch tensors can be added, multiplied, subtracted, etc, just like Numpy arrays. In general, you'll use PyTorch tensors pretty much the same way you'd use Numpy arrays. They come with some nice benefits though such as GPU acceleration which we'll get to later. For now, use the generated data to calculate the output of this simple single layer network.
> **Exercise**: Calculate the output of the network with input features `features`, weights `weights`, and bias `bias`. Similar to Numpy, PyTorch has a [`torch.sum()`](https://pytorch.org/docs/stable/torch.html#torch.sum) function, as well as a `.sum()` method on tensors, for taking sums. Use the function `activation` defined above as the activation function.

```
## Calculate the output of this network using the weights and bias tensors
y = activation(torch.sum(torch.matmul(weights,features),bias))
```

You can do the multiplication and sum in the same operation using a matrix multiplication. In general, you'll want to use matrix multiplications since they are more efficient and accelerated using modern libraries and high-performance computing on GPUs.

Here, we want to do a matrix multiplication of the features and the weights. For this we can use [`torch.mm()`](https://pytorch.org/docs/stable/torch.html#torch.mm) or [`torch.matmul()`](https://pytorch.org/docs/stable/torch.html#torch.matmul) which is somewhat more complicated and supports broadcasting. If we try to do it with `features` and `weights` as they are, we'll get an error

```python
>> torch.mm(features, weights)

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-13-15d592eb5279> in <module>()
----> 1 torch.mm(features, weights)

RuntimeError: size mismatch, m1: [1 x 5], m2: [1 x 5] at /Users/soumith/minicondabuild3/conda-bld/pytorch_1524590658547/work/aten/src/TH/generic/THTensorMath.c:2033
```

As you're building neural networks in any framework, you'll see this often. Really often. What's happening here is our tensors aren't the correct shapes to perform a matrix multiplication. Remember that for matrix multiplications, the number of columns in the first tensor must equal to the number of rows in the second column. Both `features` and `weights` have the same shape, `(1, 5)`. This means we need to change the shape of `weights` to get the matrix multiplication to work.

**Note:** To see the shape of a tensor called `tensor`, use `tensor.shape`. If you're building neural networks, you'll be using this method often.

There are a few options here: [`weights.reshape()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.reshape), [`weights.resize_()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.resize_), and [`weights.view()`](https://pytorch.org/docs/stable/tensors.html#torch.Tensor.view).

* `weights.reshape(a, b)` will return a new tensor with the same data as `weights` with size `(a, b)` sometimes, and sometimes a clone, as in it copies the data to another part of memory.
* `weights.resize_(a, b)` returns the same tensor with a different shape. However, if the new shape results in fewer elements than the original tensor, some elements will be removed from the tensor (but not from memory). If the new shape results in more elements than the original tensor, new elements will be uninitialized in memory. Here I should note that the underscore at the end of the method denotes that this method is performed **in-place**. Here is a great forum thread to [read more about in-place operations](https://discuss.pytorch.org/t/what-is-in-place-operation/16244) in PyTorch.
* `weights.view(a, b)` will return a new tensor with the same data as `weights` with size `(a, b)`.

I usually use `.view()`, but any of the three methods will work for this. So, now we can reshape `weights` to have five rows and one column with something like `weights.view(5, 1)`.

> **Exercise**: Calculate the output of our little network using matrix multiplication.

```
## Calculate the output of this network using matrix multiplication

y = activation(torch.sum(torch.matmul(weights.view(5,1),features)+bias))
```

```

```

Here we use + so that to broacast the bias

### Stack them up!

That's how you can calculate the output for a single neuron. The real power of this algorithm happens when you start stacking these individual units into layers and stacks of layers, into a network of neurons. The output of one layer of neurons becomes the input for the next layer. With multiple input units and output units, we now need to express the weights as a matrix.

The first layer shown on the bottom here are the inputs, understandably called the **input layer**. The middle layer is called the **hidden layer**, and the final layer (on the right) is the **output layer**. We can express this network mathematically with matrices again and use matrix multiplication to get linear combinations for each unit in one operation. For example, the hidden layer ($h_1$ and $h_2$ here) can be calculated

$$
\vec{h} = [h_1 \, h_2] =
\begin{bmatrix}
x_1 \, x_2 \cdots \, x_n
\end{bmatrix}
\cdot
\begin{bmatrix}
           w_{11} & w_{12} \\
           w_{21} &w_{22} \\
           \vdots &\vdots \\
           w_{n1} &w_{n2}
\end{bmatrix}
$$

The output for this small network is found by treating the hidden layer as inputs for the output unit. The network output is expressed simply

$$
y =  f_2 \! \left(\, f_1 \! \left(\vec{x} \, \mathbf{W_1}\right) \mathbf{W_2} \right)
$$

```
### Generate some data
torch.manual_seed(7) # Set the random seed so things are predictable

# Features are 3 random normal variables
features = torch.randn((1, 3))

# Define the size of each layer in our network
n_input = features.shape[1]     # Number of input units, must match number of input features
n_hidden = 2                    # Number of hidden units
n_output = 1                    # Number of output units

# Weights for inputs to hidden layer
W1 = torch.randn(n_input, n_hidden)
# Weights for hidden layer to output layer
W2 = torch.randn(n_hidden, n_output)

# and bias terms for hidden and output layers
B1 = torch.randn((1, n_hidden))
B2 = torch.randn((1, n_output))
```

> **Exercise:** Calculate the output for this multi-layer network using the weights `W1` & `W2`, and the biases, `B1` & `B2`.

```
## Your solution here
# first activation unit (hidden layer)
a1 = activation(torch.matmul(features, W1) + B1)

# Output Layer
y = activation(torch.matmul(a1, W2) + B2)
y
```

    tensor([[0.3171]])

As features is of shape (1,3) and weights (3,2) we should use them in the same order in MatMul operation

If you did this correctly, you should see the output `tensor([[ 0.3171]])`.

The number of hidden units a parameter of the network, often called a **hyperparameter** to differentiate it from the weights and biases parameters. As you'll see later when we discuss training a neural network, the more hidden units a network has, and the more layers, the better able it is to learn from data and make accurate predictions.

## Numpy to Torch and back

Special bonus section! PyTorch has a great feature for converting between Numpy arrays and Torch tensors. To create a tensor from a Numpy array, use `torch.from_numpy()`. To convert a tensor to a Numpy array, use the `.numpy()` method.

```
import numpy as np
a = np.random.rand(4,3)
a
```

    array([[0.84722099, 0.78130965, 0.16510276],
           [0.81326341, 0.05835855, 0.3870242 ],
           [0.1314127 , 0.30821338, 0.47271118],
           [0.03319515, 0.93786461, 0.98596601]])

```
b = torch.from_numpy(a)         # create a tensor from a Numpy array
b
```

    tensor([[0.8472, 0.7813, 0.1651],
            [0.8133, 0.0584, 0.3870],
            [0.1314, 0.3082, 0.4727],
            [0.0332, 0.9379, 0.9860]], dtype=torch.float64)

```
b.numpy()              # convert a tensor to a Numpy array
```

    array([[0.84722099, 0.78130965, 0.16510276],
           [0.81326341, 0.05835855, 0.3870242 ],
           [0.1314127 , 0.30821338, 0.47271118],
           [0.03319515, 0.93786461, 0.98596601]])

The memory is shared between the Numpy array and Torch tensor, so if you change the values in-place of one object, the other will change as well.

```
# Multiply PyTorch Tensor by 2, in place
b.mul_(2)
```

    tensor([[1.6944, 1.5626, 0.3302],
            [1.6265, 0.1167, 0.7740],
            [0.2628, 0.6164, 0.9454],
            [0.0664, 1.8757, 1.9719]], dtype=torch.float64)

```
# Numpy array matches new values from Tensor
a
```

    array([[1.69444197, 1.56261931, 0.33020551],
           [1.62652682, 0.1167171 , 0.77404841],
           [0.2628254 , 0.61642676, 0.94542235],
           [0.06639029, 1.87572921, 1.97193202]])

```

```

```

```

```

```

intro-to-pytorch/Part 1 - Tensors in PyTorch (Exercises Solved).ipynb


## Subsurface Data Analytics

### Ridge Regression for Subsurface Data Analytics in Python

#### Michael Pyrcz, Associate Professor, University of Texas at Austin

##### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

### PGE 383 Exercise: Ridge Regression for Subsurface Modeling in Python

Here's a simple workflow, demonstration of ridge regression for subsurface modeling workflows. This should help you get started with building subsurface models with data analytics and machine learning. Here's some basic details about linear regression.

Ridge regression is an extension of linear regresion, so let's review some basic details about linear regression first.

#### Linear Regression

Linear regression for prediction.  Here are some key aspects of linear regression:

**Parametric Model**

* the fit model is a simple weighted linear additive model based on all the available features, $x_1,\ldots,x_m$.

* the parametric model takes the form of:

\begin{equation}
y = \sum_{\alpha = 1}^m b_{\alpha} x_{\alpha} + b_0
\end{equation}

**Least Squares**

* least squares optimization is applied to select the model parameters, $b_1,\ldots,b_m,b_0$

* we minize the error, residual sum of squares (RSS) over the training data:

\begin{equation}
RSS = \sum_{i=1}^n (y_i - (\sum_{\alpha = 1}^m b_{\alpha} x_{\alpha} + b_0))^2
\end{equation}

* this could be simplified as the sum of square error over the training data,

\begin{equation}
\sum_{i=1}^n (\Delta y_i)^2
\end{equation}

**Assumptions**

* **Error-free** - predictor variables are error free, not random variables
* **Linearity** - response is linear combination of feature(s)
* **Constant Variance** - error in response is constant over predictor(s) value
* **Independence of Error** - error in response are uncorrelated with each other
* **No multicollinearity** - none of the features are redundant with other features

#### Other Resources

This is a tutorial / demonstration of **Linear Regression**.  In $Python$, the $SciPy$ package, specifically the $Stats$ functions (https://docs.scipy.org/doc/scipy/reference/stats.html) provide excellent tools for efficient use of statistics.
I have previously provided this example in R and posted it on GitHub:

1. R https://github.com/GeostatsGuy/geostatsr/blob/master/linear_regression_demo_v2.R
2. Rmd with docs https://github.com/GeostatsGuy/geostatsr/blob/master/linear_regression_demo_v2.Rmd
3. knit as an HTML document(https://github.com/GeostatsGuy/geostatsr/blob/master/linear_regression_demo_v2.html)

#### Ridge Regression

With ridge regression we add a hyperparameter, $\lambda$, to our minimization, with a shrinkage penalty term.

\begin{equation}
\sum_{i=1}^n (y_i - (\sum_{\alpha = 1}^m b_{\alpha} x_{\alpha} + b_0))^2 + \lambda \sum_{j=1}^m b_{\alpha}^2
\end{equation}

As a result ridge regression has 2 criteria:

* set the model parameters to minimize the error with training data

* shrink the estimates of the slope parameters towards zero

Note: the intercept is not affected by lambda.

The $\lambda$ is a hyperparameter that controls the degree of fit of the model and may be related to the model variance and bias trade-off.

* for $\lambda \rightarrow 0$ the solution approaches linear regression, there is no bias (relative to a linear model fit), but the variance is high

* as $\lambda$ increases the model variance decreases and the model bias increases

* for $\lambda \rightarrow \infty$ the coefficients approach 0.0 and the model approaches the global mean

#### Workflow Goals

Learn the basics of ridge regression in Python to for analysis, modeling and prediction of porosity from density. This includes:

* Basic Python workflows and data preparation

* Training / fitting a ridge regression model

* Checking the model and learning about the impact of hyperparameters

#### Objective

In the PGE 383: Stochastic Subsurface Modeling class I want to provide hands-on experience with building subsurface modeling workflows. Python provides an excellent vehicle to accomplish this. I have coded a package called GeostatsPy with GSLIB: Geostatistical Library (Deutsch and Journel, 1998) functionality that provides basic building blocks for building subsurface modeling workflows.

The objective is to remove the hurdles of subsurface modeling workflow construction by providing building blocks and sufficient examples. This is not a coding class per se, but we need the ability to 'script' workflows working with numerical methods.

#### Getting Started

Here's the steps to get setup in Python with the GeostatsPy package:

1. Install Anaconda 3 on your machine (https://www.anaconda.com/download/).
2. From Anaconda Navigator (within Anaconda3 group), go to the environment tab, click on base (root) green arrow and open a terminal.
3. In the terminal type: pip install geostatspy.
4. Open Jupyter and in the top block get started by copy and pasting the code block below from this Jupyter Notebook to start using the geostatspy functionality.

There are examples below with these functions. You can go here to see a list of the available functions, https://git.io/fh4eX, other example workflows and source code.

#### Import Required Packages

Let's import the GeostatsPy package.

```python
import os                                                   # to set current working directory
import numpy as np                                          # arrays and matrix math
import scipy.stats as st                                    # statistical methods
import pandas as pd                                         # DataFrames
import matplotlib.pyplot as plt                             # for plotting
from sklearn.metrics import mean_squared_error, r2_score    # specific measures to check our models
from sklearn.linear_model import Ridge                      # ridge regression implemented in scikit learn
from sklearn.model_selection import cross_val_score         # multi-processor K-fold crossvalidation
```

If you get a package import error, you may have to first install some of these packages. This can usually be accomplished by opening up a command window on Windows and then typing 'python -m pip install [package-name]'. More assistance is available with the respective package docs.

#### Set the working directory

I always like to do this so I don't lose files and to simplify subsequent read and writes (avoid including the full address each time).  Also, in this case make sure to place the required (see below) data file in this working directory.

```python
os.chdir("C:\PGE337")                                       # set the working directory
```

#### Loading Data

Let's load the provided dataset. 'Density_Por_data.csv' is available at https://github.com/GeostatsGuy/GeoDataSets. It is a comma delimited file with 20 density ($\frac{g}{cm^3}$) and porosity (as a fraction) measures from the subsurface. We load the data file with the pandas 'read_csv' function into a data frame we called 'df' and then separate it into train and test datasets.  The smaples are in random order so we just split the dataset at the 80th sample.  We preview each with the head function from Pandas DataFrames.

```python
df = pd.read_csv("Density_Por_data.csv")                    # read a .csv file in as a DataFrame
df_train = df.iloc[0:80,:]                                  # extract a training set, note samples are random ordered
df_train.head()                                             # preview the DataFrame
```

      Density
      Porosity

      0
      1.281391
      16.610982

      1
      1.404932
      13.668073

      2
      2.346926
      9.590092

      3
      1.348847
      15.877907

      4
      2.331653
      4.968240

```python
df_test = df.iloc[80:]                                      # extract a testing set, note samples are random ordered
df_test.head()                                              # preview the DataFrame
```

      Density
      Porosity

      80
      1.750352
      11.325941

      81
      1.666285
      15.609445

      82
      1.466517
      17.066529

      83
      1.650921
      13.876841

      84
      0.996736
      20.964941

It is useful to review the summary statistics of our loaded DataFrame.  That can be accomplished with the 'describe' DataFrame member function.  We transpose to switch the axes for ease of visualization.  We will summarize over the training and testing subsets separately.

```python
df_train.describe().transpose()
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      Density
      80.0
      1.759945
      0.295260
      1.067960
      1.552707
      1.767908
      1.953262
      2.410560

      Porosity
      80.0
      12.187127
      3.125755
      4.966421
      10.011526
      12.170687
      14.063670
      19.600717

```python
df_test.describe().transpose()
```

      count
      mean
      std
      min
      25%
      50%
      75%
      max

      Density
      25.0
      1.667429
      0.257608
      0.996736
      1.568988
      1.718085
      1.791432
      2.339324

      Porosity
      25.0
      13.632564
      2.948887
      9.489298
      11.325941
      13.767060
      15.051759
      20.964941

Here we extract the ndarrays with porsity and density, training and testing datasets separate arrays for convenience.

```python
por_train = df_train['Porosity'].values                     # make a shallow copy of the features for convenvience
df = pd.read_csv("Density_Por_data.csv")
den_train = df_train['Density'].values
por_test = df_test['Porosity'].values
den_test = df_test['Density'].values
n_train = len(df_train); n_test = len(df_test)              # get the number of data in training and testing
```

Let's look at the training and testing data.

```python
plt.subplot(111)
plt.scatter(df_train["Density"].values, df_train["Porosity"],  color='black', s = 20, alpha = 0.3, label = 'training')
plt.scatter(df_test["Density"].values, df_test["Porosity"],  color='red', s = 20, alpha = 0.3, label = 'testing')
plt.title('Linear Regression Porosity from Density with Training Data'); plt.xlabel('Density (g/cm^3)'); plt.ylabel('Porosity (%)')
plt.legend()
plt.xlim(1.,2.6)#; plt.ylim(0,1500000)
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
plt.show()
```

#### Linear Regression Model

Let's first calculate the linear regression model. We use scikit learn and then extend the same workflow to ridge regression.

```python
from sklearn import linear_model

# Step 1. Instantiate the Model
linear_reg = linear_model.LinearRegression()

# Step 2: Fit the Data on Training Data
linear_reg.fit(df_train["Density"].values.reshape(n_train,1), df_train["Porosity"]) # fit model
density_model = np.linspace(1.2,2.4,10)

# Print the model parameters
porosity_model = linear_reg.predict(density_model.reshape(10,1)) # predict with the fit model
print('Coefficients: ', str(round(linear_reg.coef_[0],3)) + ', Intercept: ', str(round(linear_reg.intercept_,3)))

# Plot model fit
plt.subplot(111)
plt.scatter(df_train["Density"].values, df_train["Porosity"],  color='black', s = 20, alpha = 0.3)
plt.plot(density_model,porosity_model, color='red', linewidth=1)
plt.title('Linear Regression Porosity from Density with Training Data'); plt.xlabel('Density (g/cm^3)'); plt.ylabel('Porosity (%)')
plt.xlim(1.,2.6)#; plt.ylim(0,1500000)
plt.subplots_adjust(left=0.0, bottom=0.0, right=1.0, top=1.0, wspace=0.2, hspace=0.2)
plt.show()
```

Let's run some quick model checks.  Much more could be done, but I limit this for breviety here.

```python
# Step 3: - Make predictions using the testing dataset
y_pred = linear_reg.predict(df_test['Density'].values.reshape(n_test,1))

# Report the goodness of fit
print('Variance explained: %.2f' % r2_score(df_test['Porosity'].values, y_pred))

# Plot testing diagnostics
plt.subplot(121)
plt.scatter(df_test['Density'].values, df_test['Porosity'].values,  color='black', s = 20, alpha = 0.3)
plt.scatter(df_test['Density'], y_pred, color='blue', s = 20, alpha = 0.3)
plt.title('Linear Regression Model Testing - Production from Porosity'); plt.xlabel('Density (g/cm^3)'); plt.ylabel('Porosity (%)')
plt.xlim(1.0,2.6); plt.ylim(5,24)

y_res = y_pred - df_test['Porosity'].values

print('Residual: mean = ' + str(round(np.average(y_res),2)) + ', standard deviation = ' + str(round(np.var(y_res),2)))

plt.subplot(122)
plt.hist(y_res, alpha = 0.2, color = 'red', edgecolor = 'black', bins=20)
plt.title('Linear Regression Model Prediction Error - Porosity form Density'); plt.xlabel('Porosity Estimation Error (%) (Estimate - Truth)'); plt.ylabel('Frequency')
plt.xlim(-4,4)#; plt.ylim(0,1500000)

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.2, top=1.2, wspace=0.3, hspace=0.2)
plt.show()
```

#### Ridge Regression

Let's replace the scikit learn linear regression method with the scikit learn ridge regression method.  Note, we must now set the lambda hyperparameter.

* the hyperparameter is set with the instantiation of the model

```python
lam = 1.0

# Step 1. Instantiate the Model
ridge_reg = Ridge(alpha=lam)

# Step 2: Fit the Data on Training Data
ridge_reg.fit(df_train["Density"].values.reshape(n_train,1), df_train["Porosity"]) # fit model
density_model = np.linspace(1.2,2.4,10)

# Print the model parameters
porosity_model = ridge_reg.predict(density_model.reshape(10,1)) # predict with the fit model
print('Coefficients: ', str(round(linear_reg.coef_[0],3)) + ', Intercept: ', str(round(linear_reg.intercept_,3)))

# Plot model fit
plt.subplot(111)
plt.scatter(df_train["Density"].values, df_train["Porosity"],  color='black', s = 20, alpha = 0.3)
plt.plot(density_model,porosity_model, color='red', linewidth=1)
plt.title('Ridge Regression Porosity from Density with Training Data'); plt.xlabel('Density (g/cm^3)'); plt.ylabel('Porosity (%)')
plt.xlim(1.,2.6); plt.ylim(8,17)
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
plt.show()
```

Let's repeat the simple model checks that we applied with our linear regression model.

```python
# Step 3: - Make predictions using the testing dataset
y_pred = ridge_reg.predict(df_test['Density'].values.reshape(n_test,1))

# Report the goodness of fit
print('Variance explained: %.2f' % r2_score(df_test['Porosity'].values, y_pred))

# Plot testing diagnostics
plt.subplot(121)
plt.scatter(df_test['Density'].values, df_test['Porosity'].values,  color='black', s = 20, alpha = 0.3)
plt.scatter(df_test['Density'], y_pred, color='blue', s = 20, alpha = 0.3)
plt.title('Linear Regression Model Testing - Production from Porosity'); plt.xlabel('Density (g/cm^3)'); plt.ylabel('Porosity (%)')
plt.xlim(1.0,2.6); plt.ylim(5,24)

y_res = y_pred - df_test['Porosity'].values

print('Residual: mean = ' + str(round(np.average(y_res),2)) + ', standard deviation = ' + str(round(np.var(y_res),2)))

plt.subplot(122)
plt.hist(y_res, alpha = 0.2, color = 'red', edgecolor = 'black', bins=20)
plt.title('Linear Regression Model Prediction Error - Porosity form Density'); plt.xlabel('Porosity Estimation Error (%) (Estimate - Truth)'); plt.ylabel('Frequency')
plt.xlim(-4,4)#; plt.ylim(0,1500000)

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.2, top=1.2, wspace=0.3, hspace=0.2)
plt.show()
```

Interesting, we explained less variance and have a larger residual standard deviation (more error).

* we see we actually reduced both testing variance explained and accuracy

#### Investigating the Lambda Hyperparameter

Let's loop over multiple lambda values - from 0 to 100 and observe the change in:

* training and testing, mean square error (MSE) and variance explained

```python
# Arrays to store the results
ncases = 100
lamd_mat = np.linspace(0.0,100.0,ncases)
density_model = np.linspace(1.2,2.4,10)
var_explained_train = np.zeros(ncases); var_explained_test = np.zeros(ncases)
mse_train = np.zeros(ncases); mse_test = np.zeros(ncases)

for ilam in range(0,len(lamd_mat)):                         # Loop over all lambda values
    ridge_reg = Ridge(alpha=lamd_mat[ilam])
    ridge_reg.fit(df_train["Density"].values.reshape(n_train,1), df_train["Porosity"]) # fit model

    porosity_model = ridge_reg.predict(density_model.reshape(10,1)) # predict with the fit model
    porosity_pred_train = ridge_reg.predict(df_train['Density'].values.reshape(n_train,1)) # predict with the fit model
    var_explained_train[ilam] = r2_score(df_train['Porosity'].values, porosity_pred_train)
    mse_train[ilam] = mean_squared_error(df_train['Porosity'].values, porosity_pred_train)

    porosity_pred_test = ridge_reg.predict(df_test['Density'].values.reshape(n_test,1))
    var_explained_test[ilam] = r2_score(df_test['Porosity'].values, porosity_pred_test)
    mse_test[ilam] = mean_squared_error(df_test['Porosity'].values, porosity_pred_test)

    if ilam <= 7:
        plt.subplot(4,2,ilam+1)
        plt.scatter(df_train["Density"].values, df_train["Porosity"],  color='black', s = 20, alpha = 0.3)
        plt.plot(density_model,porosity_model, color='red', linewidth=1)
        plt.title('Ridge Regression Porosity from Density with Training Data - Lambda = ' + str(round(lamd_mat[ilam],2))); plt.xlabel('Density (g/cm^3)'); plt.ylabel('Porosity (%)')
        plt.xlim(1.,2.6); plt.ylim(5,24)

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=4.2, wspace=0.2, hspace=0.3)
plt.show()
```

We can observed from the first 8 cases above of ridge regression model fit that increase in the lambda hyper parameter decreases the slope of the linear fit.

Let's plot the MSE and variance explained over training and testing datasets.

```python
plt.subplot(121)
plt.plot(lamd_mat, var_explained_train,  color='blue', linewidth = 2, label = 'Training')
plt.plot(lamd_mat, var_explained_test,  color='red', linewidth = 2, label = 'Test')
plt.title('Variance Explained vs. Lambda'); plt.xlabel('Lambda'); plt.ylabel('Variance Explained')
plt.xlim(0.,100.); plt.ylim(0,1.0)
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
plt.legend()

plt.subplot(122)
plt.plot(lamd_mat, mse_train,  color='blue', linewidth = 2, label = 'Training')
plt.plot(lamd_mat, mse_test,  color='red', linewidth = 2, label = 'Test')
plt.title('MSE vs. Lambda'); plt.xlabel('Lambda'); plt.ylabel('Mean Square Error')
plt.xlim(0.,100.); plt.ylim(0,10.0)
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
plt.legend()

plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.2, wspace=0.2, hspace=0.3)
plt.show()
```

We observe that as we increase the lambda parameter the variance explained decreases and the mean square error increases.

* this makes sense as the data has a consistent linear trend and as the slope 'shrinks' to zero the error increases and the variance explained decreases

* there could be other cases where the reduced slope actually performs better in testing.  For example with sparce and noisy data.

#### Model Variance

Now let's explore the concept of model variance, an important part of machine learning accuracy in testing.

* the sensitivity of the model to the specfic training data

* as lambda increases the sensitivity to the training data, model variance decreases

Let's demonstrate this with this workflow:

* loop over multiple lambda values
    * loop over multiple bootstrap samples of the data
        * calculate the ridge regression fit (slope)
    * calculate the variance of these bootstrap results

```python
L = 200                                                     # the number of bootstrap realizations
nsamples = 20                                               # the number of samples in each bootstrap realization
nlambda = 100                                               # number of lambda values to evaluate

coef_mat = np.zeros(L)                                      # declare arrays to store the results
variance_coef = np.zeros(nlambda)

lamd_mat = np.linspace(0.0,100.0,nlambda)
df = pd.read_csv("Density_Por_data.csv")
for ilam in range(0,len(lamd_mat)):                         # loop over all lambda values
    for l in range(0, L):                                   # loop over all bootstrap realizations
        df_sample = df.sample(n = nsamples)                 # random sample (1 bootstrap)
        ridge_reg = Ridge(alpha=lamd_mat[ilam])             # instatiate model
        ridge_reg.fit(df_sample["Density"].values.reshape(nsamples,1), df_sample["Porosity"]) # fit model
        coef_mat[l] = ridge_reg.coef_[0]                    # get the slope parameter

    variance_coef[ilam] = np.var(coef_mat)                  # calculate the variance of the slopes over the L bootstraps
```

Now let's plot the result.

```python
plt.subplot(111)
plt.plot(lamd_mat, variance_coef,  color='black', linewidth = 2, label = 'Slope Variance')
plt.title('Model Fit Variance vs. Lambda'); plt.xlabel('Lambda'); plt.ylabel('Model Fit Variance')
plt.xlim(0.,100.); plt.ylim(0.001,10.0); plt.yscale('log')
plt.subplots_adjust(left=0.0, bottom=0.0, right=2.0, top=1.0, wspace=0.2, hspace=0.2)
plt.legend()
```

The result is as expected, with increase in lambda hyperparameter the sensitivity of the model to the training data is decreased.

#### k-fold Cross Validation

It would be useful to conduct a complete k-fold validation to evaluate the testing error vs. the hyperparameter lambda for model tuning.

* the following code should do this

* but with a single feature as input for fitting the fit function requires a reshape

```python
my_array.reshape((nsample,1))
```

* this is not included in the scikit learn function 'cross_val_score' so we will skip this for now

I have left the code commented out below for reference:

```python
#score = []                                                  # code modified from StackOverFlow by Dimosthenis
#nlambda = 1
#lambd_mat = np.linspace(0.0,100.0,nlambda)
#for ilam in range(0,nlambda):
#    ridge_reg = Ridge(alpha=lambd_mat[ilam])
#    scores = cross_val_score(estimator=ridge_reg, X= df['Density'].values, y=df['Porosity'].values, cv=10, n_jobs=4, scoring = "neg_mean_squared_error") # Perform 10-fold cross validation
#    score.append(abs(scores.mean()))
```

#### Comments

Ridge regression is a variant of linear regression that includes a hyperparameter to constrain the degree of model fit.  This allow us to tune the variance-bias trade-off of our model. I hope this was helpful,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin
On twitter I'm the @GeostatsGuy.

***

#### More on Michael Pyrcz and the Texas Center for Geostatistics:

### Michael Pyrcz, Associate Professor, University of Texas at Austin
*Novel Data Analytics, Geostatistics and Machine Learning Subsurface Solutions*

With over 17 years of experience in subsurface consulting, research and development, Michael has returned to academia driven by his passion for teaching and enthusiasm for enhancing engineers' and geoscientists' impact in subsurface resource development.

For more about Michael check out these links:

#### [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

#### Want to Work Together?

I hope this content is helpful to those that want to learn more about subsurface modeling, data analytics and machine learning. Students and working professionals are welcome to participate.

* Want to invite me to visit your company for training, mentoring, project review, workflow design and / or consulting? I'd be happy to drop by and work with you!

* Interested in partnering, supporting my graduate student research or my Subsurface Data Analytics and Machine Learning consortium (co-PIs including Profs. Foster, Torres-Verdin and van Oort)? My research combines data analytics, stochastic modeling and machine learning theory with practice to develop novel methods and workflows to add value. We are solving challenging subsurface problems!

* I can be reached at mpyrcz@austin.utexas.edu.

I'm always happy to discuss,

*Michael*

Michael Pyrcz, Ph.D., P.Eng. Associate Professor The Hildebrand Department of Petroleum and Geosystems Engineering, Bureau of Economic Geology, The Jackson School of Geosciences, The University of Texas at Austin

#### More Resources Available at: [Twitter](https://twitter.com/geostatsguy) | [GitHub](https://github.com/GeostatsGuy) | [Website](http://michaelpyrcz.com) | [GoogleScholar](https://scholar.google.com/citations?user=QVZ20eQAAAAJ&hl=en&oi=ao) | [Book](https://www.amazon.com/Geostatistical-Reservoir-Modeling-Michael-Pyrcz/dp/0199731446) | [YouTube](https://www.youtube.com/channel/UCLqEr-xV-ceHdXXXrTId5ig)  | [LinkedIn](https://www.linkedin.com/in/michael-pyrcz-61a648a1)

```python

```


```python
# Descomente para rodar no colab
# ! pip install git+https://github.com/flaviovdf/musica.git
```

    Collecting git+https://github.com/flaviovdf/musica.git
      Cloning https://github.com/flaviovdf/musica.git to /tmp/pip-req-build-653f2dwl
      Running command git clone -q https://github.com/flaviovdf/musica.git /tmp/pip-req-build-653f2dwl
    Building wheels for collected packages: musica-dcc-ufmg
      Building wheel for musica-dcc-ufmg (setup.py) ... done
      Created wheel for musica-dcc-ufmg: filename=musica_dcc_ufmg-0.1-py3-none-any.whl size=3357 sha256=7630060dc428b2414ec63c8d007ff13edf365edc47b56fe56ef5a0e55123cc51
      Stored in directory: /tmp/pip-ephem-wheel-cache-9mo34non/wheels/94/cb/e0/72e4ee72153bf78a970722ac92a93e8a8f31c1853e32a3d1bc
    Successfully built musica-dcc-ufmg
    Installing collected packages: musica-dcc-ufmg
    Successfully installed musica-dcc-ufmg-0.1

# Transformada de Fourier

**Objetivos**
1. Implementar a *fast fourier transform*

**Resultado Esperado**
1. Entender qual é a matriz base do processamento musical

```python
from μsica import μsf
from μsica import μsp
from numba import jit

import librosa
import librosa.display

import IPython.display as ipd

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

import numpy as np
import pandas as pd
```

```python
μsf.download('https://dcc.ufmg.br/~flaviovdf/mir/lab04/files.zip', 'files.zip',
             False)
μsf.extract('files.zip', '.')
```

    files.zip: 1.39MB [00:03, 417kB/s]

```python
μsp.init_matplotlib()
plt.ion()
```

## Introdução

Como vimos em sala de aula, a Transformada de Fourier é o algoritmo base para realizar tarefas de Recuperação de Informação Musical. Durante as aulas, vimos três versões da transformada:

1. A transformada contínua
1. A transformada discreta
1. A transformada com janelas (ou *short time fourier transform (STFT)*)

Resumindo o assunto em texto, os sinais de áudio podem ser representados misturas complexas que consistem em uma infinidade de diferentes componentes de som. Tais componentes podem ser encontradas fazendo uso da transformada de Fourier. A mesma mapeia um sinal dependente do tempo para um espectro de frequências que revelam as frequências que das quais o áudio é composto.

Em termos simples, um sinal e sua Transformada de Fourier são dois lados da mesma moeda. Por um lado, o sinal exibe as informações de tempo e esconde o informações sobre frequências. Por outro lado, a transformação de Fourier revela informações sobre frequências e oculta as informações de tempo. Para obter de volta as informações de tempo ocultas, Dennis Gabor apresentou no ano de 1946, a transformada de Fourier STFT.

## Transformada Discreta

A transformada de Fourier é definida como:

\begin{align}
X(k) = \sum_{n=0}^{N-1} e^{-2 \pi i n k / N} x(n) \\
x(n) = \frac{1}{N}\sum_{k=0}^{N-1} e^{2 \pi i n k / N} X(k)
\end{align}

$X(k)$ é a importância de uma frequência no índice $k$. $x(n)$ é o valor do sinal na posição $n$. Como vimos em sala de aula, a mesma pode ser escrita de forma matricial. Para tal, precisamos definir os vetores: $\mathbf{X}[k] = X(k)$ e $\mathbf{x}[n] = x(n)$. Após isto, podemos definir a matriz:

\begin{align}
{\displaystyle \text{DFT}_N ={\begin{pmatrix}1& 1&\cdots &1\\1&\omega^{11}&\cdots &\omega^{2(N-1)}\\\vdots &\vdots &\ddots &\vdots \\1&\omega^{(N-1)1}&\cdots &\omega^{(N-1)(N-1)}\end{pmatrix}}}
\end{align}

Onde,

\begin{align}
\omega^{kn} = (e^{-2 \pi i / N})^{kn} \\
\omega^{kn} = e^{-2 \pi kni / N}
\end{align}

Daqui, a transformada é simplesmente o produto: $\mathbf{X} = \text{DFT}_N \cdot \mathbf{x}$. Com, $\mathbf{X}[k] = X(k)$ e $\mathbf{x}[n] = x(n)$.

Sua primeira tarefa é implementar uma função Python que retorna a matriz $\text{DFT}_N$. Lembre-se que definimos $k \in [0, N)$ e $n \in [0, N)$. Tanto os coeficientes quanto o número de posições no sinal é o mesmo.

Observe como a função tem uma chamada `@jit` no começo. Tal chamada deixa o código mais rápido a ajuda os alunos que desejam gerar a matriz com laços. Dentro de funções dessa forma, a biblioteca *numba* compila o código para *C*. Então, você é limitado a usar comandos python, tipos simples como listas e vetores/matrizes numpy dentro da função.

```python
@jit(nopython=True)
def generate_matrix_dft(N):
    return M
```

O código abaixo vai plotar os coeficientes reais e imaginários da sua matriz. Observe como existe uma redundância. Aproximadamente uma metade de cada matriz (os blocos na digonais) são iguais. Isto vale tanto para a diagonal principal como a secundária. Tal matriz serve como evidência de que computamos mais coeficientes do que precisamos, algo que foi dito em sala de aula, na prática, apenas K/2 coeficientes seriam necessários.

O código abaixo deve gerar uma saída como:

```python
dft_mat = generate_matrix_dft(32)

plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.title('$\mathrm{Re}(\mathrm{DFT}_N)$')
plt.imshow(np.real(dft_mat), origin='lower', cmap='seismic', aspect='equal')
plt.xlabel('Time index $n$')
plt.ylabel('Frequency index $k$')
plt.colorbar()

plt.subplot(1, 2, 2)
plt.title('$\mathrm{Im}(\mathrm{DFT}_N)$')
plt.imshow(np.imag(dft_mat), origin='lower', cmap='seismic', aspect='equal')
plt.xlabel('Time index $n$')
plt.ylabel('Frequency index $k$')
plt.colorbar()
plt.tight_layout()
```

Agora escreva uma função para realizar a transformada:

```python
@jit(nopython=True)
def dft(x):
    return X
```

```python
X = dft(x)
```

Abaixo fazemos uso da sua função. O código abaixo deve gerar uma saída como:

```python
N = 128
n = np.arange(N)
k = 10
x = np.cos(2 * np.pi * (k * n / N) + 2 * (np.random.rand(N) - 0.5))

plt.figure(figsize=(10, 3))

plt.subplot(1, 2, 1)
plt.title('$x$')
plt.plot(x)
plt.xlabel('Time (index $n$)')

plt.subplot(1, 2, 2)
plt.title('$|X|$')
plt.plot(np.abs(X))
plt.xlabel('Frequency (index $k$)')
plt.tight_layout()
```

A seguir, discutimos a famosa transformada rápida de Fourier (Fast Fourier Transform - FFT). O algoritmo FFT foi originalmente descoberto por Gauss em volta de 1805. No ano de 1965, o mesmo foi então redescoberto por Cooley e Tukey em 1965. O algoritmo FFT é baseado na observação de que a aplicação de uma DFT de tamanho par $ N = 2M $ pode ser expressa em termos de aplicação de duas DFTs com metade do tamanho $ M $. O algoritmo explora o fato de que existem relações algébricas entre as entradas $\omega^{kn} = \mathrm{exp}(-2 \pi i / N)^{kn}$ de matrizes DFT.

No algoritmo FFT, calcula-se o DFT dos elementos pares e ímpares de $x(n)$

\begin{align}
(A(0), \dots, A (N / 2-1)) & = \mathrm{DFT}_{N / 2} \cdot (x (0), x (2), x (4), \cdots, x (N-2)) \\
(B(0), \dots, B (N / 2-1)) & = \mathrm{DFT}_{N / 2} \cdot (x (1), x (3), x (5), \cdots, x (N-1))
\end{align}

Com esses dois DFTs de tamanho $N/2$, pode-se calcular o DFT completo de tamanho $N$ por meio de:

\begin{eqnarray}
C(k) & = & \sigma^k \cdot B(k) \\
X(k) & = & A(k) + C(k) \\
X(N / 2 + k) & = & A(k) - C(k) \\
\end{eqnarray}

para $ k \in [0: N / 2 - 1] $. Os números $\sigma^k$ também são chamados de *fatores de torção*. Se $N$ é uma potência de dois, esta ideia pode ser aplicada recusivamente até que se alcance o cálculo de $ \mathrm {DFT}_{1} $ (o caso $ N = 1 $), que é simplesmente multiplicação por um ( ou seja, apenas retornando o sinal de comprimento $ N = 1 $).

Por fim implemente o algoritmo FFT e plote o mesmo resultado do caso anterior. Os comandos python abaixo separam os pares dos ímpares!

```python
x = np.array([0, 1, 2, 3, 4, 5])
x_par = x[::2]
x_impar = x[1::2]
```

```python
x = np.array([0, 1, 2, 3, 4, 5])
x_par = x[::2]
x_impar = x[1::2]

print(x_par)
print(x_impar)
```

```python
@jit(nopython=True)
def fft(x):
    return X
```

Compare o tempo de execução dos dois com a chamada abaixo:

```python
import timeit

Ns = [2 ** n for n in range(5, 11)]
times_dft = []
times_fft = []
execuctions = 10

for N in Ns:
    n = np.arange(N)
    x = np.sin(2 * np.pi * 5 * n / N )

    time_dft = timeit.timeit(lambda: dft(x), number=execuctions) / execuctions
    time_fft = timeit.timeit(lambda: fft(x), number=execuctions) / execuctions
    times_dft.append(time_dft)
    times_fft.append(time_fft)

plt.figure(figsize=(10, 4))

plt.plot(Ns, times_dft, '-xk', label='DFT')
plt.plot(Ns, times_fft, '-xr', label='FFT')
plt.xticks(Ns)
plt.legend()
plt.grid()
plt.xlabel('$N$')
plt.ylabel('Tempo de execução (seconds)');
```

## Sinal Real com FFT e STFT

Agora sua tarefa será aplicar a transformada em um sinal real.

O aúdio abaixo toca as notas do piano seguindo a escala cromática. A figura também mostra duas versões do espectograma. Sua resposta final deve parecer com a Fig (b).

```python
ipd.Audio(filename='./files/Scale_Chromatic_Piano.wav')
```

Aplique a transformada de Fourier rápida no mesmo. Para tal, você pode fazer uso do pacote já pronto em python:

```python
from scipy.fft import fft
```

```python
song, F_s = librosa.load('./files/Scale_Chromatic_Piano.wav')
print(len(song))
print(F_s)
```

### FFT

O código abaixo carrega a música e indica sua taxa de frequência. Agora, sabendo dos dados abaixo, aplique a Transformada de Fourier usando seu código ou o scipy. Recomendo fazer uso do scipy por corretude! Após isto, responda.

**Questão**: Quais são as três frequência mais importante no sinal? Você deve converter entre índice e frequência aqui. Veja a equação nos slides. Além do mais, você deve descartar metade da saída do código do FFT, é redundante!

> Resposta Aqui. Use Latex

### STFT

Agora vamos fazer uso do código de STFT já pronto em Python. Leia a documentação do mesmo, observe como o mesmo recebe dois argumentos importantes (abaixo os nomes e valores padrão):

1. window='hann'
1. nperseg=256

O primeiro é o tipo de janela. O segundo é o número de elementos por janela.

```python
from scipy.signal import stft
```

Observe como no plot eu coloquei o np.log para deixar as importâncias mais claras!

```python
f, t, Zxx = stft(song, nperseg=500)

plt.figure(figsize=(20, 10))

plt.pcolormesh(t, f, np.log(np.abs(Zxx)), shading='gouraud')
plt.title('STFT Magnitude')
plt.ylabel('Frequency [Hz]')
plt.xlabel('Time [sec]')
plt.show()
```

**Questão**: Para cada segundo da música, qual a frequência mais importante? Você pode fazer um código para responder esta questão.


# TPHLCT 3階の導関数を相殺する

```python
import numpy as np
import scipy.misc
from scipy.fftpack import dct, idct
import sys
from PIL import Image
import matplotlib
import matplotlib.pyplot as plt
import random
from tqdm import tqdm_notebook as tqdm
from scipy.fftpack import dct, idct
import seaborn as sns

%matplotlib inline

class ImageLoader:
    def __init__(self, FILE_PATH):
        self.img = np.array(Image.open(FILE_PATH))
        # 行数
        self.row_blocks_count = self.img.shape[0] // 8
        # 列数
        self.col_blocks_count = self.img.shape[1] // 8

    def get_points(self, POINT):
        Row = random.randint(0, len(self.img) - POINT - 1)
        Col = random.randint(0, len(self.img) - 1)
        return self.img[Row : Row + POINT, Col]

    def get_block(self, col, row):
        return self.img[col * 8 : (col + 1) * 8, row * 8 : (row + 1) * 8]

# plt.rcParams['font.family'] ='sans-serif'#使用するフォント
# plt.rcParams["font.sans-serif"] = "Source Han Sans"
plt.rcParams["font.family"] = "Source Han Sans JP"  # 使用するフォント
plt.rcParams["xtick.direction"] = "in"  # x軸の目盛線が内向き('in')か外向き('out')か双方向か('inout')
plt.rcParams["ytick.direction"] = "in"  # y軸の目盛線が内向き('in')か外向き('out')か双方向か('inout')
plt.rcParams["xtick.major.width"] = 1.0  # x軸主目盛り線の線幅
plt.rcParams["ytick.major.width"] = 1.0  # y軸主目盛り線の線幅
plt.rcParams["font.size"] = 12  # フォントの大きさ
plt.rcParams["axes.linewidth"] = 1.0  # 軸の線幅edge linewidth。囲みの太さ
matplotlib.font_manager._rebuild()

# 画像の用意
AIRPLANE = ImageLoader("Mono/airplane512.bmp")
BARBARA = ImageLoader("Mono/BARBARA.bmp")
BOAT = ImageLoader("Mono/BOAT.bmp")
BRIDGE = ImageLoader("Mono/BRIDGE.bmp")
BUILDING = ImageLoader("Mono/Building.bmp")
CAMERAMAN = ImageLoader("Mono/Cameraman.bmp")
GIRL = ImageLoader("Mono/girl.bmp")
LAX = ImageLoader("Mono/LAX.bmp")
LENNA = ImageLoader("Mono/lenna512.bmp")
LIGHTHOUSE = ImageLoader("Mono/Lighthouse.bmp")
SIDBAtitle = ImageLoader("Mono/SIDBAtitle256.bmp")
TEXT = ImageLoader("Mono/Text.bmp")
WOMAN = ImageLoader("Mono/WOMAN.bmp")

Images = np.array(
    [
        AIRPLANE,
        BARBARA,
        BOAT,
        BRIDGE,
        BUILDING,
        CAMERAMAN,
        GIRL,
        LAX,
        LENNA,
        LIGHTHOUSE,
        SIDBAtitle,
        TEXT,
        WOMAN,
    ]
)

N = 8
```

# DCT

## 基底関数

$$
\phi_k[i] = \begin{cases}
\cfrac{1}{\sqrt{N}}  \quad \quad \quad (k=0) \\
\sqrt{\cfrac{2}{N}} \cos \left({\cfrac{\pi}{2N}(2i+1)k}\right) \quad  (k=1,2,...,N-1)
\end{cases}
$$

```python
class DCT:
    def __init__(self, N):
        self.N = N  # データ数
        # 1次元DCTの基底ベクトルの生成
        self.phi_1d = np.array([self.phi(i) for i in range(self.N)])
        # 2次元DCTの基底ベクトルの格納
        self.phi_2d = np.zeros((N, N))

    def phi(self, k):
        """ 離散コサイン変換(DCT)の基底関数 """
        # DCT-II
        if k == 0:
            return np.ones(self.N) / np.sqrt(self.N)
        else:
            return np.sqrt(2.0 / self.N) * np.cos(
                (k * np.pi / (2 * self.N)) * (np.arange(self.N) * 2 + 1)
            )

    def dct(self, data):
        """ 1次元離散コサイン変換を行う """
        return self.phi_1d.dot(data)

    def idct(self, c):
        """ 1次元離散コサイン逆変換を行う """
        return np.sum(self.phi_1d.T * c, axis=1)

    def get_dct2_phi(self, y, x):
        """ 2次元離散コサイン変換の基底を返す """
        phi_x, phi_y = np.meshgrid(self.phi_1d[x], self.phi_1d[y])
        return phi_x * phi_y

    def get_dct2(self, y, x, data):
        """ i,jの2次元DCT係数を返す """
        phi_2d_phi = np.zeros((self.N, self.N))
        phi_2d_phi = self.get_dct2_phi(y, x)
        return np.sum(np.sum(phi_2d_phi * data))

    def dct2(self, data):
        """ 2次元離散コサイン変換を行う """
        for y in range(self.N):
            for x in range(self.N):
                self.phi_2d[y, x] = self.get_dct2(y, x, data)

        return self.phi_2d

    def idct2(self, c):
        """ 2次元離散コサイン逆変換を行う """
        idct2_data = np.zeros((self.N, self.N))
        phi_2d_phi = np.zeros((self.N, self.N))
        for y in range(self.N):
            for x in range(self.N):
                phi_2d_phi = self.get_dct2_phi(y, x)
                idct2_data += c[y,x] * phi_2d_phi

        return idct2_data
```

# 係数の計算

$\alpha_k, \beta_k$を計算する。
1階導関数を相殺するような予測関数は
$$u(x)=-\frac{f_x(0)}{2}(1-x^2)+\frac{f_x(1)}{2}x^2$$
で与えられる。

そのとき、
$$\alpha_k = -\sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}\frac{(1-x_\ell)^2}{2}\cos(\pi k x_\ell)$$
$$\beta_k = \sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}\frac{x_\ell^2}{2}\cos(\pi k x_\ell)$$

$a_k, b_k, c_k, d_k$を計算する。
3階導関数を相殺するような予測関数は
$$
\begin{align}
u(x) & =( -\frac{1}{2}f_x(0) + \frac{1}{12}f^{(3)}_x(0) )(1-x^2)\\
     & -\frac{1}{24}f^{(3)}_x(0)(1-x)^4\\
     & +( \frac{1}{2}f_x(1)-\frac{1}{12}f^{(3)}_x(1) )x^2\\
     & + \frac{1}{24}f^{(3)}_x(1)x^4
\end{align}
$$
で与えられる。

そのとき、
$$a_k = -\sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}\frac{(1-x_\ell)^2}{2}\cos(\pi k x_\ell)$$

$$b_k= \sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}( \frac{1}{24}-\frac{x_\ell^2}{6}+\frac{x_\ell^3}{6}-\frac{x_\ell^4}{24} )\cos(\pi k x_\ell)$$

$$c_k = \sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}\frac{x_\ell^2}{2}\cos(\pi k x_\ell)$$

$$d_k = \sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}( -\frac{x_\ell^2}{12}+\frac{x_\ell^4}{24} )\cos(\pi k x_\ell)$$

```python
sampling_x = (0.5 + np.arange(N)) / N
u_1 = (1 - sampling_x) ** 2 / 2
u_2 = 1 / 24 - sampling_x ** 2 / 6 + sampling_x ** 3 / 6 - sampling_x ** 4 / 24
u_3 = sampling_x ** 2 / 2
u_4 = -sampling_x ** 2 / 12 + sampling_x ** 4 / 24
plt.plot(u_1, label="u_1")
plt.plot(u_2, label="u_2")
plt.plot(u_3, label="u_3")
plt.plot(u_4, label="u_4")
plt.legend()
```

```python
dct = DCT(N)
```

```python
ak = -dct.dct(u_1)
ak
```

    array([-0.4695631 , -0.40264519, -0.09857256, -0.04209093, -0.02209709,
           -0.01255643, -0.00700534, -0.0031689 ])

```python
bk = dct.dct(u_2)
bk
```

    array([5.49963549e-02, 4.10626933e-02, 2.56510471e-03, 5.05282177e-04,
           1.58247367e-04, 6.27721528e-05, 2.74988517e-05, 1.08051832e-05])

```python
ck = dct.dct(u_3)
ck
```

    array([ 0.4695631 , -0.40264519,  0.09857256, -0.04209093,  0.02209709,
           -0.01255643,  0.00700534, -0.0031689 ])

```python
dk = dct.dct(u_4)
dk
```

    array([-5.49963549e-02,  4.10626933e-02, -2.56510471e-03,  5.05282177e-04,
           -1.58247367e-04,  6.27721528e-05, -2.74988517e-05,  1.08051832e-05])

```python
alpha = ak
alpha
```

    array([-0.4695631 , -0.40264519, -0.09857256, -0.04209093, -0.02209709,
           -0.01255643, -0.00700534, -0.0031689 ])

```python
beta = ck
beta
```

    array([ 0.4695631 , -0.40264519,  0.09857256, -0.04209093,  0.02209709,
           -0.01255643,  0.00700534, -0.0031689 ])

```python
Ak = (2 * ak - 16 * bk) / np.sqrt(N)
Ak
```

    array([-0.64313761, -0.51699881, -0.08421175, -0.03262109, -0.01652018,
           -0.00923383, -0.00510908, -0.00230187])

```python
Bk = (2 * ck - 16 * dk) / np.sqrt(N)
Bk
```

    array([ 0.64313761, -0.51699881,  0.08421175, -0.03262109,  0.01652018,
           -0.00923383,  0.00510908, -0.00230187])

```python
Ck = (2 * ak - 32 * bk) / np.sqrt(N)
Ck
```

    array([-0.95424398, -0.74928449, -0.09872217, -0.03547939, -0.01741536,
           -0.00958892, -0.00526463, -0.00236299])

```python
Dk = (2 * ck - 32 * dk) / np.sqrt(N)
Dk
```

    array([ 0.95424398, -0.74928449,  0.09872217, -0.03547939,  0.01741536,
           -0.00958892,  0.00526463, -0.00236299])

# DCTして残差を計算

$
V_k = F_k - U_k\\
V_k = F_k - A_k(F_0-F_0^L) -B_k(F_0^R-F_0)-C_k(F_1+F_1^L)-D_k(F_1^R+F_1)
$

```python
IMG = LENNA
```

```python
Fk = np.zeros(IMG.img.shape)
```

## DCT 縦方向

2次元入力信号を垂直方向の一次元信号が水平方向に並列に並んでいるものとみなし、各列において8画素単位の1次元DCTを適用する

```python
for row in range(IMG.img.shape[0] // 8):
    for col in range(IMG.img.shape[1]):
        eight_points = IMG.img[8 * row : 8 * (row + 1), col]
        c = dct.dct(eight_points)
        Fk[8 * row : 8 * (row + 1), col] = c
```

## 縦方向の残差

### 3階の導関数

```python
for row in range(Fk.shape[0] // 8):
    for col in range(Fk.shape[1]):
        F = Fk[8 * row : 8 * (row + 1), col]

        F_0_r = 0
        F_1_r = 0
        if row is not Fk.shape[0] // 8 - 1:
            F_0_r = Fk[8 * (row + 1), col]
            F_1_r = Fk[8 * (row + 1) + 1, col]

        F_0_l = 0
        F_1_l = 1
        if row is not 0:
            F_0_l = Fk[8 * (row - 1), col]
            F_1_l = Fk[8 * (row - 1) + 1, col]

        # 残差
        F_0 = F[0]
        F_1 = F[1]

        F = (
            F
            - Ak * (F_0 - F_0_l)
            - Bk * (F_0_r - F_0)
            - Ck * (F_1 + F_1_l)
            - Dk * (F_1_r + F_1)
        )
        # F_0, F_1は残す
        F[0] = F_0
        F[1] = F_1
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[8 * row : 8 * (row + 1), col] = F
```

### 1階の導関数

```python
for row in range(Fk.shape[0] // 8):
    for col in range(Fk.shape[1]):
        F = Fk[8 * row : 8 * (row + 1), col]

        F_0_r = 0
        if row is not Fk.shape[0] // 8 - 1:
            F_0_r = Fk[8 * (row + 1), col]

        F_0_l = 0
        if row is not 0:
            F_0_l = Fk[8 * (row - 1), col]

        # 残差
        F_0 = F[0]
        F = F - alpha * (F_0_r - F_0) / np.sqrt(N) - beta * (F_0 - F_0_l) / np.sqrt(N)
        # F_0は残す
        F[0] = F_0
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[8 * row : 8 * (row + 1), col] = F
```

## DCT 横方向

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 8):
        eight_points = Fk[row, 8 * col : 8 * (col + 1)]
        c = dct.dct(eight_points)
        Fk[row, 8 * col : 8 * (col + 1)] = c
```

## 横方向の残差

### 3階の導関数

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 8):
        F = Fk[row, 8 * col : 8 * (col + 1)]

        F_0_r = 0
        F_1_r = 0
        if col is not Fk.shape[1] // 8 - 1:
            F_0_r = Fk[row, 8 * (col + 1)]
            F_1_r = Fk[row, 8 * (col + 1) + 1]

        F_0_l = 0
        F_1_l = 0
        if col is not 0:
            F_0_l = Fk[row, 8 * (col - 1)]
            F_1_l = Fk[row, 8 * (col - 1) + 1]

        # 残差
        F_0 = F[0]
        F_1 = F[1]

        F = (
            F
            - Ak * (F_0 - F_0_l)
            - Bk * (F_0_r - F_0)
            - Ck * (F_1 + F_1_l)
            - Dk * (F_1_r + F_1)
        )
        # F_0は残す
        F[0] = F_0
        F[1] = F_1
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[row, 8 * col : 8 * (col + 1)] = F
```

### 1階の導関数

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 8):
        F = Fk[row, 8 * col : 8 * (col + 1)]

        F_0_r = 0
        if col is not Fk.shape[1] // 8 - 1:
            F_0_r = Fk[row, 8 * (col + 1)]

        F_0_l = 0
        if col is not 0:
            F_0_l = Fk[row, 8 * (col - 1)]

        # 残差
        F_0 = F[0]
        F = F - alpha * (F_0_r - F_0) / np.sqrt(N) - beta * (F_0 - F_0_l) / np.sqrt(N)
        # F_0は残す
        F[0] = F_0
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[row, 8 * col : 8 * (col + 1)] = F
```

# 低域3成分 (0,1)(1,0)(1,1)の絶対値の和

```python
low_3_value = 0
others_value = 0
```

```python
for row in range(Fk.shape[0] // 8):
    for col in range(Fk.shape[1] // 8):
        block = Fk[row * 8 : (row + 1) * 8, col * 8 : (col + 1) * 8]
        low_3_value += np.abs(block[0, 1]) + np.abs(block[1, 0]) + np.abs(block[1, 1])
        others_value += (
            np.sum(np.sum(np.abs(block)))
            - np.abs(block[0, 0])
            - np.abs(block[0, 1])
            - np.abs(block[1, 0])
            - np.abs(block[1, 1])
        )
```

```python
low_3_value
```

    319580.4518894131

```python
others_value
```

    1026933.5600949787

# 逆変換

$
F_k = F_k + U_k\\
F_k = V_k + A_k(F_0-F_0^L) +B_k(F_0^R-F_0)+C_k(F_1+F_1^L)+D_k(F_1^R+F_1)
$

```python
# recover = np.zeros(IMG.img.shape).astype("uint8")
recover = np.zeros(IMG.img.shape)
```

## 横方向の残差

### 1階の導関数

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 8):
        F = Fk[row, 8 * col : 8 * col + 8]

        F_0_r = 0
        if col is not Fk.shape[1] // 8 - 1:
            F_0_r = Fk[row, 8 * (col + 1)]

        F_0_l = 0
        if col is not 0:
            F_0_l = Fk[row, 8 * (col - 1)]

        # 残差
        F_0 = F[0]
        F = F + alpha * (F_0_r - F_0) / np.sqrt(N) + beta * (F_0 - F_0_l) / np.sqrt(N)
        # F_0は残す
        F[0] = F_0
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[row, 8 * col : 8 * col + 8] = F
```

### 3階の導関数

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 8):
        F = Fk[row, 8 * col : 8 * (col + 1)]

        F_0_r = 0
        F_1_r = 0
        if col is not Fk.shape[1] // 8 - 1:
            F_0_r = Fk[row, 8 * (col + 1)]
            F_1_r = Fk[row, 8 * (col + 1) + 1]

        F_0_l = 0
        F_1_l = 0
        if col is not 0:
            F_0_l = Fk[row, 8 * (col - 1)]
            F_1_l = Fk[row, 8 * (col - 1) + 1]

        # 残差
        F_0 = F[0]
        F_1 = F[1]

        F = (
            F
            + Ak * (F_0 - F_0_l)
            + Bk * (F_0_r - F_0)
            + Ck * (F_1 + F_1_l)
            + Dk * (F_1_r + F_1)
        )
        # F_0は残す
        F[0] = F_0
        F[1] = F_1
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[row, 8 * col : 8 * (col + 1)] = F
```

## IDCT 横方向

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 8):
        F = Fk[row, 8 * col : 8 * col + 8]
        data = dct.idct(F)
        # Fkに代入した後、縦方向に対して処理
        Fk[row, 8 * col : 8 * col + 8] = data
        # 復元画像
#         recover[row, 8 * col : 8 * col + 8] = data
```

## 縦方向

### 1階の導関数

```python
for row in range(Fk.shape[0] // 8):
    for col in range(Fk.shape[1]):
        F = Fk[8 * row : 8 * row + 8, col]

        F_0_r = 0
        if row is not Fk.shape[0] // 8 - 1:
            F_0_r = Fk[8 * (row + 1), col]

        F_0_l = 0
        if row is not 0:
            F_0_l = Fk[8 * (row - 1), col]

        # 残差
        F_0 = F[0]
        F = F + alpha * (F_0_r - F_0) / np.sqrt(N) + beta * (F_0 - F_0_l) / np.sqrt(N)
        # F_0は残す
        F[0] = F_0
        # F_0 F_1 F_2 F_3 F_4 F_5 F_6 F_7
        Fk[8 * row : 8 * row + 8, col] = F
```

### 3階の導関数

```python
for row in range(Fk.shape[0] // 8):
    for col in range(Fk.shape[1]):
        F = Fk[8 * row : 8 * (row + 1), col]

        F_0_r = 0
        F_1_r = 0
        if row is not Fk.shape[0] // 8 - 1:
            F_0_r = Fk[8 * (row + 1), col]
            F_1_r = Fk[8 * (row + 1) + 1, col]

        F_0_l = 0
        F_1_l = 1
        if row is not 0:
            F_0_l = Fk[8 * (row - 1), col]
            F_1_l = Fk[8 * (row - 1) + 1, col]

        # 残差
        F_0 = F[0]
        F_1 = F[1]

        F = (
            F
            + Ak * (F_0 - F_0_l)
            + Bk * (F_0_r - F_0)
            + Ck * (F_1 + F_1_l)
            + Dk * (F_1_r + F_1)
        )
        # F_0, F_1は残す
        F[0] = F_0
        F[1] = F_1
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[8 * row : 8 * (row + 1), col] = F
```

```python
for row in range(Fk.shape[0] // 8):
    for col in range(Fk.shape[1]):
        F = Fk[8 * row : 8 * (row + 1), col]
        data = dct.idct(F)
        # 復元画像
        recover[8 * row : 8 * (row + 1), col] = data
        # FKに代入した後、横方向に対して処理
#         Fk[8 * row : 8 * (row + 1), col] = data
```

```python
plt.imshow(np.round(recover), cmap="gray")
```

```python
plt.imshow(IMG.img, cmap="gray")
```

```python
recover[0, 0:10]
```

    array([162., 162., 162., 161., 162., 157., 163., 161., 165., 161.])

```python
LENNA.img[0, 0:10]
```

    array([162, 162, 162, 161, 162, 157, 163, 161, 165, 161], dtype=uint8)

```python
for i in range(LENNA.img.shape[0] // 8):
    for j in range(LENNA.img.shape[1]):
        array = LENNA.img[8 * i : 8 * i + 8, j]
        c = dct.dct(array)
        Fk[8 * i : 8 * i + 8, j] = c
```

ちゃんと復元できた

```python
Original = IMG.img.copy()
np.sum(np.sum(np.abs(Original - np.round(recover))))
```

    0.0

```python

```


# Glosten-Milgrom Model for security market

## Introduction

Security markets play a key factor in the modern economy. Securities are used as a means of saving and generating income, especially higher risk securities have become popular. The increased risk of securities likes stock leads to an interest in prediction of a market. Most markets however are hard to predict due to the difference in available information, which often can be hard to include in models.

Some models do however include this information disparity such as the Glosten-Milgrom, which models a so-called dealer market. In this project we will take a closer look at the Glosten-Milgrom and solve the model for a single security market. We will further extend the project with information shocks, which gives us an insight to the model’s ability to mimic a real market, where information changes daily.

```python
%matplotlib inline
#import the needed packages
import time
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
import sympy as sm
import pandas as pd

#from local file import plotterwindow
from modelproject.plotter import PlotterWindow
from modelproject.gm_simulation import gm_simulation

#use pretty print
sm.init_printing(use_unicode=True)
```

## Deriving the model

we create the equation symbols needed for sympy

```python
mu_t = sm.symbols("mu_t")
mu_t1 = sm.symbols("mu_t-1")
b_t = sm.symbols("b_t")
a_t = sm.symbols("a_t")
p_t = sm.symbols("P_t")
v = sm.symbols("v")
v_h = sm.symbols("v^H")
v_l = sm.symbols("v^L")
d_t = sm.symbols("d_t")
sd_t = sm.symbols("s(d_t)")
s_a = sm.symbols("s_t^a")
s_b = sm.symbols("s_t^b")
theta_t = sm.symbols("theta_t")
theta_t1 = sm.symbols("theta_t-1")
pi = sm.symbols("pi")
beta_b = sm.symbols("beta_B")
beta_s = sm.symbols("beta_S")
spib = sm.symbols("Pi_buy")
spis = sm.symbols("Pi_sell")
theta_p=sm.symbols("theta^+")
theta_m=sm.symbols("theta^-")
```

### Setting the model

There is a single market with 1 dealer. The dealer buys and sells a single security at the ask price $a_t$ and bid price $b_t$. In every time period t he takes a single order from a single trader. The order is denoted $d_t$ which takes the value 1 for buy order, -1 for a sale order and 0 for no order.

On the market there are two types of traders, Informed traders and liquidity traders. The informed traders have a high amount of information about the security and knows the true value of it. Further they seek to maximize their profit trading with the security, which leads to only buying if the security value higher than the ask price and only selling if the value is lower than the bid price.
Liquidity traders do not have information about the security or rather, they don’t care. These traders buys or sells the security independent on the value of the security. Among liquidity traders you find traders who either seeks to diversify their portfolio, thus buy, or who needs to liquify some assets. The liquidity trader buys with probability $\beta_B$ and sells with probability $\beta_S$.

$\pi$ share of the traders is informed traders.

On the market there is unequal information. The informed traders have private information about the value of the security. The dealer only has access to public information about the market, such as the ratio of informed traders. The dealer however receives information with each trade order. An informed trader would never sell if $a_t<v$, therefore an unequal amount of buy orders would indicate $v=v^H$.

### Model equations
Before we start on our model equations, we make two assumptions:
 i) The dealer is risk neutral and is in a competitive.
 ii) there is no trade cost on orders on the market.

To simplify our model, we set the security v to be binary distributed taking the value $v^H$ or $v^L$, the superscript denoting high and low value.

The dealer has the belief $\theta_t$, which is his estimate of the probability $p(v=v^h)=\theta$. From his belief he finds his expected value of security v denoted $\mu_t$.

```python
sm.Eq(mu_t,theta_t*v_h+(1-theta_t)*v_l)
```

The dealer sets his ask and bid price based on his expectation to v conditional on the information $\Omega_{t-1}$ and whether he expects a buy or sell order.
$$a_t=E(v\mid \Omega_{t-1} , d_t=1)$$
$$b_t=E(v\mid \Omega_{t-1} , d_t=-1)$$
Further, since the dealer is in a competitive market, he sets the ask and bid price according to an expected null profit. Since the dealer does not know the true value of the security, he loses on every trade made with an informed trader. The deficit is made up by trading with liquidity traders. Based on his expectation to the security and his knowledge of the market, he derives the following profit functions.

```python
sm.Eq(theta_t1*pi*(a_t-v_h)+beta_b*(1-pi)*(a_t-mu_t1), sm.symbols("Pi^buy_t"))
```

```python
sm.Eq((1-theta_t1)*pi*(v_l-b_t)+beta_s*(1-pi)*(mu_t1-b_t),sm.symbols("Pi^sell_t"))
```

From these profits he derives his optimal ask and bid price.

```python
sm.Eq(mu_t1+(pi*theta_t1*(1-theta_t1))/(pi*theta_t1+(1-pi)*beta_b)*(v_h-v_l), a_t)
```

```python
sm.Eq(mu_t1-(pi*theta_t1*(1-theta_t1))/(pi*(1-theta_t1)+(1-pi)*beta_s)*(v_h-v_l),b_t)
```

The ask and bid price equal the dealers expected value of the security with a mark-up and discount respectively. We denote the mark-up $s_t^a$ and the discount $s_t^b$.

```python
sm.Eq((pi*theta_t1*(1-theta_t1))/(pi*theta_t1+(1-pi)*beta_b)*(v_h-v_l), s_a)
sm.Eq(mu_t1+s_a, a_t)
```

```python
sm.Eq((pi*(1-theta_t1)*(1-theta_t1))/(pi*theta_t1+(1-pi)*beta_s)*(v_h-v_l), s_b)
sm.Eq(mu_t1-s_b, b_t)
```

Depending on the trade order the dealer receives in period t, his beliefs about the value of the security in period t+1 is changed.
$$\theta_t^{+}\equiv P(v=v^H|\Omega_{t-1},d_t=1)$$
$$\theta_t^{-}\equiv P(v=v^L|\Omega_{t-1},d_t=-1)$$

```python
sm.Eq(((1+pi)*beta_b)/(pi*theta_t1+(1-pi)*beta_b),theta_p)
```

```python
sm.Eq(((1-pi)*beta_s)/(pi*(1-theta_t1)+(1-pi)*beta_s),theta_m)
```

With his updated beliefs about the security, the dealer updates his expectation of the value of the security for the period t+1. If the dealer recieved a buy order in in period t, his expectation $\mu$ can be shown to have the following relation:
$$\mu^+_{t+1}-\mu_t=s_t^a$$
The dealer’s expectation for any order is therefore:
$$\mu_t=\mu_{t-1}+s(d_t)d_t$$
with $s(d_t)\equiv \begin{cases} s_t^a & d_t=1 \\ s_t^b & d_t=-1 \end{cases}$

## Simulating the model

Solving the multiperiod Glosten-Milgrom model is unfortunately not straight forward. The equations for the ask and bid price derived in the section Model Equations describes an equilibrium in which the informed trader maximizes his profit and the dealer ensures null profit. As seen the prices are dependent on the dealer’s beliefs, which are updated after each order. Due to the dynamic beliefs of the dealer, we chose to simulate the model in order to get estimates for the multiperiod equilibrium prices.

In order to simulate the Glosten-Milgrom model, we need some kind of indicator of an equilibrium. The dealer set his prices based on his information about the market and his expectation of the value of the security. In the first period the information used to set the ask and bid price is limited, which results in a rather large gap between the ask and bid price. The gap can be said to represent the dealer’s uncertainty about the security. The dealer he receives information about the security's value with each order placed and based on this information he is better able to predict the value. As his uncertainty about the security is diminished, as is the price gap between ask and bid prices. A sufficient low-price gap can therefore be used to indicate a multiperiod equilibrium.

Since our equilibrium indicator is based on the price gap, which in equilibrium converges on 0, we instead set a threshold value denoted $\epsilon$, at which we assume the gap to have converged to its equilibrium value. Based on our equilibrium indicator we use the following algorithm to simulate the Glosten-Milgrom model.

**How to simulate the Glosten-Milgrom model**
1. choose start values and parameters. Set threshold value $\epsilon$
2. calculate value expectation, then ask/bid price.
3. determine trader type, then order type
4. update dealer beliefs
5. repeat step 2-4 until $\epsilon > spread = a_t - b_t$

In the first step we set up all information in the model, including dealer's beliefs, ratio of informed traders, chance of uninformed placing a buy/sell order etc. We then use this information to calculate the equilibrium prices in the first period. We then determine the trader type and type of order the trader places. With this info we update the dealer's beliefs for the next period. We then calculate the next period same as the first and repeat until we reach an acceptable spread, which is lower than our threshold value.

To further dissect the algorithm we constructed a flowchart, which step by step shows how we simulate a simple Glosten-Milgrom model. The method described in the chart is the method used to construct the function gm_simulation. To ease reading the flowchart, we'll include a quick list of the variables in the model:

1. pi = $\pi$ : ratio of informermed traders on the market
2. v : the security traded on the market. taking value $v^H or v^L$
3. $\theta_t$ : dealer's beliefs about v
4. $\mu$ : dealer's expected value of v
5. $a_t$ : ask price
6. $b_t$ : bid price
7. spread = $a_t - b_t$
8. $d_t$ : order type. takes value 1 for buy, -1 for sale, 0 for no order
9. $\epsilon$ : threshold value

Based on the algorithm we made the function gm_simulation in the file gm_simulation.py in the module modelproject. With the function we are able to fully customize the Glosten-Milgrom model described in this project. Besides simulating the model, we have expanded the function to also being able to simulate an information shock on the market, which we will make use of in a later section. To get an overview of the function, we have called the docstring below.

```python
help(gm_simulation)
```

    Help on function gm_simulation in module modelproject.gm_simulation:

    gm_simulation(distribution=(0, 1), decision='v_h', ratio=0.2, uninformed=0.5, startvalue=0.5, iterations=500, seed=5000, epsilon=1e-05, shockperiod=None, shock={})
        Simulates a simple Glosten-Milgrom model with binary distributed security.
        Repeats simulation until either the threshold parameter is reached or the maximum
        number of simulations is reached.

        Args:
            distribution (tuple): upper and lower value for the security. Default (0,1)
            decision (string): selecting the true value of the security. Default "v_h". options = ("v_h", "v_l")

            ratio (float): Ratio of informed traders on the market. Default 0.2
            uninformed (float): Chance to receieve buy order from uninformed trader. Default 0.5
            startvalue (float): Dealer's start belief about the value of the security. Default 0.5

            iterations (int): Maximum number of iterations run by the simulation. Default 500
            seed (int): Seed used to generate random numbers. Default 5000
            epsilon (float): Threshold parameter. Default 10**-5

            shockperiod (int): Selects which iteration the shock is introduced. Default None
            shock (dict): Type of shock introduced. Default {}

        Returns:
            dataframe (pandas dataframe): dataframe containing the simulation data for all iterations
            values (dictionary): dictionary containing parameter values from the final iteration.

The gm_simulation function depends on several random distributions when simulating the model. Our results are therefore heavily influenced by the draw. To properly demonstrate this feature we run the gm_simulation function 5 times with different seeds and plot the development in the dealer's beliefs $\theta_t$.

```python
#calling gm_simulation function with different seeds.
thetarun1 = gm_simulation(ratio=0.15, iterations=201,seed=4795)
thetarun2 = gm_simulation(ratio=0.15, iterations=201,seed=6279)
thetarun3 = gm_simulation(ratio=0.15, iterations=201,seed=6130)
thetarun4 = gm_simulation(ratio=0.15, iterations=201,seed=9352)
thetarun5 = gm_simulation(ratio=0.15, iterations=201,seed=5059)

#saving the returned data
thetadata1, thetavalues1 = thetarun1
thetadata2, thetavalues2 = thetarun2
thetadata3, thetavalues3 = thetarun3
thetadata4, thetavalues4 = thetarun4
thetadata5, thetavalues5 = thetarun5

#plotting the theta data from the simulations
fig = plt.figure(dpi=100)
ax2 = fig.add_subplot(1,1,1)
ax2.plot(thetadata1["theta"], label="1")
ax2.plot(thetadata2["theta"], label="2")
ax2.plot(thetadata3["theta"], label="3")
ax2.plot(thetadata4["theta"], label="4")
ax2.plot(thetadata5["theta"], label="5")
ax2.grid(True)
ax2.set_xlabel("Iterations")
ax2.set_ylabel("Theta Value")
ax2.legend()
ax2.set_title("Theta")
```

As we can see in the graph above, all the simulations converge towards the same long-term equilibrium. The speed of convergence however is vastly different for the different seeds. This inconsistency in convergence could lead to misleading results for the equilibrium values, especially for lower values of $\pi$

In order to get more credible results for the equilibrium values, we create the function numericalsolution, which loops the gm_simulation function N times with random seeds and output the mean values for the N different equilibria. For a sufficiently large N the results converge to an asymptotic distribution, which yields more credible results for the equilibrium values.

```python
def numericalsolution(N=1000, ratio=0.15):
    """Runs the gm_simulation N number of times with different seeds.

    Args:
        N (int): number of runs. Default 1000
        ratio (float): ratio of informed traders used in gm_simulation. Default 0.15.

    Returns:
        Meanvalues (dict): mean of the values dictionaries returned by gm_simulation
    """
    data = pd.DataFrame()

    for i in range(N):
        seed = int(np.random.uniform(0,10000))
        dataframe, values = gm_simulation(distribution=(0,10), ratio=ratio, startvalue=0.5,
                                          iterations = 1001, seed=seed, epsilon=5**-5)

        for key in values:
            data.loc[i,key]=values[key]

    meanvalues = {}
    for key in list(data.columns):
        meanvalues[key]=data[key].mean()
    return meanvalues
```

Below we call the function with default values. It should be noted the function is rather computation heavy as the gm_simulation with $\pi=0.15$ runs an average 216 iterations, and the numerical solution runs the gm_simulation 1000 times.

```python
%time numericalsolution()
```

    Wall time: 8.85 s

    {'Theta': 0.9999543796226845,
     'Bid': 9.99937042512405,
     'Ask': 9.999656044732731,
     'Mu': 9.999534654405569,
     'Equilibrium period': 217.62}

With a ratio 0.15 we reach equilibrium after 216 iterations on average. Note results may vary a bit if running the function. As expected, $\theta_t$ converges towards 1, while ask price, bid price and expected value $\mu$ converges towards the true value of the security.

In order to examine the influence of the ratio of traders, we construct a for loop to run numericalsolution for every $\pi$ value in the range $[0.01;1]$.

The code below is extremely slow due to the amount of computations needed to perform the task. The first couple of $\pi$ values do not converge to an equilibrium within 1000 iterations. The first 5 values require a total of 5 million iterations of the gm_simulation loop. Due to the high computation requirement we added a faulty line of code to prevent you from accidently running the code. If you wish to run the code, comment out the line: import PreventYouFromRunning.

We have run the code and saved the data from that run in the file doomloopdata.csv. The code below is included to show how we got the results in the file. We do not advise you to run the code.

```python
#To prevent you from accidently running it we have include the line below.
import PreventYouFromRunning

#make lists
ratiolist = []
periodlist = []

#loop the function numericalsolution
for i in range(99):
    num = ((i+1)/100)
    values = numericalsolution(ratio=num)

    ratiolist.append(num)
    periodlist.append(values["Equilibrium period"])

```

With the data from the doomloopdata.csv we create a plot which shows the correlation between the ratio of informed traders $\pi$ and the equilibrium period. We have included commented out code to create the plot with data from the loop above, if you ran it. Else the graph is created from the doomloopdata.csv file.

```python
#if you ran the above code, uncomment the three lines below to graph the data
#plt.plot(ratiolist,periodlist)
#plt.xlabel("ratio")
#plt.ylabel("Period")

#alternative:
#read data file
doomloopdata = pd.read_csv("./doomloopdata.csv")
doomloopdata.columns = ["periodlist","ratiolist"]

#plot the data
doomloopdata.plot("ratiolist","periodlist")
plt.xlabel("ratio")
plt.ylabel("Period")
```

The speed of convergence towards the equilibrium is heavily determined by the ratio of informed traders. For higher values of $\pi$ the amount of information in the trade flow is higher. The dealer therefore received less noise about the security's true value, which results in a faster convergence towards the equilibrium prices.

### Initial expectation

In this section we examine the effect of the dealer's initial belief.

We call the gm_simulation for different values of $\pi$ and plot bid price, ask price and expected value $\mu_t$ in our custom interactive graphwindow class. In the graph window you are able move around in the plot and examine the development of the model for different $\pi$ values.

```python
#call gm_simulation function 5 times with different ratios
startrun1 = gm_simulation(startvalue = 0.10, ratio=0.13, iterations=1001, seed=404)
startrun2 = gm_simulation(startvalue = 0.15, ratio=0.13, iterations=1001, seed=404)
startrun3 = gm_simulation(startvalue = 0.25, ratio=0.13, iterations=1001, seed=404)
startrun4 = gm_simulation(startvalue = 0.5, ratio=0.13, iterations=1001, seed=404)
startrun5 = gm_simulation(startvalue = 0.8, ratio=0.13, iterations=1001, seed=404)

#save data
startrundata1, startrunvalues1 = startrun1
startrundata2, startrunvalues2 = startrun2
startrundata3, startrunvalues3 = startrun3
startrundata4, startrunvalues4 = startrun4
startrundata5, startrunvalues5 = startrun5

#merge data
startrunmerged = pd.concat([startrundata1, startrundata2, startrundata3, startrundata4, startrundata5])
```

By plotting the ask price $a_t$, bid price $b_t$ and the expected value of the security $\mu$ the path to equilibrium is clearly shown. Convergence in the bid-ask spread, which is used as our equilibrium indicator, can clearly be seen from such a plot.

In order to give an optimal view of our simulation results we make use of a custom module plotter, which renders an interactive graph window. The plotter module used in this project is a repurposed version of the module we developed as a part of our dataproject. One of the major features of the PlotterWindow is the toolbar, which allows you to zoom in/out and move around the graph. We encourage you to make use of this feature to get a better picture of the data.

It should be noted on some python installations the graph window does not take screen priority over the notebook and will hide behind it. A simple alt-tab or command-tab should bring it to the front.

```python
#call custom graphwindow class with graphtype=piplot
startrungraphwindow = PlotterWindow(data = startrunmerged, slicename = "startvalue", xvariable = "Iteration",
                                    yvariablelist = ["mu","ask","bid"], graphtype ="piplot")
startrungraphwindow.start()
```

As seen in the PlotterWindow the convergence for our startrun simulations is rather rapid at first with all getting close to the value 1. There is however some instability and divergence for most graphs, especially around iterations 450-500. The graphs with a lower start value show a more volatile behavior and a slower convergence on the equilibrium. The higher values, which for these simulations are better start predictions, shows less volatility and faster convergence. The better information about the security the dealer has access to in the first period, the faster convergence does the market have towards optimal prices.

Below we perform the simulation again though with the difference of the security taking the low value $v^L$ as to properly show the conclusion on the startvalues influence.

In order to better show the bid-ask spread, we make use of a different graph type from the PlotterWindow class. We use the PlotterWindow to make a candlestick graph. The vertical bars in the candlestick graphs represent the ask-bid spread.

```python
#call gm_simulation function 5 times with different ratios
startlowrun1 = gm_simulation(decision = "v_l", startvalue = 0.10, ratio=0.13, iterations=1001, seed=2009)
startlowrun2 = gm_simulation(decision = "v_l", startvalue = 0.15, ratio=0.13, iterations=1001, seed=2009)
startlowrun3 = gm_simulation(decision = "v_l", startvalue = 0.25, ratio=0.13, iterations=1001, seed=2009)
startlowrun4 = gm_simulation(decision = "v_l", startvalue = 0.5, ratio=0.13, iterations=1001, seed=2009)
startlowrun5 = gm_simulation(decision = "v_l", startvalue = 0.8, ratio=0.13, iterations=1001, seed=2009)

#save data
startlowrundata1, startlowrunvalues1 = startlowrun1
startlowrundata2, startlowrunvalues2 = startlowrun2
startlowrundata3, startlowrunvalues3 = startlowrun3
startlowrundata4, startlowrunvalues4 = startlowrun4
startlowrundata5, startlowrunvalues5 = startlowrun5

#merge data
startlowrunmerged = pd.concat([startlowrundata1, startlowrundata2, startlowrundata3, startlowrundata4, startlowrundata5])

#call custom graphwindow class with graphtype=candlestick
startlowrungraphwindow2 = PlotterWindow(data = startlowrunmerged, slicename = "startvalue", graphtype = "candlestick")
startlowrungraphwindow2.start()
```

With the low value $v^L$ we have faster convergence in general towards equilibrium compared to the high value $v^H$ simulations. The conclusions about the initial prediction is however the same. The better informed the dealer is in the first period the faster convergence on the optimal prices.

## Shocks to the model

To further examine the model we make use of the built-in shock options in the gm_simulation function. We introduce several types of shocks to the market and graph the results with the PlotterWindow. In the interest of getting credible results we would have preferred to use a slightly modified version of the numericalsolution function. We however chose to use the less reliable gm_simulation instead due to the lower amount of computational power needed.

We simulate our model with three different types of shocks.
1. The first shock is a shock in private information. We change the realisation of the variable v from $v^H$ to $v^L$. This information is only available to the informed traders, who know the true value of the security. A real-life example of this type of shock would be a trader receiving insider info from a firm.
2. The second shock is a public information shock. We change the upper and lower value for v from (0,1) to (0.3, 0.8). This information is available to all participants on the market. A real-life example of this type of shock would be the release of the annual report from a firm
3. We apply both types of shock to the model..

all of the shocks incur in the 90th period. To avoid naming conflict in our graphwindow we set a slightly different ratio $\pi$ for the different simulation. The difference on the results have been tested and found to be negligible.

```python
#call gm_simulation function with different shock types
shockrun1 = gm_simulation(ratio=0.15001, iterations=1001, seed=404, shockperiod = 90, shock={"Private":0})
shockrun2 = gm_simulation(ratio=0.15002, iterations=1001, seed=404, shockperiod = 90, shock={"Public":(0.3, 0.8)})
shockrun3 = gm_simulation(ratio=0.15003, iterations=1001, seed=404, shockperiod = 90, shock={"Private":0, "Public":(0.3,0.8)})

#save results
shockdata1, shockvalues1 = shockrun1
shockdata2, shockvalues2 = shockrun2
shockdata3, shockvalues3 = shockrun3

#merge results
shockmerged = pd.concat([shockdata1,shockdata2,shockdata3])

#call custom graphwindow class with graphtype = piplot
shockgraphwindow = PlotterWindow(data = shockmerged, slicename = "ratio", xvariable = "Iteration", yvariablelist = ["mu","ask","bid"], graphtype="piplot")
shockgraphwindow.start()
```

From the graphs we find the following
1. Private information: shock After the shock the model slowly converges towards the new equilibrium, which is at $a_t, b_t, \mu_t \rightarrow v^L$. The dealer slowly receives information about the new equilibrium from the informed traders’ behaviour. He adjusts his ask and bid price to make up for the new informed trader strategy.
2. Public information: The change in the prices happens instantly when the new information is made available. This behaviour on the market suggests our model is semi-strong form efficient, since the market instantly adepts to the new public information.
3. Both: With both shock types we see the instant response to the public information. The reaction to the private information shock is rather slow, as with the first graph.

The Glosten-Milgrom model is capable of adapting to the different information shocks and converges on the equilibrium for all simulations.

# Conclusion

In this paper we found the Gloston-milgrom model capable of adapting to public and private information. The model simulated the behaviour of several types of traders and a market maker (the dealer) and showed their adaptions to the information available. The model further shows this adaptation of information by the equilibrium prices converging on the true value of the security

The ratio of informed traders $\pi$ on the market have a huge impact on the speed of convergence and spread of ask and bid prices.

From the shocks we introduce to the model we conclude the model is semi-strong form efficient.