# Kalman Filter

```python
import sys
# Add the symgp folder path to the sys.path list
module_path = r'/Users/jaduol/Documents/Uni (original)/Part II/IIB/MEng Project/'
if module_path not in sys.path:
    sys.path.append(module_path)

from symgp import SuperMatSymbol, utils, MVG, Variable, SuperDiagMat, Kernel, Covariance, Constant, Mean
from sympy import symbols, ZeroMatrix, Identity
from IPython.display import display, Math, Latex, clear_output
```

```python
# Shapes
m, n, t = symbols('m n t')
```

## 1. Variables

```python
# Variables
x_prev, x_t, y_prev, y_t, b_t, d_t, w_t, e_t = utils.variables('x_{t-1} x_t y_{1:t-1} y_t b_t d_t w_t e_t',
                                                                [m, m, (n,t-1), n, m, n, m, n])
```

## 2. Constant Parameters

```python
# Constant parameters
A, C = utils.constants('A C',[(m,m), (n,m)])
Q = Covariance(w_t, name='Q')
R = Covariance(e_t, name='R')
```

## 3. p(x_(t-1)|y_(1:t-1))

```python
# p(x_(t-1)|y_(1:t-1))
f_prev = Mean(x_prev, name='m_{t-1|t-1}')
F_prev = Covariance(x_prev, name='P_{t-1|t-1}')
p_xprev = MVG([x_prev], mean=f_prev, cov=F_prev, cond_vars=[y_prev])

print("p_xprev:")
display(Latex(utils.matLatex(p_xprev)))
```

    p_xprev:

\begin{align*}
p\left(\mathbf{x_{t-1}}|\mathbf{y_{1:t-1}}\right)&= \mathcal{N}\left(\mathbf{x_{t-1}};\mathbf{m}_{\mathbf{x_{t-1}}|\mathbf{y_{1:t-1}}},\mathbf{\Sigma}_{\mathbf{x_{t-1}}|\mathbf{y_{1:t-1}}}\right)\\
\mathbf{m}_{\mathbf{x_{t-1}}|\mathbf{y_{1:t-1}}} &= \mathbf{m_{t-1|t-1}}\\
\mathbf{\Sigma}_{\mathbf{x_{t-1}}|\mathbf{y_{1:t-1}}} &= \mathbf{P_{t-1|t-1}}\\
\end{align*}

```python
print(p_xprev.covar.expanded)
```

    P_{t-1|t-1}

## 4. p(x_t|x_(t-1))

```python
p_xt = MVG([x_t], mean=A*x_prev, cov=Q, cond_vars=[x_prev])

print("p_xt:")
display(Latex(utils.matLatex(p_xt)))
```

    p_xt:

\begin{align*}
p\left(\mathbf{x_t}|\mathbf{x_{t-1}}\right)&= \mathcal{N}\left(\mathbf{x_t};\mathbf{m}_{\mathbf{x_t}|\mathbf{x_{t-1}}},\mathbf{\Sigma}_{\mathbf{x_t}|\mathbf{x_{t-1}}}\right)\\
\mathbf{m}_{\mathbf{x_t}|\mathbf{x_{t-1}}} &= \mathbf{A} \mathbf{x_{t-1}}\\
\mathbf{\Sigma}_{\mathbf{x_t}|\mathbf{x_{t-1}}} &= \mathbf{Q}\\
\end{align*}

```python
print(Covariance._used_names)
```

    ['x_{t-1}', 'x_t', 'y_{1:t-1}', 'y_t', 'b_t', 'd_t', 'w_t', 'e_t', 'A', 'C', 'Q', 'R', 'm_{t-1|t-1}', 'P_{t-1|t-1}', 'm_{x_{t-1}|y_{1:t-1}}', 'S_{x_{t-1},x_{t-1}|y_{1:t-1}}', 'm_{x_t|x_{t-1}}', 'S_{x_t,x_t|x_{t-1}}']

```python
print(p_xprev.covar)
```

    S_{x_{t-1},x_{t-1}|y_{1:t-1}}

## 5. p(x_t,x_(t-1)|y_(1:t-1))

```python
p_xt_xprev = p_xt*p_xprev

print("p_xt_xprev:")
display(Latex(utils.matLatex(p_xt_xprev)))
```

    p_xt_xprev:

\begin{align*}
p\left(\mathbf{x_t},\mathbf{x_{t-1}}|\mathbf{y_{1:t-1}}\right)&= \mathcal{N}\left(\left[\begin{smallmatrix}\mathbf{x_t}\\\mathbf{x_{t-1}}\end{smallmatrix}\right];\mathbf{m}_{\mathbf{x_t},\mathbf{x_{t-1}}|\mathbf{y_{1:t-1}}},\mathbf{\Sigma}_{\mathbf{x_t},\mathbf{x_{t-1}}|\mathbf{y_{1:t-1}}}\right)\\
\mathbf{m}_{\mathbf{x_t},\mathbf{x_{t-1}}|\mathbf{y_{1:t-1}}} &= \left[\begin{smallmatrix}\mathbf{A} \mathbf{m_{t-1|t-1}}\\\mathbf{m_{t-1|t-1}}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{x_t},\mathbf{x_{t-1}}|\mathbf{y_{1:t-1}}} &= \left[\begin{smallmatrix}\mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T&\mathbf{A} \mathbf{P_{t-1|t-1}}\\\mathbf{P_{t-1|t-1}} \mathbf{A}^T&\mathbf{P_{t-1|t-1}}\end{smallmatrix}\right]\\
\end{align*}

## 6. p(x_t|y_(1:t-1))

```python
p_xt_predict = p_xt_xprev.marginalise([x_prev])

print("p_xt_predict:")
display(Latex(utils.matLatex(p_xt_predict)))
```

    p_xt_predict:

\begin{align*}
p\left(\mathbf{x_t}|\mathbf{y_{1:t-1}}\right)&= \mathcal{N}\left(\mathbf{x_t};\mathbf{m}_{\mathbf{x_t}|\mathbf{y_{1:t-1}}},\mathbf{\Sigma}_{\mathbf{x_t}|\mathbf{y_{1:t-1}}}\right)\\
\mathbf{m}_{\mathbf{x_t}|\mathbf{y_{1:t-1}}} &= \mathbf{A} \mathbf{m_{t-1|t-1}}\\
\mathbf{\Sigma}_{\mathbf{x_t}|\mathbf{y_{1:t-1}}} &= \mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T\\
\end{align*}

## 7. p(y_t|x_t)

```python
p_yt = MVG([y_t], mean=C*x_t, cov=R, cond_vars=[x_t])

print("p_yt:")
display(Latex(utils.matLatex(p_yt)))
```

    p_yt:

\begin{align*}
p\left(\mathbf{y_t}|\mathbf{x_t}\right)&= \mathcal{N}\left(\mathbf{y_t};\mathbf{m}_{\mathbf{y_t}|\mathbf{x_t}},\mathbf{\Sigma}_{\mathbf{y_t}|\mathbf{x_t}}\right)\\
\mathbf{m}_{\mathbf{y_t}|\mathbf{x_t}} &= \mathbf{C} \mathbf{x_t}\\
\mathbf{\Sigma}_{\mathbf{y_t}|\mathbf{x_t}} &= \mathbf{R}\\
\end{align*}

## 8. p(y_t ,x_t|y_(1:t-1))

```python
p_yt_xt = p_yt*p_xt_predict

print("p_yt_xt:")
display(Latex(utils.matLatex(p_yt_xt)))
```

    p_yt_xt:

\begin{align*}
p\left(\mathbf{y_t},\mathbf{x_t}|\mathbf{y_{1:t-1}}\right)&= \mathcal{N}\left(\left[\begin{smallmatrix}\mathbf{y_t}\\\mathbf{x_t}\end{smallmatrix}\right];\mathbf{m}_{\mathbf{y_t},\mathbf{x_t}|\mathbf{y_{1:t-1}}},\mathbf{\Sigma}_{\mathbf{y_t},\mathbf{x_t}|\mathbf{y_{1:t-1}}}\right)\\
\mathbf{m}_{\mathbf{y_t},\mathbf{x_t}|\mathbf{y_{1:t-1}}} &= \left[\begin{smallmatrix}\mathbf{C} \mathbf{A} \mathbf{m_{t-1|t-1}}\\\mathbf{A} \mathbf{m_{t-1|t-1}}\end{smallmatrix}\right]\\
\mathbf{\Sigma}_{\mathbf{y_t},\mathbf{x_t}|\mathbf{y_{1:t-1}}} &= \left[\begin{smallmatrix}\mathbf{R} + \mathbf{C} \left(\mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T\right) \mathbf{C}^T&\mathbf{C} \left(\mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T\right)\\\left(\mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T\right) \mathbf{C}^T&\mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T\end{smallmatrix}\right]\\
\end{align*}

## 9. p(x_t|y_(1:t))

```python
p_xt_update = p_yt_xt.condition([y_t])

print("p_xt_update:")
display(Latex(utils.matLatex(p_xt_update)))
```

    p_xt_update:

\begin{align*}
p\left(\mathbf{x_t}|\mathbf{y_{1:t-1}},\mathbf{y_t}\right)&= \mathcal{N}\left(\mathbf{x_t};\mathbf{m}_{\mathbf{x_t}|\mathbf{y_{1:t-1}},\mathbf{y_t}},\mathbf{\Sigma}_{\mathbf{x_t}|\mathbf{y_{1:t-1}},\mathbf{y_t}}\right)\\
\mathbf{m}_{\mathbf{x_t}|\mathbf{y_{1:t-1}},\mathbf{y_t}} &= \mathbf{A} \mathbf{m_{t-1|t-1}} + \left(\mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T\right) \mathbf{C}^T \left(\mathbf{R} + \mathbf{C} \left(\mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T\right) \mathbf{C}^T\right)^{-1} \left(\mathbf{y_t} - \mathbf{C} \mathbf{A} \mathbf{m_{t-1|t-1}}\right)\\
\mathbf{\Sigma}_{\mathbf{x_t}|\mathbf{y_{1:t-1}},\mathbf{y_t}} &= \mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T - \left(\mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T\right) \mathbf{C}^T \left(\mathbf{R} + \mathbf{C} \left(\mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T\right) \mathbf{C}^T\right)^{-1} \mathbf{C} \left(\mathbf{Q} + \mathbf{A} \mathbf{P_{t-1|t-1}} \mathbf{A}^T\right)\\
\end{align*}

## Numerical Evaluation

```python
from sympy import Matrix, Identity
import numpy as np

d = {'A': np.array([[1, 0, 1, 0],[0, 1, 0, 1],[0, 0, 1, 0],[0, 0, 0, 1]],dtype=np.float32),
     'C': np.array([[1, 0, 0, 0],[0, 1, 0, 0]], dtype=np.float32),
     'Q': 0.001*np.eye(4),#Matrix(Identity(4)),
     'R': np.eye(2),#Matrix(Identity(2)),
     'm_{t-1|t-1}': np.array([[8],[10],[1],[0]],dtype=np.float32),
     'P_{t-1|t-1}': np.eye(4),#Matrix(Identity(4)),
     'b_t': np.array([[0],[0],[0],[0]],dtype=np.float32),
     'd_t': np.array([[0],[0]],dtype=np.float32)}

```

```python
def ldsSample(A, C, Q, R, initmu, T):
    """
        Simulates a run of a linear dynamical system
    """

    A, C = np.array(A.tolist(), dtype=np.float32), np.array(C.tolist(), dtype=np.float32)
    Q, R = np.array(Q.tolist(), dtype=np.float32), np.array(R.tolist(), dtype=np.float32)
    initmu = np.array(initmu.tolist(), dtype=np.float32).reshape((-1,))

    ss = A.shape[0]
    os = C.shape[0]

    x = np.zeros((ss,T))
    y = np.zeros((os,T))

    x[:,0] = np.dot(A,initmu) + np.random.multivariate_normal(np.zeros(ss),Q)
    y[:,0] = np.dot(C,x[:,0]) + np.random.multivariate_normal(np.zeros(os),R)

    for t in range(1,T):
        x[:,t] = np.dot(A,x[:,t-1]) + np.random.multivariate_normal(np.zeros(ss),Q)
        y[:,t] = np.dot(C,x[:,t-1]) + np.random.multivariate_normal(np.zeros(os),R)

    return x,y

```

```python
T = 15
x, y = ldsSample(d['A'], d['C'], d['Q'], d['R'], d['m_{t-1|t-1}'], T)

d['y_t'] = Matrix(y[:,0].reshape((-1,1)))
```

```python
def kalmanFilter(y, A, C, Q, R, init_m, init_V, debug=False):

    A, C = np.array(A.tolist(), dtype=np.float32), np.array(C.tolist(), dtype=np.float32)
    Q, R = np.array(Q.tolist(), dtype=np.float32), np.array(R.tolist(), dtype=np.float32)
    init_m = np.array(init_m.tolist(), dtype=np.float32).reshape((-1,))
    init_V = np.array(init_V.tolist(), dtype=np.float32)

    os, T = y.shape   #Observations shape
    ss = A.shape[0]   #State space size

    m = np.zeros((ss, T))
    mpred = np.zeros((ss, T))
    V = np.zeros((ss, ss, T))
    Vpred = np.zeros((ss, ss, T))

    for t in range(T):
        if t==0:
            d['m_{t-1|t-1}'] = init_m
            d['P_{t-1|t-1}'] = init_V
        else:
            d['m_{t-1|t-1}'] = m[:,t-1]
            d['P_{t-1|t-1}'] = V[:,:,t-1]

        d['y_t'] = Matrix(y[:,t].reshape((-1,1)))

        if debug:
            print("t: ", t)
            print("d['m_{t-1|t-1}']: ",d['m_{t-1|t-1}'])
            print("d['P_{t-1|t-1}']: ",d['P_{t-1|t-1}'])
            print("d['y_t']: ", d['y_t'])

        m[:,t] =  utils.replace_with_num(p_xt_update.mean.to_full_expr(), d, debug)
        V[:,:,t] = utils.replace_with_num(p_xt_update.covar.to_full_expr(), d, debug)
        mpred[:,t] = utils.replace_with_num(p_xt_predict.mean.to_full_expr(), d, debug)
        Vpred[:,:,t] = utils.replace_with_num(p_xt_predict.covar.to_full_expr(), d, debug)

    return m, V, mpred, Vpred
```

```python
mfilt, Vfilt, mpred, Vpred = kalmanFilter(y, d['A'], d['C'], d['Q'], d['R'], d['m_{t-1|t-1}'], d['P_{t-1|t-1}'])
```

```python
#Configure the matplotlib backend as plotting inline
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.patches as patches

#Enables latex
from matplotlib import rc
rc('font',**{'family':'sans-serif','sans-serif':['Helvetica'],'size':'20'})
## for Palatino and other serif fonts use:
#rc('font',**{'family':'serif','serif':['Palatino']})
rc('text', usetex=True)

def plotGaussianEllipse(ax, m, V, ellipsecolor, markercolor,label=None,t=None):
    """
        Plots an ellipse representing the covariance matrix of a Gaussian.

        Args:
            ax - The Axes object on which to plot the ellipses
            m - The centre of the ellipse
            V - Covariance matrix
            ellipsecolor - Ellipse color border colour
            markercolor - Centre marker colour
          (Optional)
            label - label for the plot containing this point
            t - point at which to add new data
    """

    s = 4.605  #90% confidence interval
    eigVals, eigVecs = np.linalg.eig(V)

    #Sort eigvalues to make sure that large eigval is first
    idx = eigVals.argsort()[::-1]
    eigVals = eigVals[idx]
    eigVecs = eigVecs[:,idx]

    #Add ellipse to axes
    border = patches.Arc(xy=m,width = 2*(s*eigVals[0])**0.5,height = 2*(s*eigVals[1])**0.5,
                        angle=np.degrees(np.arctan2(eigVecs[1,0],eigVecs[0,0])), \
                         linewidth=0.5, linestyle='--',color=ellipsecolor,fill=False, zorder=2)
    ax.add_patch(border)

    #Either plot points that are joined by a line or plot them individually
    if t != None:
        data = ax.lines[-1].get_data()
        if t >= len(data[0]):
            new_xdata = np.append(data[0],m[0])
            new_ydata = np.append(data[1],m[1])
            ax.lines[-1].set_data((new_xdata, new_ydata))
        else:
            data[0][t] = m[0]
            data[1][t] = m[1]
            ax.lines[-1].set_data((data[0],data[1]))
    else:
        ax.plot(m[0],m[1],'x-',ms=10,mew=2,c=markercolor,label=label)

def plotLine(ax, a, b, color):
    """
        Plots a line between two points
    """
    ax.plot([a[0], b[0]],[a[1], b[1]], color=color,linestyle='solid')
```

```python
#Set up figure
fig = plt.figure()
fig.set_size_inches(20,7)
ax = fig.add_subplot(111)
plt.axis('equal')

#Plot observations and true data
ax.set_xlim(left=5,right=25)
ax.set_ylim(bottom=5,top=15)
ax.plot(y[0,:],y[1,:],'ko',label=r'Obs')
ax.plot(x[0,:],x[1,:],'g-s',label=r'True')
ax.set_xlabel('$x_{1}$')
ax.set_ylabel('$x_{2}$')
fig.tight_layout()

runTillEnd = False

#Plot Kalman filter estimate
for t in range(mfilt.shape[1]):
    if not runTillEnd:
        if input("Press enter to continue (Predict). Type 'rte' to run simulation to end.") == "rte":
            runTillEnd = True

    clear_output(wait=True)

    #Plot prediction
    plotGaussianEllipse(ax, mpred[:,t], Vpred[:,:,t], \
                    ellipsecolor='green', markercolor='green')
    display(plt.gcf())

    if not runTillEnd:
        if input("Press enter to continue (Measurement). Type 'rte' to run simulation to end.") == "rte":
            runTillEnd = True

    clear_output(wait=True)

    #Remove last prediction ellipse
    del ax.patches[-1]
    del ax.lines[-1]

    #Plot update
    if t == 0:
        plotGaussianEllipse(ax, mfilt[:2,t],Vfilt[:2,:2,t],\
                    ellipsecolor='blue', markercolor='red',label=r'Filter')
        ax.legend()
    else:
        plotGaussianEllipse(ax, mfilt[:2,t],Vfilt[:2,:2,t],\
                    ellipsecolor='blue', markercolor='red',t=t)
    display(plt.gcf())

clear_output(wait=True)
```

## Simplification

### Covariance of p(x_t|y_{1:t})

```python
simp_exprs, subs = utils.simplify(p_xt_update.covar.to_full_expr())
```

```python
print(p_xt_update.covar.to_full_expr())
```

    Q + A*P_{t-1|t-1}*A' + (-1)*(Q + A*P_{t-1|t-1}*A')*C'*(R + C*(Q + A*P_{t-1|t-1}*A')*C')^-1*C*(Q + A*P_{t-1|t-1}*A')

```python
for s in simp_exprs:
    print("s: ", s)

for s in subs.keys():
    print("%s: %s"%(s,subs[s]))
```

    s:  (I + (-1)*A_{2}*C)*A_{1}
    s:  (-1)*A_{2}*C*A_{1} + A_{1}
    s:  A_{1}*(I + (-1)*C'*(R + C*A_{1}*C')^-1*C*A_{1})
    s:  (I + (-1)*A_{1}*C'*(R + C*A_{1}*C')^-1*C)*A_{1}
    s:  (-1)*A_{1}*C'*(R + C*A_{1}*C')^-1*C*A_{1} + A_{1}
    s:  (Q + A_{0})*(I + (-1)*C'*(R + C*(Q + A_{0})*C')^-1*C*(Q + A_{0}))
    s:  (I + (-1)*(Q + A_{0})*C'*(R + C*(Q + A_{0})*C')^-1*C)*(Q + A_{0})
    s:  Q + (-1)*(Q + A_{0})*C'*(R + C*(Q + A_{0})*C')^-1*C*(Q + A_{0}) + A_{0}
    Q + A_{0}: A_{1}
    A*P_{t-1|t-1}*A': A_{0}
    A_{1}*C'*(R + C*A_{1}*C')^-1: A_{2}

```python

```


```python
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import matplotlib
import seaborn as sns
from mpl_toolkits.mplot3d import Axes3D

```

```python
%matplotlib inline
matplotlib.rcParams['figure.figsize'] = (12, 8) # set default figure size, 12in by 10in
sns.set_style("whitegrid")
```

This week, we will be covering anomaly detection which is widely used in fraud detection (e.g. ‘has this credit card been stolen?’). Given a large number of data points, we may sometimes want to figure out which ones vary significantly from the average. For example, in manufacturing, we may want to detect defects or anomalies. We show how a dataset can be modeled using a Gaussian distribution, and how the model can be used for anomaly detection.

# Video W9 01: Problem Motivation (Anomaly Detection)

[YouTube Video Link](https://www.youtube.com/watch?v=hhI-PdMO1sk&list=PLZ9qNFMHZ-A4rycgrgOYma6zxF4BZGGPW&index=89)

Imagine you are a a manufacturer and are performing quality control on your manufacturing process. You might
gather data with features like:

- $x_0 = $ heat generated
- $x_1 = $ vibration intensity.

We might have data that looks like this:

```python
# just make a random dataset example where both features are distributed in a normal distribution
m = 30 # number of samples to generate
mu = 0.0
sigma = 0.5

np.random.seed(42)
X = np.random.normal(mu, sigma, (m, 2)) + 3.0

# plot the ok data
plt.plot(X[:,0], X[:,1], 'rx');
plt.axis([1, 5, 1, 5]);
plt.xlabel('$x_0$ (heat)', fontsize=16);
plt.ylabel('$x_1$ (vibration)', fontsize=16);

# new item x_test
x_test1 = np.array([3.3, 2.8])
plt.plot(x_test1[0], x_test1[1], 'gx', markersize=10)
plt.text(x_test1[0]+0.1, x_test1[1]-0.1, 'ok', fontsize=16)

x_test2 = np.array([4.0, 1.8])
plt.plot(x_test2[0], x_test2[1], 'gx', markersize=10)
plt.text(x_test2[0]+0.1, x_test2[1]-0.1, 'anomaly', fontsize=16)

```

More formaly, in anomaly detection we are given some dataset: $\{ x^{(1)}, x^{(2)}, \cdots, x^{(m)} \}$

We assume that the given dataset is normal, that it represents usual samples of the dataset.

We want an algorithm that tells us if a new sample $x_{test}$ is "anomalous", where this will mean that
it looks out of the expected range or pattern for the dataset given the nonanomalous data.

In one sense the training set is unlabeled, though you can also think of it as a set of data all labeled
as ok.

We want to build a model $p(x)$, which is the probability of $x$, where $x$ is the features of the dataset.

\begin{equation}
\begin{cases}
p(x_{test}) < \epsilon \to \text{flag as anomalous}\\
p(x_{test}) \ge \epsilon \to \text{ok}
\end{cases}
\end{equation}

The model $p(x)$ needs to estimate the "density" of the data, so that points in the dense part of the
distribution are assigned a high probability density, and those that are far from other examples are
assigned a low probability.

## Anomaly detection example

- Fraud detection:
  - $x^{(i)} = $ features of user $i$'s activities (how often log in, typing speed, etc.)
  - Model $p(x)$ from data.
  - Identify unusual users by checking which have $p(x) < \epsilon$

- Manufacturing
- Monitoring computers in a data center.
  - $x^{(i)} = $ features of machine $i$
  - memory use, number of disk accesses/sec, CPU load, CPU load / network traffic

# Video W9 02: Gaussian Distribution

[YouTube Video Link](https://www.youtube.com/watch?v=2yosK5CJZmo&index=90&list=PLZ9qNFMHZ-A4rycgrgOYma6zxF4BZGGPW)

## Gaussian (Normal) distribution

A review of the gaussian distribution.  Say $x \in \mathbb{R}$ If $x$ is a random variable distributed according
to a Gaussian distribution, with a mean $\mu$ and variance $\sigma^2$ (or equivalently standard deviation $\sigma$,
then the continuous probability density function of the random variable is given by the formula:

\begin{equation}
n(x; \mu, \sigma) = \frac{1}{ \sqrt{2 \pi \sigma} } e^{- \frac{1}{2 \sigma^2} (x - \mu)^2 }
\end{equation}

We normally don't have to know the formula to work with a gaussian distribution.  In Python, it is
best to use the `scipy.stats` classes that are defined to work with standard probability distributions.
The `scipy.stats.norm` class defines the normal or gaussian distribution.

```python
from scipy.stats import norm

# the default when creating a normal distribution from scipy.stats is to create a standard
# normal distribution with mu=0 and sigma=1, so we don't really have to pass in the
# metaparameters here if we want a standard normal distribution, we are given one by default
n_standard = norm()

# create some other normal distributions
mu_1 = 5.0
sigma_1 = 0.4
n_1 = norm(mu_1, sigma_1)

mu_2 = -2.0
sigma_2 = 2.2
n_2 = norm(mu_2, sigma_2)

# plot the standard normal distribution and the other ones for comparison
x = np.linspace(-8.0, 8.0, 1000)
plt.plot(x, n_standard.pdf(x), 'b-', label='standard normal distribution $\mu=0, \sigma=1$')
plt.plot(x, n_1.pdf(x), 'g-', label='normal distribution $\mu=%0.1f, \sigma=%0.1f$' % (mu_1, sigma_1))
plt.plot(x, n_2.pdf(x), 'r-', label='normal distribution $\mu=%0.1f, \sigma=%0.1f$' % (mu_2, sigma_2))
plt.xlabel('normal random variable x', fontsize=16);
plt.ylabel('$f(x)$ probability density function PDF', fontsize=16);
plt.legend();
plt.title('Gaussian (Normal) distribution');
```

Notice that we use the `.pdf()` function of the `scipy.stats.norm` object to plot the probability density function
of the normal curves.   A standard normal distribution has a mean of 0 and a standard deviation/variance of 1.
Notices that for other normal distributions, the `norm` object takes the mean $\mu$ and the standard deviation
$\sigma$ as meta parameters that define the distributions.  When the standard deviation is small, the
distribution will be skinny and tall.  When the standard deviation is large, the density will be more flat
and spread out.  But in all cases, the are probability density functions, so the area under the curves
all add up to 1.

```python
from scipy.integrate import quad as integrate

integrate(n_standard.pdf, -np.infty, np.infty)
```

    (0.9999999999999998, 1.0178191320905743e-08)

As an aside, we can also draw or generate random samples from a normal distribution. I actually did that above
to create the example data, using the `np.random.normal()` function.  You can also use `scipy.stats.norm`
function to draw a random sample from a normal distribution, using the `.rvs()` function, like this:

```python
# draw random samples
NUM_SAMPLES = 1000
samples = n_standard.rvs(NUM_SAMPLES)

# visualize the resulting density of the random samples, using seaborn built in scatter plotting and density
# visualization capabilities
sns.distplot(samples, hist=True, rug=True, label='estimated density function');

# put the true pdf density function onto the estimated density plot
x = np.linspace(-4.0, 4.0, 1000)
plt.plot(x, n_standard.pdf(x), 'r-', label='actual standard normal pdf')

plt.legend();
```

## Parameter Estimation ##

The parameter estimation problem is basically, given a set of data that you suspect may be distributed according to a
Gaussian distribution, how do you determine the values of the mean $\mu$ and the standard deviation $\sigma$ that best
fit or explain the data you have been given.  We can use a simple maximum likelihood estimate to determine which values
for the $\mu, \sigma$ parameters fit the data.  To estimate $\mu$, simply calculate the mean of the data points
you have been given:

$$
\mu = \frac{1}{m} \sum_{i=1}^{m} x^{(i)}
$$

Likewise, to estimate the variance $\sigma^2$, sum up the squares of each data point subtracted from your estimated
mean value:

$$
\sigma^2 = \frac{1}{m} \sum_{i=1}^{m} (x^{(i)} - \mu)^2
$$

Just to show that this approximation works, and to generate a set of data and figure similar to the one from our
companion video, lets create a set of 20 points drawn from a normal distribution with a mean of 1.5 and
a standard deviation of 0.75 using python:

```python
m = 20 # number of data points
mu_actual = 1.5 # the actual mean we will use to generate some example data points
sigma_actual = 0.75 # the actual standard deviation we will use to generate random data points

x = np.random.normal(loc=mu_actual, scale=sigma_actual, size = (m,))
plt.plot(x, np.zeros( (m,)), 'rx');
```

Remember that the actual mean and standard deviation we used for the points are 1.5 and 0.75 respectively.  We can get
mean likelihood estimates of the values for our randomly generated data like this:

```python
mu_estimate = np.sum(x) / m
variance_estimate = np.sum( (x - mu_estimate)**2.0 ) / m
sigma_estimate = np.sqrt(variance_estimate)
print(mu_estimate, sigma_estimate, variance_estimate)
```

    1.3382921172106135 0.8480846198163837 0.7192475223691002

```python
# the .mean(), .std() and .var() functions of numpy arrays calculate the same thing
print(x.mean(), x.std(), x.var())
```

    1.3382921172106135 0.8480846198163837 0.7192475223691002

# Video W9 03: Anomaly Detection Algorithm

[YouTube Video Link](https://www.youtube.com/watch?v=egIWLtZjHoo&list=PLZ9qNFMHZ-A4rycgrgOYma6zxF4BZGGPW&index=91)

## Density Estimation ##

One way to estimate the probability of a given observation being an anomaly or not is to make an independence assumption on
each of the $n$ features, and calculate the probability of getting each feature given our observed data.  To do this, we
use the previous method and assume that each of our features is normally distributed, and use a maximum likelihood estimate
to determine the mean and variance parameters for each of our features independently.  Given these estimations, we can use them
to calculate the probability of each observed feature, and multiplying all of these probabilities together (which works
because of the assumption of independence) gives us an overall estimate of the probability of seeing the new observation.

The final probability (or density) estimate can be stated compactly using the product formula

$$
\prod_{j=1}^n p(x_j; \mu_j, \sigma_j^2) = p(x_1; \mu_1, \sigma_1^2) \times p(x_2; \mu_2, \sigma_2^2) \times \ldots \times p(x_n; \mu_n, \sigma_n^2)
$$

Where for each of our $n$ features, we have determined the maximum likelihood $\mu_j, \sigma_j^2$ mean and variance
parameters.  The $\prod$ symbol simply means we are multiplying each of the $n$ probabilities together.

## Anomaly Detection Algorithm ##

Putting these previous concepts together, we can build an anomaly detection system using the following algorithm:

1. Choose features $x_i$ that you think might be indicative of anomalous examples.
2. Fit parameters $\mu_1, \ldots, \mu_n, \sigma_1^2, \ldots, \sigma_n^2$
$$
\mu_j = \frac{1}{m} \sum_{i=1}^{m} x_j^{(i)} \\
\sigma_j^2 = \frac{1}{m} \sum_{i=1}^{m} (x_j^{(i)} - \mu_j)^2
$$
3. Given a new example $x$, compute $p(x)$:
$$
p(x) = \prod_{j=1}^n p(x_j; \mu_j, \sigma_j^2) = \prod_{j=1}^{n} \frac{1}{\sqrt{2 \pi} \sigma_j} \textrm{exp}(- \frac{(x_j - \mu_j)^2}{2 \sigma_j^2})
$$

And finally, we will determine a new observation is an anomaly if the probability is below some threshold,
e.g. if $p(x) < \epsilon$.

Lets give an example in Python similar to the one shown in our video.  Lets again generate a set of data, but with
2 dimensions or features in this case.  As shown in the video, the mean and standard deviation we will use for our two
features will be:
$$
\mu_1 = 5, \sigma_1 = 2\\
\mu_2 = 3, \sigma_2 = 1
$$

Here we will show vectorized versions of computing the maximum likelihood estimates and probabilities:

```python
m, n = 20, 2 # number of example data points ; number of features/dimensions
mu_actual = np.array([5.0, 3.0])
sigma_actual = np.array([2.0, 1.0])

# we will generate a mxn array of values with mean 0 and std 1.0
x = np.random.normal(loc=0.0, scale=1.0, size=(m, n))

# and we will scale each dimension to have the desired mean/std.
x[:,0] = (x[:,0] * sigma_actual[0]) + mu_actual[0]
x[:,1] = (x[:,1] * sigma_actual[1]) + mu_actual[1]
```

```python
# display our randomly generated set of 20 data points in
# our 2 dimensional feature space
#plt.plot(x[:,0], x[:,1], 'rx', markersize=10, markeredgewidth=3, label='training data points')
plt.scatter(x[:,0], x[:,1], c='r', marker='x', s=75, label='training data points')
plt.xlabel('$x_0$', fontsize=20)
plt.ylabel('$x_1$', fontsize=20)
plt.axis([-2, 12, -2, 8])

# display location of 2 new observations we will use to demonstrate
# calculating the density or probability estimate
x_new = np.array([[5.5, 2.9],
                  [2.5, 0.8]])
#plt.plot(x_new[:,0], x_new[:,1], 'bo', markersize=10, markeredgewidth=3, label='new observations')
plt.scatter(x_new[:,0], x_new[:,1], c='b', marker='o', s=75, label='new observations')
plt.legend()
```

```python
# define a function that compute the probability/density of an observation
def probability_density_function(x, mu, sigma):
    """Calculate the probability of the given observation x for a normal distribution with mean mu and
    standard deviation sigma.  These are known as computing the probability density function, or pdf.
    This is equivalent to the scipy libraries stats.norm.pdf() function.
    """
    return (1.0 / (np.sqrt(2.0 * np.pi) * sigma) ) * np.exp(- (x - mu)**2.0 / (2.0 * sigma**2.0) )
```

```python
# calculate the maximum likelihood mean and variance for each dimension
print( "The maximum likelihood mean and variance for our 2 dimensions:" )
print( np.mean(x, axis=0) )
print( np.std(x, axis=0) )
mu_estimate = np.mean(x, axis=0)
sigma_estimate = np.std(x, axis=0)

# calculate the density or probability estimate for the new observations
# first an example of the probability calculation for the point 0, for dimension 0 and 1
print( "" )
print( "A new observation that is not analomous:", x_new[0,:] )
p_0 = probability_density_function(x_new[0, 0], mu_estimate[0], sigma_estimate[0])
print( "Probability for dimension 0:", p_0 )
p_1 = probability_density_function(x_new[0, 1], mu_estimate[1], sigma_estimate[1])
print( "Probability for dimension 1:", p_1 )
print( "Product of independent probabilities:", p_0 * p_1 )

# now the same calculation for our anamolous point 1
print( "" )
print( "A new observation that IS analomous:", x_new[1,:] )
p_0 = probability_density_function(x_new[1, 0], mu_estimate[0], sigma_estimate[0])
print( "Probability for dimension 0:", p_0 )
p_1 = probability_density_function(x_new[1, 1], mu_estimate[1], sigma_estimate[1])
print( "Probability for dimension 1:", p_1 )
print( "Product of independent probabilities:", p_0 * p_1 )

```

    The maximum likelihood mean and variance for our 2 dimensions:
    [6.04844122 2.96691212]
    [1.54294066 1.10933966]

    A new observation that is not analomous: [5.5 2.9]
    Probability for dimension 0: 0.2427309421015668
    Probability for dimension 1: 0.35896781649658716
    Product of independent probabilities: 0.08713259628235895

    A new observation that IS analomous: [2.5 0.8]
    Probability for dimension 0: 0.018367945262696094
    Probability for dimension 1: 0.05337234663482837
    Product of independent probabilities: 0.0009803403415301697

```python
# we can get the exact same results using the pdf function from scipy, which we should normally do rather
# than implementing the function by hand as I have done above
import scipy.stats
print("")
print("A new observation that is not analomous:", x_new[0,:])
p_0 = scipy.stats.norm.pdf(x_new[0, 0], loc=mu_estimate[0], scale=sigma_estimate[0])
print("Probability for dimension 0:", p_0)
p_1 = scipy.stats.norm.pdf(x_new[0, 1], loc=mu_estimate[1], scale=sigma_estimate[1])
print("Probability for dimension 1:", p_1)
print("Product of independent probabilities:", p_0 * p_1)

# now the same calculation for our anamolous point 1
print("")
print("A new observation that IS analomous:", x_new[1,:])
p_0 = scipy.stats.norm.pdf(x_new[1, 0], loc=mu_estimate[0], scale=sigma_estimate[0])
print("Probability for dimension 0:", p_0)
p_1 = scipy.stats.norm.pdf(x_new[1, 1], loc=mu_estimate[1], scale=sigma_estimate[1])
print("Probability for dimension 1:", p_1)
print("Product of independent probabilities:", p_0 * p_1)

```

    A new observation that is not analomous: [5.5 2.9]
    Probability for dimension 0: 0.24273094210156682
    Probability for dimension 1: 0.35896781649658716
    Product of independent probabilities: 0.08713259628235896

    A new observation that IS analomous: [2.5 0.8]
    Probability for dimension 0: 0.018367945262696087
    Probability for dimension 1: 0.05337234663482838
    Product of independent probabilities: 0.0009803403415301693

```python
# or to vectorize the above
print("probabilities of all points for each dimension:")
p = scipy.stats.norm.pdf(x_new, loc=mu_estimate, scale=sigma_estimate)
print(p)

print("")
print("The computed product of the probabilites for our points:")
p = np.prod(p, axis=1)
print(p)
```

    probabilities of all points for each dimension:
    [[0.24273094 0.35896782]
     [0.01836795 0.05337235]]

    The computed product of the probabilites for our points:
    [0.0871326  0.00098034]

```python
# and finally, we can select an epsilon threshold, and determine which points are anomalous and which not
# using the given threshold
epsilon = 0.005
print(p < epsilon)
```

    [False  True]

The above cells showed how we can compute the anomaly detection algoritm.  Lets visualize what an epsilon value of 0.005 means
for our given set of 20 data points.  Here we will plot what is essentially the decision boundary for our given set of
data, above which the threshold will determine points are "anamolous"

```python
# display our randomly generated set of 20 data points in
# our 2 dimensional feature space
#plt.plot(x[:,0], x[:,1], 'rx', markersize=10, markeredgewidth=3, label='training data points')
plt.scatter(x[:,0], x[:,1], c='r', marker='x', s=75, label='training data points')
plt.xlabel('$x_0$', fontsize=20)
plt.ylabel('$x_1$', fontsize=20)
plt.axis([-2, 12, -2, 8])

# display location of 2 new observations we will use to demonstrate
# calculating the density or probability estimate
x_new = np.array([[5.5, 2.9],
                  [2.5, 0.8]])
#plt.plot(x_new[:,0], x_new[:,1], 'bo', markersize=10, markeredgewidth=3, label='new observations')
plt.scatter(x_new[:,0], x_new[:,1], c='b', marker='o', s=75, label='new observations')
plt.legend()

# and create a mesh to detect decision boundary for a given epsilon and plot it
epsilon = 0.005
x_min, x_max = -2.0, 12.0
y_min, y_max = -2.0, 8.0
h = .01  # step size in the mesh
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                     np.arange(y_min, y_max, h))

gridpoints = np.c_[xx.ravel(), yy.ravel()]
p = scipy.stats.norm.pdf(gridpoints, loc=mu_estimate, scale=sigma_estimate)
p = (np.prod(p, axis=1) < epsilon) * 1.0
Z = p.reshape(xx.shape)
plt.contourf(xx, yy, Z, cmap=plt.cm.Paired, alpha=0.4);

```

```python
# display our randomly generated set of 20 data points in
# our 2 dimensional feature space
#plt.plot(x[:,0], x[:,1], 'rx', markersize=10, markeredgewidth=3, label='training data points')
plt.scatter(x[:,0], x[:,1], c='r', marker='x', s=75, label='training data points')
plt.xlabel('$x_0$', fontsize=20)
plt.ylabel('$x_1$', fontsize=20)
plt.axis([-2, 12, -2, 8])

# display location of 2 new observations we will use to demonstrate
# calculating the density or probability estimate
x_new = np.array([[5.5, 2.9],
                  [2.5, 0.8]])
#plt.plot(x_new[:,0], x_new[:,1], 'bo', markersize=10, markeredgewidth=3, label='new observations')
plt.scatter(x_new[:,0], x_new[:,1], c='b', marker='o', s=75, label='new observations')
plt.legend()

# and create a mesh to detect decision boundary for a given epsilon and plot it
epsilon = 0.005
x_min, x_max = -2.0, 12.0
y_min, y_max = -2.0, 8.0
h = .01  # step size in the mesh
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                     np.arange(y_min, y_max, h))

gridpoints = np.c_[xx.ravel(), yy.ravel()]
p = scipy.stats.norm.pdf(gridpoints, loc=mu_estimate, scale=sigma_estimate)
p = np.prod(p, axis=1)
Z = p.reshape(xx.shape)
levels = np.arange(0.0, 0.11, 0.005)
plt.contourf(xx, yy, Z, levels, cmap=plt.cm.RdYlBu_r, alpha=0.4);
plt.colorbar();

```

# Video W9 04: Developing and Evaluating an Anomaly Detection System

[YouTube Video Link](https://www.youtube.com/watch?v=20dfmwVwK-8&list=PLZ9qNFMHZ-A4rycgrgOYma6zxF4BZGGPW&index=92)

To evaluate an anomaly detection system, we might get some labeled data.  Often this training set is
a bit skewed, we will often have a large colleciton of nonanomalous normal examples, with relatively few
anomalous examples.

## Training Example

Say we have data with 10000 good (normal) examples, and 20 flawed (anomalous) examples.

- Training set: use 6000 good examples ($y = 0$)
- CV: 2000 good examples ($y = 0$), 10 anomalous ($y = 1$)
- Test: 2000 good examples ($y = 0$), 10 anomalous ($y = 1$)

## Algorithm Evaluation

- Fit model $p(x)$ on training set $\{x^{(1)}, \cdots, x^{(m)}  \}$
- On cross validation/test example $x$, predict

\begin{equation}
y =
\begin{cases}
0 & p(x) < \epsilon \text{anomaly}\\
1 & p(x) \ge \epsilon \text{normal}
\end{cases}
\end{equation}

Because the data is very skewed, classification accuracy would not be a very good metric to use
to evaluate your anomaly detector.  By predicting all items are not anomalous, you will get
pretty good accuracy if there are very few anomalous items.

Possible evaluation metrics:
- True postivie, false positive, false negative, true negative
- Precision/Recall
- $F_1$ -score

Can also use cross validation set to choose parameter $\epsilon$.

Lets give an example of how to compute these metrics.  First of all, we will make up some
data again with 2 features.  I will generate anomalies by randomly choosing some values that are
below a threshold, and removing them from the data set.

```python
# same method as before, but generate 10020 points so we can pull out 20 anomalous points.
np.random.seed(42) # set seed so example is reproducable
m, n = 10020, 2 # number of example data points ; number of features/dimensions
mu_actual = np.array([5.0, 3.0])
sigma_actual = np.array([2.0, 1.0])

# we will generate a mxn array of values with mean 0 and std 1.0
x = np.random.normal(loc=0.0, scale=1.0, size=(m, n))

# and we will scale each dimension to have the desired mean/std.
x[:,0] = (x[:,0] * sigma_actual[0]) + mu_actual[0]
x[:,1] = (x[:,1] * sigma_actual[1]) + mu_actual[1]
```

```python
# need to calculate the density estimate, so we can choose some values at random to be the anomalies
# get parameter estimates
mu_estimate = np.mean(x, axis=0)
sigma_estimate = np.std(x, axis=0)
print(mu_estimate, sigma_estimate)

# compute density estimate
p = scipy.stats.norm.pdf(x, loc=mu_estimate, scale=sigma_estimate)
p = np.prod(p, axis=1)
print(p.min(), p.max())

# determine the value for the 50 lowest scores, so we can randomly choose 20 of these 50 as anomalies
p_sorted = np.copy(p)
p_sorted.sort()
candidate_cutoff = p_sorted[50]
print(candidate_cutoff)
print(np.sum(p < candidate_cutoff))

# find indexes of the 50 candidates in original data
candidate_indexes = np.where(p < candidate_cutoff)[0]
print(candidate_indexes)

# randomly choose 20 of the candidate indexes to be anomalies
anomaly_indexes = np.random.choice(candidate_indexes, 20)
print(anomaly_indexes)

# extract these items for anomalies, and remove them from the training set x
x_anomalies = np.copy(x[anomaly_indexes])
print(x_anomalies.shape)

# bug? not sure why we end up with 10001 here, but none of the items
# removed appear to be in x so?
x = np.delete(x, anomaly_indexes, axis=0)
x = x[:10000,:]
print(x.shape)

```

    [5.00686592 3.00814818] [2.02279433 0.99329815]
    2.53267379519127e-06 0.07920067447773657
    0.0003677434885608923
    50
    [ 104  131  580  978 1152 1447 1727 1858 1991 2319 2389 2425 2435 2688
     3445 3720 3805 3838 4082 4100 4124 4885 5367 5383 5544 5590 5669 5713
     5821 6122 6354 6456 6612 6722 6973 7290 7327 7581 7921 8690 8718 8873
     9059 9107 9407 9425 9515 9745 9837 9926]
    [4885 5383 2319  131 5713 8718 7327 3805 1447 9059 1152 7921 1727 6456
      978 4124 8873 6456 3838 6612]
    (20, 2)
    (10000, 2)

```python
# visualize the points and anomalies
plt.plot(x[:,0], x[:,1], 'bo', alpha=0.1);
plt.plot(x_anomalies[:,0], x_anomalies[:,1], 'ro', alpha=0.5);
```

```python
# we will split into train, cv and test sets as shown in the video
x_train = x[:6000, :]
x_cv = x[6000:8000, :]
x_test = x[8000:, :]
print(x_train.shape, x_cv.shape, x_test.shape)

x_anomalies_cv = x_anomalies[:10, :]
x_anomalies_test = x_anomalies[10:, :]
print(x_anomalies_cv.shape, x_anomalies_test.shape)
```

    (6000, 2) (2000, 2) (2000, 2)
    (10, 2) (10, 2)

```python
# put the anomalies into the cv and test, and create a y labels for each
x_cv = np.concatenate((x_cv, x_anomalies_cv), axis=0)
x_test = np.concatenate((x_test, x_anomalies_test), axis=0)
print(x_cv.shape, x_test.shape)

# create y values, to make it easier to calculate evaluation metrics
# anomalies are "positive" or 1 cases, and normal are "negative" or 0 cases
m, n = x_cv.shape
y_cv = np.zeros(m)
y_cv[2000:] = 1

m, n = x_test.shape
y_test = np.zeros(m)
y_test[2000:] = 1

```

    (2010, 2) (2010, 2)

```python
# retrain the density estimation only on the "normal" 6000 training items
p = scipy.stats.norm.pdf(x_train, loc=mu_estimate, scale=sigma_estimate)
p = np.prod(p, axis=1)
print(p.min(), p.max())
```

    2.730093667765102e-05 0.07919897520096435

All of the previous was just to create a random data set with 10000 normal items and 20 anomalies.
We have also created our density estimates using a 6000 test subset of the data.

### True/False Positive/Negative rates

[reference](https://developers.google.com/machine-learning/crash-course/classification/true-false-positive-negative)

Lets first look at calculating true/false positives and negatives as an evaluation method.  Lets try
to evalute using the method and the cross validation test set to try and set the optimal value
for epsilon $\epsilon$, which is our threshold for the anomaly detector.

The true vs. false and positive vs. negative is basically like a confusion matrix, but for a binary
classifier there are only 2 output classes.  So for problems like this dataset, we expect to get a lot
of true negatives (because we consider normal or no anomaly as negative examples with $y = 0$) here.
So our interest is in the true positives we get, and the number of false negatives and positives.

```python
from sklearn.metrics import confusion_matrix
```

```python
def compute_tfpn_metrics(y_true, y_pred):
    """Compute True/False Positive/Negative rates
    This could be vectorized, but lets be explicit to make sure we understand
    what we are calculating.
    """
    m = y_true.shape[0]
    tp = 0 # true positives
    tn = 0 # true negatives
    fp = 0 # false positives
    fn = 0 # false negatives
    for i in range(m):
        # a true positive is when y is positive and we predict positive
        if y_true[i] == 1 and y_pred[i] == 1:
            tp += 1

        # a true negative is when y is negative and we predict negative
        if y_true[i] == 0 and y_pred[i] == 0:
            tn += 1

        # a false positive is when y is negative but we predict positive
        if y_true[i] == 0 and y_pred[i] == 1:
            fp += 1

        # a false negative is when y is positive but we predict negative
        if y_true[i] == 1 and y_pred[i] == 0:
            fn += 1

    #print("True Positives : %5d   False Positives: %5d" % (tp, fp))
    #print("False Negatives: %5d   True Negatives:  %5d" % (fn, tn))
    return tp, fp, fn, tn
```

```python
def plot_tfpn_matrix(y_true, y_pred):
    """Visualize the true/false positive/negatives confusion matrix.
    Here we use an actual confusin_matrix() function from sklearn.metrics
    But to display in the standard way, we have to completely flip
    the matrix around to get into the normal way one displays a the tfpn
    confusion matrix.
    """
    c = confusion_matrix(y_true, y_predicted)
    c = np.fliplr(c)
    c = np.flipud(c)
    c = c.T
    sns.heatmap(c, annot=True, cbar=False, fmt='0.0f', cmap=plt.cm.Blues, annot_kws={"size": 16}, linewidths=4);
    plt.xlabel('True Label', fontsize=16)
    plt.ylabel('Predicted Label', fontsize=16)
    plt.xticks([0.5, 1.5], labels=['Positive', 'Negative'])
    plt.yticks([0.5, 1.5], labels=['Positive', 'Negative'])
    plt.text(0.3, 0.4, 'True Positive (TP)', fontsize=16);
    plt.text(1.3, 0.4, 'False Positive (FP)', fontsize=16);
    plt.text(0.3, 1.4, 'False Negative (FN)', fontsize=16);
    plt.text(1.3, 1.4, 'True Negative (TN)', color='white', fontsize=16);
```

```python
# we compute probability on the cv data using the mu_estimate and sigma_estimate
# from the training set calculated
# previously
p = scipy.stats.norm.pdf(x_cv, loc=mu_estimate, scale=sigma_estimate)
p = np.prod(p, axis=1)
print(p.min(), p.max())

# as an example, choose an epsilon and calculate the true/false positive/negatives
epsilon = 1e-04
y_predicted = (p < epsilon) * 1.0 # multiplying by 1 turns the boolean results into 1 or 0
```

    1.3566191452835629e-05 0.07920067447773657

```python
tp, fp, fn, tn = compute_tfpn_metrics(y_cv, y_predicted)
print("True Positives : %5d   False Positives: %5d" % (tp, fp))
print("False Negatives: %5d   True Negatives:  %5d" % (fn, tn))

plot_tfpn_matrix(y_cv, y_predicted)
```

As you can see, we ended up only getting 4 of the anomalies correct of the 10, and we
misclassified one of the normal items as an anomaly.

Tuning this kind of threshold represents a tradeoff.  Is it worse to let 1 true anomaly get through
the system (false negatives here)?  Or are false positives to costly to deal with (e.g. because
we are annoying customers too much who aren't commiting fraud, or we are pulling too many
engines for inspection that are actually ok).

We can try a range of epsilon values, and see the FN/FP ratios that result.

```python
# first try
for epsilon in np.logspace(-6, -1, 10):
    print('Testing epsilon %0.8f:' % epsilon)
    y_predicted = (p < epsilon) * 1.0 # multiplying by 1 turns the boolean results into 1 or 0
    tp, fp, fn, tn = compute_tfpn_metrics(y_cv, y_predicted)
    print("True Positives : %5d   False Positives: %5d" % (tp, fp))
    print("False Negatives: %5d   True Negatives:  %5d" % (fn, tn))
    print("\n")
```

    Testing epsilon 0.00000100:
    True Positives :     0   False Positives:     0
    False Negatives:    10   True Negatives:   2000

    Testing epsilon 0.00000359:
    True Positives :     0   False Positives:     0
    False Negatives:    10   True Negatives:   2000

    Testing epsilon 0.00001292:
    True Positives :     0   False Positives:     0
    False Negatives:    10   True Negatives:   2000

    Testing epsilon 0.00004642:
    True Positives :     2   False Positives:     1
    False Negatives:     8   True Negatives:   1999

    Testing epsilon 0.00016681:
    True Positives :     4   False Positives:     1
    False Negatives:     6   True Negatives:   1999

    Testing epsilon 0.00059948:
    True Positives :    10   False Positives:    12
    False Negatives:     0   True Negatives:   1988

    Testing epsilon 0.00215443:
    True Positives :    10   False Positives:    48
    False Negatives:     0   True Negatives:   1952

    Testing epsilon 0.00774264:
    True Positives :    10   False Positives:   201
    False Negatives:     0   True Negatives:   1799

    Testing epsilon 0.02782559:
    True Positives :    10   False Positives:   716
    False Negatives:     0   True Negatives:   1284

    Testing epsilon 0.10000000:
    True Positives :    10   False Positives:  2000
    False Negatives:     0   True Negatives:      0

```python
# we really want something between 0.00004 and 0.00050
# second try
for epsilon in np.linspace(0.00004, 0.0005, 20):
    print('Testing epsilon %0.8f:' % epsilon)
    y_predicted = (p < epsilon) * 1.0 # multiplying by 1 turns the boolean results into 1 or 0
    tp, fp, fn, tn = compute_tfpn_metrics(y_cv, y_predicted)
    print("True Positives : %5d   False Positives: %5d" % (tp, fp))
    print("False Negatives: %5d   True Negatives:  %5d" % (fn, tn))
    print("\n")
```

    Testing epsilon 0.00004000:
    True Positives :     1   False Positives:     1
    False Negatives:     9   True Negatives:   1999

    Testing epsilon 0.00006421:
    True Positives :     3   False Positives:     1
    False Negatives:     7   True Negatives:   1999

    Testing epsilon 0.00008842:
    True Positives :     4   False Positives:     1
    False Negatives:     6   True Negatives:   1999

    Testing epsilon 0.00011263:
    True Positives :     4   False Positives:     1
    False Negatives:     6   True Negatives:   1999

    Testing epsilon 0.00013684:
    True Positives :     4   False Positives:     1
    False Negatives:     6   True Negatives:   1999

    Testing epsilon 0.00016105:
    True Positives :     4   False Positives:     1
    False Negatives:     6   True Negatives:   1999

    Testing epsilon 0.00018526:
    True Positives :     6   False Positives:     1
    False Negatives:     4   True Negatives:   1999

    Testing epsilon 0.00020947:
    True Positives :     7   False Positives:     1
    False Negatives:     3   True Negatives:   1999

    Testing epsilon 0.00023368:
    True Positives :     7   False Positives:     2
    False Negatives:     3   True Negatives:   1998

    Testing epsilon 0.00025789:
    True Positives :     7   False Positives:     2
    False Negatives:     3   True Negatives:   1998

    Testing epsilon 0.00028211:
    True Positives :     9   False Positives:     3
    False Negatives:     1   True Negatives:   1997

    Testing epsilon 0.00030632:
    True Positives :    10   False Positives:     5
    False Negatives:     0   True Negatives:   1995

    Testing epsilon 0.00033053:
    True Positives :    10   False Positives:     6
    False Negatives:     0   True Negatives:   1994

    Testing epsilon 0.00035474:
    True Positives :    10   False Positives:     6
    False Negatives:     0   True Negatives:   1994

    Testing epsilon 0.00037895:
    True Positives :    10   False Positives:     7
    False Negatives:     0   True Negatives:   1993

    Testing epsilon 0.00040316:
    True Positives :    10   False Positives:     8
    False Negatives:     0   True Negatives:   1992

    Testing epsilon 0.00042737:
    True Positives :    10   False Positives:    10
    False Negatives:     0   True Negatives:   1990

    Testing epsilon 0.00045158:
    True Positives :    10   False Positives:    10
    False Negatives:     0   True Negatives:   1990

    Testing epsilon 0.00047579:
    True Positives :    10   False Positives:    10
    False Negatives:     0   True Negatives:   1990

    Testing epsilon 0.00050000:
    True Positives :    10   False Positives:    10
    False Negatives:     0   True Negatives:   1990

So we perhaps want something like $\epsilon = 0.00028211$ where we only caught almost all of
the anomalies while only have 3 false positives.  Lets see how that setting of epsilon works
for the held back final test set.

```python
# calculate density on the test set now
p = scipy.stats.norm.pdf(x_test, loc=mu_estimate, scale=sigma_estimate)
p = np.prod(p, axis=1)
print(p.min(), p.max())

# and check the fn/fp ratio
epsilon = 0.00028211
y_predicted = (p < epsilon) * 1.0 # multiplying by 1 turns the boolean results into 1 or 0
tp, fp, fn, tn = compute_tfpn_metrics(y_cv, y_predicted)
print("True Positives : %5d   False Positives: %5d" % (tp, fp))
print("False Negatives: %5d   True Negatives:  %5d" % (fn, tn))
```

    2.53267379519127e-06 0.07916662594378182
    True Positives :     7   False Positives:     7
    False Negatives:     3   True Negatives:   1993

### Precision and Recall

[reference](https://towardsdatascience.com/precision-vs-recall-386cf9f89488)

Calculating precision and recall is just taking the true/false positive/negatives and calculating
some actual ratios.  We can calculate Precision, Recall and Accuracy

\begin{equation}
\text{Precision} = \frac{\text{True Positive (TP)}}{\text{Actual Results}} = \frac{\text{True Positive (TP)}}{\text{True Positive (TP) + False Positive (FP)}}
\end{equation}

\begin{equation}
\text{Recall} = \frac{\text{True Positive (TP)}}{\text{Predicted Results}} = \frac{\text{True Positive (TP)}}{\text{True Positive (TP) + False Negative (FN)}}
\end{equation}

\begin{equation}
\text{Accuracy} = \frac{\text{True Positive (TP)} + \text{True Negative (TN)}}{\text{Total}}
\end{equation}

Often, we think that precision and recall both indicate accuracy of the model. While that is somewhat true, there is a deeper, distinct meaning of each of these terms. Precision means the percentage of your results which are relevant. On the other hand, recall refers to the percentage of total relevant results correctly classified by your algorithm.

Lets redo the last epsilon meta search, but calculate precision and recall scores instead.

```python
def compute_precision_recall(y_true, y_predicted):
    tp, fp, fn, tn = compute_tfpn_metrics(y_true, y_predicted)
    precision = float(tp) / float(tp + fp)
    recall = float(tp) / float(tp + fn)
    m = y_true.shape[0]
    accuracy = float(tp + tn) / float(m)
    return precision, recall, accuracy
```

```python
# compute density on cv set
p = scipy.stats.norm.pdf(x_cv, loc=mu_estimate, scale=sigma_estimate)
p = np.prod(p, axis=1)
print(p.min(), p.max())

# we really want something between 0.00004 and 0.00050
# second try
for epsilon in np.linspace(0.00004, 0.0005, 20):
    print('Testing epsilon %0.8f:' % epsilon)
    y_predicted = (p < epsilon) * 1.0 # multiplying by 1 turns the boolean results into 1 or 0
    precision, recall, accuracy = compute_precision_recall(y_cv, y_predicted)
    print('precision: %0.5f, recall: %0.5f, accuracy: %0.5f' % (precision, recall, accuracy))
    print("\n")
```

    1.3566191452835629e-05 0.07920067447773657
    Testing epsilon 0.00004000:
    precision: 0.50000, recall: 0.10000, accuracy: 0.99502

    Testing epsilon 0.00006421:
    precision: 0.75000, recall: 0.30000, accuracy: 0.99602

    Testing epsilon 0.00008842:
    precision: 0.80000, recall: 0.40000, accuracy: 0.99652

    Testing epsilon 0.00011263:
    precision: 0.80000, recall: 0.40000, accuracy: 0.99652

    Testing epsilon 0.00013684:
    precision: 0.80000, recall: 0.40000, accuracy: 0.99652

    Testing epsilon 0.00016105:
    precision: 0.80000, recall: 0.40000, accuracy: 0.99652

    Testing epsilon 0.00018526:
    precision: 0.85714, recall: 0.60000, accuracy: 0.99751

    Testing epsilon 0.00020947:
    precision: 0.87500, recall: 0.70000, accuracy: 0.99801

    Testing epsilon 0.00023368:
    precision: 0.77778, recall: 0.70000, accuracy: 0.99751

    Testing epsilon 0.00025789:
    precision: 0.77778, recall: 0.70000, accuracy: 0.99751

    Testing epsilon 0.00028211:
    precision: 0.75000, recall: 0.90000, accuracy: 0.99801

    Testing epsilon 0.00030632:
    precision: 0.66667, recall: 1.00000, accuracy: 0.99751

    Testing epsilon 0.00033053:
    precision: 0.62500, recall: 1.00000, accuracy: 0.99701

    Testing epsilon 0.00035474:
    precision: 0.62500, recall: 1.00000, accuracy: 0.99701

    Testing epsilon 0.00037895:
    precision: 0.58824, recall: 1.00000, accuracy: 0.99652

    Testing epsilon 0.00040316:
    precision: 0.55556, recall: 1.00000, accuracy: 0.99602

    Testing epsilon 0.00042737:
    precision: 0.50000, recall: 1.00000, accuracy: 0.99502

    Testing epsilon 0.00045158:
    precision: 0.50000, recall: 1.00000, accuracy: 0.99502

    Testing epsilon 0.00047579:
    precision: 0.50000, recall: 1.00000, accuracy: 0.99502

    Testing epsilon 0.00050000:
    precision: 0.50000, recall: 1.00000, accuracy: 0.99502

```python
# it can also be useful to plot recall and precision as functions of the parameter (epsilon).
# basically the location they cross represents the most balanced trade off you can achieve

# we really want something between 0.00004 and 0.00050
# second try
NUM_TESTS = 100
epsilons = np.linspace(0.00004, 0.0005, NUM_TESTS)
precision = np.empty(NUM_TESTS)
recall = np.empty(NUM_TESTS)
accuracy = np.empty(NUM_TESTS)
for i, epsilon in enumerate(epsilons):
    y_predicted = (p < epsilon) * 1.0 # multiplying by 1 turns the boolean results into 1 or 0
    precision[i], recall[i], accuracy[i] = compute_precision_recall(y_cv, y_predicted)

plt.plot(epsilons, precision, label='Precision')
plt.plot(epsilons, recall, label='Recall')
plt.xlabel('epsilon $\epsilon$')
plt.ylabel('score')
plt.legend();
```

### $F_1$ score

In most problems you could either give a higher priority to maximizing precision, or recall, depending
on the problem.  The $F_1$ score is a simpler metric which takes both into account.  The $F_1$ score
is simply the harmonic mean of precision and recall:

\begin{equation}
F_1 = 2 \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}}
\end{equation}

```python
def compute_f1_score(y_true, y_predicted):
    precision, recall, accuracy = compute_precision_recall(y_true, y_predicted)
    f1 = 2 * (precision * recall) / (precision + recall)
    return f1
```

```python
# compute density on cv set
p = scipy.stats.norm.pdf(x_cv, loc=mu_estimate, scale=sigma_estimate)
p = np.prod(p, axis=1)
print(p.min(), p.max())

# we really want something between 0.00004 and 0.00050
# second try
for epsilon in np.linspace(0.00004, 0.0005, 20):
    print('Testing epsilon %0.8f:' % epsilon)
    y_predicted = (p < epsilon) * 1.0 # multiplying by 1 turns the boolean results into 1 or 0
    f1 = compute_f1_score(y_cv, y_predicted)
    print('f1: %0.5f' % (f1))
    print("\n")
```

    1.3566191452835629e-05 0.07920067447773657
    Testing epsilon 0.00004000:
    f1: 0.16667

    Testing epsilon 0.00006421:
    f1: 0.42857

    Testing epsilon 0.00008842:
    f1: 0.53333

    Testing epsilon 0.00011263:
    f1: 0.53333

    Testing epsilon 0.00013684:
    f1: 0.53333

    Testing epsilon 0.00016105:
    f1: 0.53333

    Testing epsilon 0.00018526:
    f1: 0.70588

    Testing epsilon 0.00020947:
    f1: 0.77778

    Testing epsilon 0.00023368:
    f1: 0.73684

    Testing epsilon 0.00025789:
    f1: 0.73684

    Testing epsilon 0.00028211:
    f1: 0.81818

    Testing epsilon 0.00030632:
    f1: 0.80000

    Testing epsilon 0.00033053:
    f1: 0.76923

    Testing epsilon 0.00035474:
    f1: 0.76923

    Testing epsilon 0.00037895:
    f1: 0.74074

    Testing epsilon 0.00040316:
    f1: 0.71429

    Testing epsilon 0.00042737:
    f1: 0.66667

    Testing epsilon 0.00045158:
    f1: 0.66667

    Testing epsilon 0.00047579:
    f1: 0.66667

    Testing epsilon 0.00050000:
    f1: 0.66667

```python
# it can also be useful to plot recall and precision as functions of the parameter (epsilon).
# basically the location they cross represents the most balanced trade off you can achieve

# we really want something between 0.00004 and 0.00050
# second try
NUM_TESTS = 100
epsilons = np.linspace(0.00004, 0.0005, NUM_TESTS)
precision = np.empty(NUM_TESTS)
recall = np.empty(NUM_TESTS)
accuracy = np.empty(NUM_TESTS)
f1 = np.empty(NUM_TESTS)
for i, epsilon in enumerate(epsilons):
    y_predicted = (p < epsilon) * 1.0 # multiplying by 1 turns the boolean results into 1 or 0
    precision[i], recall[i], accuracy[i] = compute_precision_recall(y_cv, y_predicted)
    f1[i] = compute_f1_score(y_cv, y_predicted)

plt.plot(epsilons, precision, label='Precision')
plt.plot(epsilons, recall, label='Recall')
plt.plot(epsilons, f1, label='$F_1$')
plt.xlabel('epsilon $\epsilon$')
plt.ylabel('score')
plt.legend();
```

# Video W9 05: Anomaly Detection vs. Supervised Learning

[YouTube Video Link](https://www.youtube.com/watch?v=mGXtKNHLRMg&index=93&list=PLZ9qNFMHZ-A4rycgrgOYma6zxF4BZGGPW)

Why don't we just use a supervised learning algorithm instead of the anomaly detection if we have
some examples of anomalous values.

**Anomaly Detection**

- very small number of positive examples ($y = 1$).  0-20 is common
- Large number of negative ($y = 0$) examples.
- Many different "types" of anomalies.  Hard for any algorithm to learn from positive examples what the
  anomalies look like; future anomalies may look nothing like any of the anomalous examples we've seen
  so far.

**Supervised Learning**

- Normally have a reasonalbly large number of positive and negative examples.
- Enough positive examples for algorithm to get a sense of what positive examples are like, future
  positive examples likely to be similar to ones in training set.

Key difference is usually in anomaly detection we have such a small number of positive examples that it
is not possible for a supervised learning method to learn that much from the positive examples.

# Video W9 06: Choosing what Features to Use

[YouTube Video Link](https://www.youtube.com/watch?v=YrCQ0uRcNCk&list=PLZ9qNFMHZ-A4rycgrgOYma6zxF4BZGGPW&index=94)

- Non-gaussian features may be problematic, can we transform it to be more "gaussian" looking
  - $\log(x)$ transform
  - or $\log(x + 1)$ or $\log(x + c)$
  - $\sqrt{x}$ or $\sqrt[4]{x}$

# Video W9 07: Multivariate Gaussian Distribution

[YouTube Video Link](https://www.youtube.com/watch?v=fr5wML3y1Xk&list=PLZ9qNFMHZ-A4rycgrgOYma6zxF4BZGGPW&index=95)

# Video W9 08: Anomaly Detection using Multivariate Gaussian Distribution

[YouTube Video Link](https://www.youtube.com/watch?v=EobKYbaJAHM&list=PLZ9qNFMHZ-A4rycgrgOYma6zxF4BZGGPW&index=96)

```python
import sys
sys.path.append("../../src") # add our class modules to the system PYTHON_PATH

from ml_python_class.custom_funcs import version_information
version_information()
```

                  Module   Versions
    --------------------   ------------------------------------------------------------
             matplotlib:   ['3.3.0']
                  numpy:   ['1.18.5']
                 pandas:   ['1.0.5']
                  scipy:   ['1.5.1']
                seaborn:   ['0.10.1']


```python
import matplotlib.pyplot as plt
import scipy.stats as st
import seaborn as sns
import pandas as pd
from scipy.stats import norm, uniform, expon, t, probplot
import scipy.stats as st
from scipy.integrate import quad
from sympy.solvers import solve
from sympy import Symbol
import numpy as np
from pandas import Series, DataFrame
df = pd.read_csv("tanken.csv", sep=";")
```

```python
print(f'arithmetisches Mittel: {float(df["km.driven"].mean())}')
print(f'empirische Varianz: {float(df["km.driven"].var())}')
print(f'Standardabweichung: {float(df["km.driven"].std())}')
print(f'Median: {float(df["km.driven"].median())}')
```

    arithmetisches Mittel: 560.4307692307692
    empirische Varianz: 7611.4723076923065
    Standardabweichung: 87.24375225591977
    Median: 576.2

```python
df.sort_values(by="km.driven")
df.head()
```

      km.driven
      filled.gas.in.litre
      Price.Per.Litre
      Bon.per.Litre
      costs
      Used.Gas.per.100.km
      Absolut.Reduction

      Date

      21.3.2019
      580.3
      41.50
      1.44
      0.04
      58.10
      7.15
      1.6600

      14.4.2019
      568.2
      42.75
      1.61
      0.04
      67.12
      7.52
      1.7100

      5.4.2019
      605.2
      41.64
      1.58
      0.05
      63.71
      6.88
      2.0820

      19.6.2019
      659.1
      42.96
      1.66
      0.04
      69.60
      6.52
      1.7184

      18.5.2019
      629.8
      42.75
      1.61
      0.05
      66.69
      6.79
      2.1375

```python
df["km.driven"]
```

    Date
    21.3.2019     580.3
    14.4.2019     568.2
    5.4.2019      605.2
    19.6.2019     659.1
    18.5.2019     629.8
    1.7.2019      322.3
    9.8.2019      518.8
    27.8.2019     600.4
    14.9.2019     514.2
    1.10.2019     576.2
    11.10.2019    643.7
    5.11.2019     575.8
    11.12.2019    491.6
    Name: km.driven, dtype: float64

```python
print(f'0.25% Quantil ist: {df["km.driven"].quantile(q=0.25)}')
print(f'0.50% Quantil ist: {df["km.driven"].quantile(q=0.50)}')
print(f'0.75% Quantil ist: {df["km.driven"].quantile(q=0.75)}')

```

    0.25% Quantil ist: 518.8
    0.50% Quantil ist: 576.2
    0.75% Quantil ist: 605.2

```python
q25, q75 = df["km.driven"].quantile(q=[.25,.75])
print(f'Die Quantilsdifferenz ist: {q75 - q25}')
```

    Die Quantilsdifferenz ist: 86.40000000000009

```python
# mit linspace siehts auch no interessant aus
q25, q50, q75 = df["km.driven"].quantile(q=np.linspace(start=0.25, stop=0.75, num=3))
print(f'0.25% Quantil ist: {q25}')
print(f'0.50% Quantil ist: {q50}')
print(f'0.75% Quantil ist: {q75}')
```

    0.25% Quantil ist: 518.8
    0.50% Quantil ist: 576.2
    0.75% Quantil ist: 605.2

```python
df["km.driven"].plot(kind="hist", bins=7, edgecolor="black")
plt.title("Histogramm Tankfüllungen")
plt.xlabel("KM pro Füllung")
plt.ylabel("Häufigkeit")
plt.show()
```

```python
df["km.driven"].plot(kind="hist", bins=7, normed=True, edgecolor="black")
plt.title("Histogramm Tankfüllungen")
plt.xlabel("KM pro Füllung")
plt.ylabel("Häufigkeit")
plt.show()
```

```python
# Boxplot
df["km.driven"].plot(kind="box", title="Boxplot Tankfüllungen", label="Tankung")
plt.ylabel("KM pro Füllung")
plt.show()
```

```python
# empirische kumulative Verteilungsfunktion
df["km.driven"].plot(kind="hist", cumulative=True, histtype="step",
normed=True, bins=8, edgecolor="black")
```

```python
from scipy.stats import norm, uniform
uniform.cdf(x=5, loc=4, scale=4)
```

    0.25

```python
data = uniform.rvs(loc=1, scale=9, size=50000)
print(((data <= 10) & (data >= 1)).astype(np.int).prod())
```

    1

```python
# empirische kumulative Verteilungsfunktion
df["km.driven"].plot(kind="hist", cumulative=True, histtype="step",
normed=True, bins=8, edgecolor="black")
```

```python
from scipy.stats import norm, uniform
uniform.cdf(x=5, loc=4, scale=4)
```

    0.25

```python
data = uniform.rvs(loc=1, scale=9, size=50000)
print(((data <= 10) & (data >= 1)).astype(np.int).prod())
```

    1

```python
df.head()
```

      Date
      km.driven
      filled.gas.in.litre
      Price.Per.Litre
      Bon.per.Litre
      costs
      Used.Gas.per.100.km
      Absolut.Reduction

      0
      21.3.2019
      580.3
      41.50
      1.44
      0.04
      58.10
      7.15
      1.6600

      1
      14.4.2019
      568.2
      42.75
      1.61
      0.04
      67.12
      7.52
      1.7100

      2
      5.4.2019
      605.2
      41.64
      1.58
      0.05
      63.71
      6.88
      2.0820

      3
      19.6.2019
      659.1
      42.96
      1.66
      0.04
      69.60
      6.52
      1.7184

      4
      18.5.2019
      629.8
      42.75
      1.61
      0.05
      66.69
      6.79
      2.1375

```python
df["Date"] = pd.to_datetime(df["Date"], format="%d.%m.%y")
```

```python
df["Date"]
```

    0      21.3.2019
    1      14.4.2019
    2       5.4.2019
    3      19.6.2019
    4      18.5.2019
    5       1.7.2019
    6       9.8.2019
    7      27.8.2019
    8      14.9.2019
    9      1.10.2019
    10    11.10.2019
    11     5.11.2019
    12    11.12.2019
    Name: Date, dtype: object

```python

```


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

# Class 17: Introduction to New-Keynesian Business Cycle Modeling

In this notebook, we will use `linearsolve` to compute impulse responses of output, inflation, and the nominal interest rate to an aggregate demand shock in the New-Keynesian model.

## The New-Keynesian Model

The most basic version of the New-Keynesian Model can be expressed as:

\begin{align}
y_t & = E_t y_{t+1}  - \left( r_{t} - \bar{r}\right) + g_t\\
i_{t} & = r_{t} + E_t \pi_{t+1}\\
i_{t} & = \bar{r} + \pi^T + \phi_{\pi}\big(\pi_t - \pi^T\big) + \phi_{y}\big(y_t - \bar{y}\big) + v_t\\
\pi_t -\pi^T & = \beta \left( E_t\pi_{t+1} - \pi^T\right)  + \kappa (y_t -\bar{y})+ u_t,
\end{align}

where: $y_t$ is (log) output, $r_t$ is the real interest rate, $i_t$ is the nominal interest rate, $\pi_t$ is the rate of inflation between periods $t-1$ and $t$, $\bar{r}$ is the long-run average real interest rate or the *natural rate of interest*, $\beta$ is the household's subjective discount factor, and $\pi^T$ is the central bank's inflation target. The coeffieints $\phi_{\pi}$ and $\phi_{y}$ reflect the degree of intensity to which the central bank *endogenously* adjusts the nominal interest rate in response to movements in inflation and output.

The variables $g_t$, $u_t$, and $v_t$ represent exogenous shocks to aggregate demand, inflation, and monetary policy. They follow AR(1) processes:

\begin{align}
g_{t+1} & = \rho_g g_{t} + \epsilon^g_{t+1}\\
u_{t+1} & = \rho_u u_{t} + \epsilon^u_{t+1}\\
v_{t+1} & = \rho_v v_{t} + \epsilon^v_{t+1}.
\end{align}

The goal is to compute impulse responses in the model to a one percent exogenous increase in the nominal interest rate. We will use the following parameterization:

| $$\bar{y}$$ | $$\beta$$ | $$\bar{r}$$    | $$\kappa$$ | $$\pi^T$$ | $$\phi_{\pi}$$ | $$\phi_y$$ | $$\rho_g$$ | $$\rho_u$$ | $$\rho_v$$ |
|-------------|-----------|----------------|------------|-----------|----------------|------------|------------|------------|------------|
| 0           | 0.995     | $$-\log\beta$$ | 0.25        | 0.02/4    | 1.5            | 0.5/4      | 0.5        | 0.5        | 0.5        |

```python
# Create a variable called 'parameters' that stores the model parameter values in a Pandas Series
parameters = pd.Series(dtype=float)
parameters['y_bar'] = 0
parameters['beta'] = 0.995
parameters['r_bar'] = -np.log(parameters.beta)
parameters['kappa'] = 0.25
parameters['pi_T'] = 0.02/4
parameters['phi_pi'] = 1.5
parameters['phi_y'] = 0.5/4
parameters['rho_g'] = 0.5
parameters['rho_u'] = 0.5
parameters['rho_v'] = 0.5

# Print the model's parameters
print(parameters)
```

    y_bar     0.000000
    beta      0.995000
    r_bar     0.005013
    kappa     0.250000
    pi_T      0.005000
    phi_pi    1.500000
    phi_y     0.125000
    rho_g     0.500000
    rho_u     0.500000
    rho_v     0.500000
    dtype: float64

```python
# Create variable called 'var_names' that stores the variable names in a list with state variables ordered first
var_names = ['g','u','v','y','pi','i','r']

# Create variable called 'shock_names' that stores an exogenous shock name for each state variable.
shock_names = ['e_g','e_u','e_v']

# Define a function that evaluates the equilibrium conditions of the model solved for zero. PROVIDED
def equilibrium_equations(variables_forward,variables_current,parameters):

    # Parameters. PROVIDED
    p = parameters

    # Current variables. PROVIDED
    cur = variables_current

    # Forward variables. PROVIDED
    fwd = variables_forward

    # IS equation
    is_equation = fwd.y - (cur.r -p.r_bar) + cur.g - cur.y

    # Fisher_equation
    fisher_equation = cur.r + fwd.pi - cur.i

    # Monetary policy
    monetary_policy = p.r_bar + p.pi_T + p.phi_pi*(cur.pi - p.pi_T) + p.phi_y*cur.y + cur.v - cur.i

    # Phillips curve
    phillips_curve = p.beta*(fwd.pi- p.pi_T) + p.kappa*cur.y + cur.u - (cur.pi-p.pi_T)

    # Demand process
    demand_process = p.rho_g*cur.g - fwd.g

    # Monetary policy process
    monetary_policy_process = p.rho_v*cur.v - fwd.v

    # Inflation process
    inflation_process = p.rho_u*cur.u - fwd.u

    # Stack equilibrium conditions into a numpy array
    return np.array([
            is_equation,
            fisher_equation,
            monetary_policy,
            phillips_curve,
            demand_process,
            monetary_policy_process,
            inflation_process
        ])

# Initialize the model into a variable named 'nk_model'
nk_model = ls.model(equations = equilibrium_equations,
                 n_states=3,
                 var_names=var_names,
                 shock_names=shock_names,
                 parameters = parameters)
```

```python
# Compute the steady state numerically using .compute_ss() method of nk_model
guess = [0,0,0,0,0.01,0.01,0.01]
nk_model.compute_ss(guess)

# Print the computed steady state
print(nk_model.ss)
```

    g     1.379068e-34
    u     2.578505e-34
    v    -2.700350e-34
    y     3.705259e-20
    pi    5.000000e-03
    i     1.001254e-02
    r     5.012542e-03
    dtype: float64

```python
# Find the log-linear approximation around the non-stochastic steady state and solve using .approximate_and_solve() method of nk_model
# set argumement 'log_linear' to False because the model is already log-linear.
nk_model.approximate_and_solve(log_linear=False)

print(nk_model.approximated())
```

    Linear equilibrium conditions:

           y[t+1|t] = -g[t]+y[t]+r[t]

          pi[t+1|t] = i[t]-r[t]

                  0 = -v[t]-0.125·y[t]-1.5·pi[t]+i[t]

    0.995·pi[t+1|t] = -u[t]-0.25·y[t]+pi[t]

            -g[t+1] = -0.5·g[t]

            -v[t+1] = -0.5·v[t]

            -u[t+1] = -0.5·u[t]

### Impulse Responses

Compute a 21 period impulse response of the model's variables to a 0.01 unit shock to the aggregate demand for goods ($g_t$) in period 5.

```python
# Compute impulse responses
nk_model.impulse(T=21,t0=5,shocks=[0.01,0,0])

# Print the first 10 rows of the computed impulse responses to the demand shock
print(nk_model.irs['e_g'].head(10))
```

        e_g         g    u    v         y        pi         i         r
    0  0.00  0.000000  0.0  0.0  0.000000  0.000000  0.000000  0.000000
    1  0.00  0.000000  0.0  0.0  0.000000  0.000000  0.000000  0.000000
    2  0.00  0.000000  0.0  0.0  0.000000  0.000000  0.000000  0.000000
    3  0.00  0.000000  0.0  0.0  0.000000  0.000000  0.000000  0.000000
    4  0.00  0.000000  0.0  0.0  0.000000  0.000000  0.000000  0.000000
    5  0.01  0.010000  0.0  0.0  0.008909  0.004432  0.007762  0.005546
    6  0.00  0.005000  0.0  0.0  0.004454  0.002216  0.003881  0.002773
    7  0.00  0.002500  0.0  0.0  0.002227  0.001108  0.001940  0.001386
    8  0.00  0.001250  0.0  0.0  0.001114  0.000554  0.000970  0.000693
    9  0.00  0.000625  0.0  0.0  0.000557  0.000277  0.000485  0.000347

Plot the computed impulses responses of the nominal interest rate, the real interest rate, output, and inflation. Express inflation and interest rates in *annualized* (e.g., multiplied by 4) terms.

```python
# Create figure. PROVIDED
fig = plt.figure(figsize=(12,8))

# Create upper-left axis. PROVIDED
ax1 = fig.add_subplot(2,2,1)
# Create upper-right axis. PROVIDED
ax2 = fig.add_subplot(2,2,2)
# Create lower-left axis. PROVIDED
ax3 = fig.add_subplot(2,2,3)
# Create lower-right axis. PROVIDED
ax4 = fig.add_subplot(2,2,4)

# Set axis 1 ylabel
ax1.set_ylabel('% dev from steady state')
# Set axis 2 ylabel
ax2.set_ylabel('% dev from steady state')
# Set axis 3 ylabel
ax3.set_ylabel('% dev from steady state')
# Set axis 4 ylabel
ax4.set_ylabel('% dev from steady state')

# Set axis 1 limits
ax1.set_ylim([-0.5,3.5])
# Set axis 2 limits
ax2.set_ylim([-0.5,3.5])
# Set axis 3 limits
ax3.set_ylim([-0.5,2])
# Set axis 4 limits
ax4.set_ylim([-0.5,2])

# Plot the nominal interest rate, real interest rate, output, and inflation
(nk_model.irs['e_g']['i']*400).plot(ax=ax1,lw=4,alpha=0.75,title='Nominal Interest (annualized)',grid=True)
(nk_model.irs['e_g']['r']*400).plot(ax=ax2,lw=4,alpha=0.75,title='Real Interest (annualized)',grid=True)
(nk_model.irs['e_g']['y']*100).plot(ax=ax3,lw=4,alpha=0.75,title='Output',grid=True)
(nk_model.irs['e_g']['pi']*400).plot(ax=ax4,lw=4,alpha=0.75,title='Inflation (annualized)',grid=True)
```


#  Estimation of Orbital Parameters of Extra-solar Planets from Radial Velocities using Markov Chain Monte Carlo (MCMC)

## MCMC: A Conceptual Introduction

Monte Carlo (MC) methods have become a cornerstone of present-day scientific analyses by providing a simple approach for obtaining information about distributions, especially for estimating posterior distributions in Bayesian inference using samples from a continuous random variable. They comprise of a class of algorithms for sampling from a desired probability distribution. The sample of the distribution are obtained by constructing a chain that has the probability distribution as its stationary distribution followed by registering and saving the states from the chain.

The samples created by the Markov chain have probability density proportional to a known function. These large number of samples drawn are used to estimate the properties of the distribution (such as its expected value or variance). Practically, different sets of samples are created using multiple chains (an ensemble of chains), starting from a set of points randomly chosen and sufficiently separated from each other in the sample space.

These stochastic chains comprise of **walkers** which explores the parameter space in random directions according to an algorithm that looks for places with a reasonably high contribution to the properties of the probability distribution to move to the next, with higher probabilities than the direct predecessor.

MCMC is essentially a *sampler* as it explores the parameter space. The fundamental process of running a Markov chain is to compare the **models** generated by the chains against the **data** while moving around the parameter space. The objective is to determine the set of parameters that produces the best fit model of our data. The process is inherently Bayesian as opposed to *frequentist* as it requires a prior knowledge of the system in terms of the parameters. For example, constructing a stellar model can contain temperature as a parameter with a prior that the temperature at the core is within a certain range, as fusion reactions can only occur above certain temperatures.

## Bayesian Inference

We shall use the MCMC technique to know more about characteristics of the planetary system, 51 Pegasi (HD 217014). The radial velocity (RV) data was obtained from NASA Exoplanet Archive which is maintained by the [NASA Exoplanet Science](https://exoplanetarchive.ipac.caltech.edu/). Let the data be denoted by **DRV** of which we are interested to make inferences.

We want to interpret the RV data in light of an underlying model **M** which is a function of a set of parameters, **$\Theta$M**, which can predict about the proximity of the data to the model. We can, therefore, estimate the conditional probability *P* (**DRV** | **$\Theta$M**, **M**) that we would obtain the data, **DRV**, from our model using a particular set of parameters, **$\Theta$M**. Alternatively, given our model **M**, we can ask about the likelihood of the parameter **$\Theta$M** such that the assumed model completely explains our data.

In Bayesian statistical inference, we are interested in determining the quantity, *P* (**$\Theta$M** | **DRV**, **M**). It is the probability that the parameters are actually **$\Theta$M** under the assumed model given our data, **DRV**. The two conditional probabilities are related as:

 \begin{equation} \it{P}\, \mathrm{(\Theta_M\,|\,D_{RV}, M)}\; \it{P}\, \mathrm{(D_{RV}\,|\,M)} = \it{P}\, \mathrm{(\Theta_M, D_{RV}\,|\,M)} = \it{P}\, \mathrm{(D_{RV}\,|\,\Theta_M, M)}\;\, \it{P}\, \mathrm{(\Theta_M\,|\,M)} \mathrm{\tag{1}}\end{equation}

where $\it{P}\, \mathrm{(\Theta_M, D_{RV}\,|\,M)}$ represents the joint probability of obtaining the parameter, **$\Theta$M**, such that the data, **DRV**, is observed. From Bayes' theorem, the two conditional probabilities can be rearranged as:

 \begin{equation} \it{P}\, \mathrm{(\Theta_M\,|\,D_{RV}, M)} = \frac {\it{P}\, \mathrm{(D_{RV}\,|\,\Theta_M, M)}\;\, \it{P}\, \mathrm{(\Theta_M\,|\,M)}}{\it{P}\, \mathrm{(D_{RV}\,|\,M)}}\mathrm{\tag{2}}\end{equation}

The MCMC analysis usually starts with selection of a set of parameter values, often referred to as **priors** - $\it{P}\, \mathrm{(\Theta_M\,|\,M)}$. As the name suggests, the values are chosen based on previous measurements, physical conditions ,and other known constraints. The denominator, $\it{P}\, \mathrm{(D_{RV}\,|\,M)}$, is termed as **evidence**. It describes how well our model explains our data after averaging over the complete range of the parameter set. The model is said to be good if it matches  our data to a large extent. $\it{P}\, \mathrm{(\Theta_M\,|\,D_{RV}, M)}$ represents the **posterior**. This is a measure of our belief in the best parameter after combining our prior values with the current observations and normalizing by the evidence over the parameter space.

The above process is summarized below.

 - With the set of input parameters as variables, write a function that specifies our model.
 - Set up an ensemble of walkers defined by the parameters **$\Theta$M**.
 - A grid containing the values of different parameters over their respective ranges is generated.
 - Every walker will now begin exploring the parameter space. To do this, each walker takes a step to a new value of parameters and generates a model with that set of parameters. It then compares the model to the dataset, usually through minimizing the $\chi^2$ or maximizing the log-likelihood function.
 \begin{equation} \mathrm{Likelihood} = -\frac{1}{2} \sum_{Data}^{}{\mathrm{ln}\;\{2\pi\;({y_{Error})}^{2}}\}-\frac{1}{2} \sum_{Data}^{}{\left(\frac{y_{Data}-y_{Model}}{y_{Error}}\right)^{2}}\mathrm{\tag{3}}\end{equation}
 - The MCMC then determines the ratio of the likelihood generated by the model with the new set of parameters to data and compares it with the previous set of parameter. If the new location produces a better ratio, the walker moves there and repeats the process. If the new location is worse than the earlier, it retreats to its previous position and looks for a new direction.
 - The walkers move around by exploring the parameter space and terminates its journey in the region of maximum likelihood.

A posterior distribution is generated at the end of the process (also termed as the **production run**).If the MCMC runs a sufficient number of steps,it converges to the region of maximum likelihood.

## Fitting the Exoplanet's Radial Velocity

The RV data of 51 Pegasi can be downloaded from the Exoplanet archive. We are interested in the RV values with thier errors as a function of the Julian Date. For representation purpose, we shall phase-fold the time axis.

```python
#!/usr/bin/env python3
import numpy as np
import scipy as sp
import pandas as pd
import matplotlib.pyplot as plt
import math
import emcee
import corner
import pickle
import tkinter as tk
from tkinter import simpledialog
from tkinter.simpledialog import askinteger
import easygui
from IPython.display import display, Math

plt.rcParams['figure.figsize'] = [12,7]
```

```python
Data = pd.read_table('UID_0113357_RVC_005.dat',sep = '\s+',skiprows=22, header = None, index_col = None)
print(Data)
RV_Data = np.array(Data)
HJD = RV_Data[:,[0]]
Rad_Vel = RV_Data[:,[1]]
RV_Error = RV_Data[:,[2]]
plt.errorbar(HJD, Rad_Vel, RV_Error, color='black', fmt='o', capsize=5, capthick=1, ecolor='black')
plt.xlabel('Heliocentric Julian Date (Time)')
plt.ylabel('Radial Velocity')
plt.grid()
plt.show(block=False)
len_HJD = len(HJD)
Phase_Folded = np.empty((len_HJD, len_HJD))
fig = plt.figure()
for i in range(0,len_HJD):
    t = HJD
    T0 = HJD[i]
    P = 4.230785
    PF = ((t-T0)/P)-np.floor((t-T0)/P)
    Phase_Folded[:,[i]] = PF

root =tk.Tk()
canvas1 = tk.Canvas(root, width = 400, height = 200,  relief = 'raised')
canvas1.pack()

label1 = tk.Label(root, text='Select the Reference Time (Fiducial)')
label1.config(font=('helvetica', 14))
canvas1.create_window(200, 25, window=label1)

label2 = tk.Label(root, text='Input the n\u1D57\u02B0 HJD to be taken as the reference \n\n(n\u2208Z\u207a; 1 \u2264 n \u2264 202):')
label2.config(font=('helvetica', 10))
canvas1.create_window(200, 95, window=label2)
mystring =tk.IntVar(root)
def close_window():
    root.destroy()
e1 = tk.Entry(root,textvariable = mystring,width=25,fg="black",bd=3,selectbackground='gray').place(x=100, y=120)
button1 = tk.Button(root,
                text='Submit',
                fg='White',
                bg= '#000000',height = 1, width = 10,command=close_window).place(x=150, y=170)
root.mainloop()
j = mystring.get()
j = j-1
plt.errorbar(2*np.pi*Phase_Folded[:,[j]], Rad_Vel, RV_Error, color='black', fmt='o', capsize=5, capthick=1, ecolor='black')
plt.xlabel('Phase Folded (Time)')
plt.ylabel('Radial Velocity')
plt.grid()
plt.show(block=False)
x_data = Phase_Folded[:,[j]]
x_model = np.linspace(0, 1, 5000)
```

The radial velocity curve repeats over time and it appears to be a sinusoid. The RV method for detecting exoplanets relies on the fact that a star does not remain completely stationary when it is orbited by a planet. The star moves, ever so slightly, in a small circle or ellipse, responding to the gravitational pull of its smaller companion. When viewed from a distance, these slight movements affect the star's normal light spectrum, or color signature. The spectrum of a star that is moving towards the observer appears slightly shifted toward bluer (shorter) wavelengths. If the star is moving away, then its spectrum is shifted toward redder (longer) wavelengths.

We define our model to be of the form
$$
\mathrm{RV} = \mathrm{y_0} + \mathrm{A_0\;cos(2\,\pi\,t + \Phi_0)}\mathrm{\tag{4}}
$$
where $\mathrm{y_0, A_0,}$ and $\mathrm{\Phi_0}$ are the parameters to be fitted.

## Setting up the MCMC

The RV function is defined below:

```python
#
# Model
#
def rvmodel_mcmc(Parameters,x=x_model):
    y0, A0, Phi0 = Parameters
    return y0 + (A0*np.cos((2*np.pi*x)+Phi0))
```

We now need a log-likelihood function that estimates how good a fit the model is to the data for a given set of parameters, weighted by the RV error. The following function is used:

 \begin{equation} \mathrm{Likelihood} = -\frac{1}{2} \sum_{Data}^{}{\mathrm{ln}\;\{2\pi\;({y_{Error})}^{2}}\}-\frac{1}{2} \sum_{Data}^{}{\left(\frac{y_{Data}-y_{Model}}{y_{Error}}\right)^{2}}\end{equation}

```python
#
# Likelihood Function
#
def lnlike(Parameters,x,y,yerror):
    return -0.5*(np.sum((np.log(2*np.pi*(yerror**2)))+(((y-rvmodel_mcmc(Parameters,x))/yerror)**2)))
```

The next function we need is one to check, before running the probability function on any set of parameters, that all variables are within their priors. The lnprior function specifies a bounds on the parameters. The output of this function is just an encoding similar to a true or false.

```python
#
# Priors
#
def lnprior(Parameters):
    y0, A0, Phi0 = Parameters
    if -50.0 < y0 < 50.0 and 0.0 < A0 < 125.0 and 0.0 < Phi0 < 2*np.pi:
        return 0.0
    return -np.inf
```

The lnprob function combines the steps above by running the lnprior function, and if the function returned -np.inf, passing that through as a return, and if not, returning the lnlike for that model.

```python
#
# Bayesian Posterior Probability
#
def lnprob(Parameters,x,y,yerror):
    lp = lnprior(Parameters)
    if not np.isfinite(lp):
        return -np.inf
    return lnlike(Parameters,x,y,yerror)+lnprior(Parameters)
```

```python
data = (x_data,Rad_Vel,RV_Error)
nwalkers = 500
niter = 5000
initial = np.array([-2.0, 100.0, 3.0])
ndim = len(initial)
p0 = [np.array(initial) + 1e-7 * np.random.randn(ndim) for k in range(nwalkers)]
```

We are now ready to run the MCMC.

```python
def main(p0,nwalkers,niter,ndim,lnprob,data):
    sampler = emcee.EnsembleSampler(nwalkers, ndim, lnprob, args=data)

    print("Running burn-in...")
    p0, _, _ = sampler.run_mcmc(p0, 1000, progress=True)
    sampler.reset()

    print("Running production...")
    pos, prob, state = sampler.run_mcmc(p0, niter,progress=True)

    return sampler, pos, prob, state
```

The sampler here contains all the outputs of the MCMC, including the walker chains and the posteriors.

```python
sampler, pos, prob, state = main(p0,nwalkers,niter,ndim,lnprob,data)
```

    Running burn-in...

    100%|██████████| 1000/1000 [00:41<00:00, 24.09it/s]
      0%|          | 0/5000 [00:00<?, ?it/s]

    Running production...

    100%|██████████| 5000/5000 [03:27<00:00, 24.13it/s]

```python
def plotter(sampler,x=x_data,Rad_Vel=Rad_Vel):
#plt.ion()
    plt.errorbar(2*np.pi*x_data,Rad_Vel, RV_Error, color='black', fmt='o', capsize=5, capthick=1, ecolor='black')
    samples = sampler.flatchain
    for Parameters in samples[np.random.randint(len(samples), size=1000)]:
        plt.plot(2*np.pi*x_model, rvmodel_mcmc(Parameters,x_model), color='#800000',alpha=0.1)
    plt.xlabel('Phase Folded (Time)')
    plt.ylabel('Radial Velocity')
    plt.legend(["Random Parameter Samples from the Posterior Distribution"],fontsize=14)
    plt.grid()
    plt.show(block=False)
```

```python
plotter(sampler)
```

We can see that our perfectly fits our data. Also, the thousand samples drawn from the posteriors seem to overlap over each other quite well.

```python
samples = sampler.flatchain
samples[np.argmax(sampler.flatlnprobability)]
samples = sampler.flatchain
```

```python
theta_max  = samples[np.argmax(sampler.flatlnprobability)]
best_fit_model_mcmc = rvmodel_mcmc(theta_max)
plt.errorbar(2*np.pi*x_data,Rad_Vel, RV_Error, color='black', fmt='o', capsize=5, capthick=1, ecolor='black')
plt.plot(2*np.pi*x_model,best_fit_model_mcmc,color='#FFFF66', label="Monte Carlo Markov Chain Fit")
plt.xlabel('Phase Folded (Time)')
plt.ylabel('Radial Velocity')
plt.legend(fontsize=14)
plt.grid()
plt.show()
```

### Posterior Spread
The corner.py module is used to visualize 1D and 2D spreads between the tested parameters and to obtain the uncertainties on the parameter estimations.

```python
def sample_walkers(nsamples,flattened_chain):
    models = []
    draw = np.floor(np.random.uniform(0,len(flattened_chain),size=nsamples)).astype(int)
    Param = flattened_chain[draw]
    for l in Param:
        mod = rvmodel_mcmc(l)
        models.append(mod)
    spread = np.std(models,axis=0)
    med_model = np.median(models,axis=0)
    return med_model,spread
med_model, spread = sample_walkers(1000,samples)
```

```python
plt.errorbar(2*np.pi*x_data,Rad_Vel, RV_Error, color='black', fmt='o', capsize=5, capthick=1, ecolor='black')
plt.fill_between(2*np.pi*x_model,med_model-2*spread,med_model+2*spread,color='#FE891B',alpha=0.45,label=r'$2\sigma$ Posterior Spread')
plt.fill_between(2*np.pi*x_model,med_model-spread,med_model+spread,color='#00FF7F',alpha=0.3,label=r'$1\sigma$ Posterior Spread')
plt.plot(2*np.pi*x_model,best_fit_model_mcmc,color='#FFFF66', label="Monte Carlo Markov Chain Fit")
plt.xlabel('Phase Folded (Time)')
plt.ylabel('Radial Velocity')
plt.legend(fontsize=14)
plt.grid()
plt.show()
```

```python
labels = ['$y₀$', '$A₀$', '$𝛟₀$']
label = ['y₀', 'A₀', '𝛟₀']
print('\033[1m' + 'Uncertainties based on the 16th, 50th, and 84th percentiles of the samples in the marginalized distributions' + '\033[0m')

for m in range(ndim):
    mcmc = np.percentile(samples[:, m], [16, 50, 84])
    q = np.diff(mcmc)
    txt1 = "\mathrm{{{3}}} = {0:.9f}_{{-{1:.3f}}}^{{{2:.3f}}}"
    txt1 = txt1.format(mcmc[1], q[0], q[1], label[m])
    display(Math(txt1))

print('\033[1m' + 'Parameters based on the Highest Likelihood Model with data from the Posterior Distribution' + '\033[0m')

for m in range(ndim):
    txt2 = "\mathrm{{{1}}} = {0:.9f}"
    txt2 = txt2.format(theta_max[m], label[m])
    display(Math(txt2))
fig = corner.corner(samples,show_titles=True,labels=labels,smooth=True,plot_datapoints=True,quantiles=[0.16, 0.5, 0.84],levels=(1-np.exp(-0.5),1-np.exp(-0.5*4)))
fig.show()
```

The most likely model doesn't always lie at the center of this spread - the spread is around the median model, but the one that absolutely maximizes the likelihood might lie at the edge or even outside this region.

**Developed and designed by [Shubhonkar Paramanick](https://github.com/Shubhonkar-Paramanick)**


```python
import autogate
import sympy
```

### Givens Gate Decomposition

```python
t = sympy.Symbol('t', real=True)

circuit = autogate.Circuit()
circuit.add_gate(gate=autogate.GateLibrary.H(), time=0, qubits=(0,))
circuit.add_gate(gate=autogate.GateLibrary.H(), time=0, qubits=(1,))
circuit.add_gate(gate=autogate.GateLibrary.cZ(), time=1, qubits=(0, 1))
circuit.add_gate(gate=autogate.GateLibrary.Ry(+t/2), time=2, qubits=(0,))
circuit.add_gate(gate=autogate.GateLibrary.Ry(-t/2), time=2, qubits=(1,))
circuit.add_gate(gate=autogate.GateLibrary.cZ(), time=3, qubits=(0, 1))
circuit.add_gate(gate=autogate.GateLibrary.H(), time=4, qubits=(0,))
circuit.add_gate(gate=autogate.GateLibrary.H(), time=4, qubits=(1,))

print(circuit)
```

    T  : |0|1|2 |3|4|

    q0 : -H-@-Ry-@-H-
            |    |
    q1 : -H-Z-Ry-Z-H-

    T  : |0|1|2 |3|4|

```python
print(sympy.pretty(circuit.operator))
```

    ⎡1    0        0     0⎤
    ⎢                     ⎥
    ⎢0  cos(t)  -sin(t)  0⎥
    ⎢                     ⎥
    ⎢0  sin(t)  cos(t)   0⎥
    ⎢                     ⎥
    ⎣0    0        0     1⎦

### PX Gate Starting Point

```python
t = sympy.Symbol('t', real=True)

circuit = autogate.Circuit()
circuit.add_gate(gate=autogate.GateLibrary.oX(), time=0, qubits=(1, 0))
circuit.add_gate(gate=autogate.GateLibrary.oX(), time=0, qubits=(2, 3))
circuit.add_gate(gate=autogate.ControlledGate(controls=[True]*2,
        gate=autogate.GateLibrary.G(t=t)), time=1, qubits=(0, 3, 1, 2))
circuit.add_gate(gate=autogate.GateLibrary.oX(), time=2, qubits=(1, 0))
circuit.add_gate(gate=autogate.GateLibrary.oX(), time=2, qubits=(2, 3))

print(circuit)
```

    T  : |0|1 |2|

    q0 : -X-@--X-
          | |  |
    q1 : -O-G0-O-
            |
    q2 : -O-G1-O-
          | |  |
    q3 : -X-@--X-

    T  : |0|1 |2|

```python
print(sympy.pretty(circuit.operator))
```

    ⎡1  0  0    0     0  0  0  0  0  0  0  0     0     0  0  0⎤
    ⎢                                                         ⎥
    ⎢0  1  0    0     0  0  0  0  0  0  0  0     0     0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  1    0     0  0  0  0  0  0  0  0     0     0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0  cos(t)  0  0  0  0  0  0  0  0  -sin(t)  0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0    0     1  0  0  0  0  0  0  0     0     0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0    0     0  1  0  0  0  0  0  0     0     0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0    0     0  0  1  0  0  0  0  0     0     0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0    0     0  0  0  1  0  0  0  0     0     0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0    0     0  0  0  0  1  0  0  0     0     0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0    0     0  0  0  0  0  1  0  0     0     0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0    0     0  0  0  0  0  0  1  0     0     0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0    0     0  0  0  0  0  0  0  1     0     0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0  sin(t)  0  0  0  0  0  0  0  0  cos(t)   0  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0    0     0  0  0  0  0  0  0  0     0     1  0  0⎥
    ⎢                                                         ⎥
    ⎢0  0  0    0     0  0  0  0  0  0  0  0     0     0  1  0⎥
    ⎢                                                         ⎥
    ⎣0  0  0    0     0  0  0  0  0  0  0  0     0     0  0  1⎦

### Orbital Rotation Tomography

```python
t = sympy.Symbol('t', real=True)

circuit = autogate.Circuit()
circuit.add_gate(gate=autogate.GateLibrary.G(t), time=0, qubits=(0, 2))
circuit.add_gate(gate=autogate.GateLibrary.G(t), time=0, qubits=(1, 3))
print(circuit)
```

    T  : |0    |

    q0 : -G0----
          |
    q1 : -|--G0-
          |  |
    q2 : -G1-|--
             |
    q3 : ----G1-

    T  : |0    |

```python
print(sympy.pretty(circuit.operator))
```

    ⎡1    0       0        0         0     0      0         0        0         0
    ⎢
    ⎢0  cos(t)    0        0      -sin(t)  0      0         0        0         0
    ⎢
    ⎢0    0     cos(t)     0         0     0      0         0     -sin(t)      0
    ⎢
    ⎢                      2                  -sin(2⋅t)                    -sin(2⋅
    ⎢0    0       0     cos (t)      0     0  ──────────    0        0     ───────
    ⎢                                             2                            2
    ⎢
    ⎢0  sin(t)    0        0      cos(t)   0      0         0        0         0
    ⎢
    ⎢0    0       0        0         0     1      0         0        0         0
    ⎢
    ⎢                   sin(2⋅t)                  2                             2
    ⎢0    0       0     ────────     0     0   cos (t)      0        0      -sin (
    ⎢                      2
    ⎢
    ⎢0    0       0        0         0     0      0       cos(t)     0         0
    ⎢
    ⎢0    0     sin(t)     0         0     0      0         0     cos(t)       0
    ⎢
    ⎢                   sin(2⋅t)                   2                           2
    ⎢0    0       0     ────────     0     0   -sin (t)     0        0      cos (t
    ⎢                      2
    ⎢
    ⎢0    0       0        0         0     0      0         0        0         0
    ⎢
    ⎢0    0       0        0         0     0      0         0        0         0
    ⎢
    ⎢                      2                   sin(2⋅t)                     sin(2⋅
    ⎢0    0       0     sin (t)      0     0   ────────     0        0      ──────
    ⎢                                             2                            2
    ⎢
    ⎢0    0       0        0         0     0      0       sin(t)     0         0
    ⎢
    ⎢0    0       0        0         0     0      0         0        0         0
    ⎢
    ⎣0    0       0        0         0     0      0         0        0         0

         0    0         0          0        0     0⎤
                                                   ⎥
         0    0         0          0        0     0⎥
                                                   ⎥
         0    0         0          0        0     0⎥
                                                   ⎥
    t)                  2                          ⎥
    ───  0    0      sin (t)       0        0     0⎥
                                                   ⎥
                                                   ⎥
         0    0         0          0        0     0⎥
                                                   ⎥
         0    0         0          0        0     0⎥
                                                   ⎥
                    -sin(2⋅t)                      ⎥
    t)   0    0     ──────────     0        0     0⎥
                        2                          ⎥
                                                   ⎥
         0    0         0       -sin(t)     0     0⎥
                                                   ⎥
         0    0         0          0        0     0⎥
                                                   ⎥
                    -sin(2⋅t)                      ⎥
    )    0    0     ──────────     0        0     0⎥
                        2                          ⎥
                                                   ⎥
         1    0         0          0        0     0⎥
                                                   ⎥
         0  cos(t)      0          0     -sin(t)  0⎥
                                                   ⎥
    t)                  2                          ⎥
    ──   0    0      cos (t)       0        0     0⎥
                                                   ⎥
                                                   ⎥
         0    0         0       cos(t)      0     0⎥
                                                   ⎥
         0  sin(t)      0          0     cos(t)   0⎥
                                                   ⎥
         0    0         0          0        0     1⎦

```python

```