# [IAPR][iapr]: Lab 3 ‒  Classification

**Group ID:** 36

**Author 1 (sciper):** Ke Wang (326760)
**Author 2 (sciper):** Jiaan Zhu (325421)
**Author 3 (sciper):** Rémy Dornier (271174)

**Release date:** 23.04.2021
**Due date:** 07.05.2021

## Important notes

The lab assignments are designed to teach practical implementation of the topics presented during class well as preparation for the final project, which is a practical project which ties together the topics of the course.

As such, in the lab assignments/final project, unless otherwise specified, you may, if you choose, use external functions from image processing/ML libraries like opencv and sklearn as long as there is sufficient explanation in the lab report. For example, you do not need to implement your own edge detector, etc.

**! Before handling back the notebook !** rerun the notebook from scratch `Kernel` > `Restart & Run All`

[iapr]: https://github.com/LTS5/iapr

## Extract relevant data
We first need to extract the `lab-03-data.tar.gz` archive.
To this end, we use the [tarfile] module from the Python standard library.

[tarfile]: https://docs.python.org/3.6/library/tarfile.html

```python
import tarfile
import os

data_base_path = os.path.join(os.pardir, 'data')
data_folder = 'lab-03-data'
tar_path = os.path.join(data_base_path, data_folder + '.tar.gz')
with tarfile.open(tar_path, mode='r:gz') as tar:
    tar.extractall(path=data_base_path)
```

---
## Part 1
In this part, we will study classification based on the data available in the Matlab file `classification.mat` that you will under `lab-03-data/part1`.
There are 3 data sets in this file, each one being a training set for a given class.
They are contained in variables `a`, `b` and `c`.

**Note**: we can load Matlab files using the [scipy.io] module.

[scipy.io]: https://docs.scipy.org/doc/scipy/reference/io.html

```python
import scipy.io

data_part1_path = os.path.join(data_base_path, data_folder, 'part1', 'classification.mat')
matfile = scipy.io.loadmat(data_part1_path)
a = matfile['a']
b = matfile['b']
c = matfile['c']

print(a.shape, b.shape, c.shape)
```

    (200, 2) (200, 2) (200, 2)

### 1.1 Bayes method (5 pts)
Using the Bayes method, give the analytical expression of the separation curves between those three classes.
Do reasonable hypotheses about the distributions of those classes and estimate the corresponding parameters based on the given training sets.
Draw those curves on a plot, together with the training data.
For simplicity reasons, round the estimated parameters to the closest integer value.

```python
import numpy as np
import matplotlib.pyplot as plt
from itertools import cycle, islice
from sympy import *

# plot the given data set
plt.figure(figsize = (8,6))
plt.plot(a[:,0], a[:,1], color = 'r', linestyle = '', marker = '+', label = 'a')
plt.plot(b[:,0], b[:,1], color = 'g', linestyle = '', marker = '+', label = 'b')
plt.plot(c[:,0], c[:,1], color = 'b', linestyle = '', marker = '+', label = 'c')
plt.legend(loc = 'lower right')
plt.title('Dataset')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.show()
```

#### Hypothsis

We make the following hypothesis: the sample points from each group are independently Gaussian distributed

We use Henze-Zirkler multivariate normality test to test whether a sample differs from a normal distribution. The null hypothesis that a sample comes from a normal distribution.

As shown below, P values are all greater than 0.05, so three groups of samples are all normal distributions.

```python
from pingouin import multivariate_normality

print(multivariate_normality(a, alpha=.05))
print(multivariate_normality(b, alpha=.05))
print(multivariate_normality(c, alpha=.05))
```

    HZResults(hz=0.5393399744748284, pval=0.6484443193667846, normal=True)
    HZResults(hz=0.5990511126254039, pval=0.5224272512722594, normal=True)
    HZResults(hz=0.43580364906111724, pval=0.8509771218027213, normal=True)

#### "Discriminant_analytic" function

Returns the corresponding analytic discriminant function $g_i(x,y)$ according to the chosen distance metrics.

- full : $g_i(\textbf{x}) = -\frac{1}{2}(\textbf{x}-\boldsymbol{\mu_i})^T\boldsymbol{\Sigma_i^{-1}}(\textbf{x}-\boldsymbol{\mu_i}) + \ln p(w_i) + \ln \frac{1}{2\pi|\boldsymbol{\Sigma_i}|^{1/2}}$

- mahanalobis = $g_i(\textbf{x}) =[(\textbf{x}-\boldsymbol{\mu_i})^T\boldsymbol{\Sigma_i^{-1}}(\textbf{x}-\boldsymbol{\mu_i})]^{1/2}$

- euclidean = $g_i(\textbf{x}) = ||\textbf{x}-\boldsymbol{\mu_i}||$

#### inputs

    - mu : mean vector of class i
    - cov : covariance matrix of class i
    - prob : probability of the class i
    - distance : type of discriminant function : 'full', 'mahanalobis' or 'euclidean'

#### outputs

    - g : the analytic disciminant function of class i

```python
def Discriminant_analytic(mu, cov, prob, distance):
    x, y = symbols('x y')
    z = np.array([x, y])

    if distance == 'full':
        cst = np.log(prob) + np.log(1 / (2 * np.pi * pow(abs(np.linalg.det(cov)), 0.5)))
        g = - 0.5 * np.dot(z - mu, np.linalg.inv(cov)).dot((z - mu).T) + cst
    if distance == "mahanalobis":
        g = (np.dot(z - mu, np.linalg.inv(cov)).dot((z - mu).T))**(1/2)
    if distance == 'euclidean':
        g = ((z - mu).dot((z - mu).T))**(1/2)

    return g
```

#### "boundary_equation" function

From 2 classes (i and j), it computes the analytical expression of the decision boundary quatrics as well as the analytical expression for the discriminant function $G(x,y) = g_i(x,y) - g_j(x,y)$.

#### inputs

    - data_set_1 : first class
    - data_set_2 : second class
    - nb_point_data_set : total number of points in the entire data set
    - distance : type of discriminant function : 'full', 'mahanalobis' or 'euclidean'

#### outputs

    - boundary : analytical expression of the decision boundary quatric, expressed in function y = f(x)
    - eq : analytical expression of the discriminant function G(x,y)

```python
def boundary_equation(data_set_1, data_set_2, nb_point_data_set, distance):
        # first class mean vector, covariance matrix and probability
        mu1 = np.round(np.mean(data_set_1, 0))
        cov1 = np.round(np.cov(np.transpose(data_set_1)))
        prob1 = data_set_1[:,0].size / nb_point_data_set

        # second class mean vector, covariance matrix and probability
        mu2 = np.round(np.mean(data_set_2, 0))
        cov2 = np.round(np.cov(np.transpose(data_set_2)))
        prob2 = data_set_2[:,0].size / nb_point_data_set

        # compute the analytical expression of the discriminant function for each class
        eq1 = Discriminant_analytic(mu1, cov1, prob1, distance)
        eq2 = Discriminant_analytic(mu2, cov2, prob2, distance)
        x, y = symbols('x y')

        # find the analytical expression of the boundary line
        boundary = solve(Eq(eq1, eq2), y)

        # compute the analytical expression of the discriminant function
        eq = eq1 - eq2

        return [boundary, eq]
```

#### "bayesian_classifier" function

Performs the bayesian classification according to the previously mentionned equations. Analytical equations of the discriminant functions and the decision boundaries are built and solved.

As suggested in the assignment, values of the mean vector and covariance matrices are rounded to the closest integer value.

#### inputs

    - data_set
    - grid_scale : sampling factor on coordinates to have better precision on separation line localization
    - T_bound : Threshold on the final separation matrix
    - labels : classes label
    - distance : type of discriminant function : 'full', 'mahanalobis' or 'euclidean'

#### outputs

    - X_bound : x-coordinate of the separation line
    - Y_bound : y-coordinate of the separation line
    - labels_graph : string with each possible class combinaisons
    - boundary : analytical expression of the discriminant functions and the boundary lines

```python
def bayesian_classifier(data_set, labels, distance):
    # initialization
    labels_graph = []
    boundary = []

    nb_class = data_set.shape[2]

    for i in range(nb_class - 1):
        for j in range(nb_class - i - 1):
            # write down the two current classes
            labels_graph.append('{}/{}'.format(labels[i], labels[i + j + 1]))

            # compute the analytical expression of the boundary (solution and discriminant function)
            boundary.append(boundary_equation(data_set[:,:,i], data_set[:,:,j + i + 1], data_set[:,0,:].size, distance))

    return labels_graph, boundary
```

#### "compute_accuracy" function

From classes in the data set, it computes the percentage of points correctly classified for each class combinaison. For exemple,

- $\text{accuracy(a/b)} = \frac{\text{nb_points_correctly_classified_in_a } + \text{ nb_points_correctly_classified_in_b}}{\text{total_nb_points_in_a_and_b}}$.

It first evaluates the disciminant function $G(x,y)$, given in 'equations', at each point of the two current data sets. Then, the classification is performed by assigning 0 to points that are misclassified and 1 to those that are correctly classified. By summing the number of '1' and dividing by the total nomber of points in both classes, the accuracy is computed for the current class combinaison.

#### inputs

    - data_set.
    - equations : set of analytical discriminant function G(x,y) for each couple of data set classes
    - distance : type of discriminant function : 'full', 'mahanalobis' or 'euclidean'

#### outputs

    - accuracy : percentage of points correctly classifed for each class combinaison

```python
def compute_accuracy(data_set, equations, distance):
    nb_class = data_set.shape[2]
    x, y = symbols('x y')
    accuracy = []

    for i in range(nb_class - 1):
        for j in range(nb_class - i - 1):
            # create a model to evaluate the equation on x and y
            f = lambdify([x,y], equations[j + (nb_class - i - 1) * i][1])

            # evaluate the equation with data_set coordinates x and y
            class_1 = f(data_set[:,0,i], data_set[:,1,i])
            class_2 = f(data_set[:,0,i + j + 1], data_set[:,1,i + j + 1])

            # performs the classification (1 and 0 to compute the mean)
            class_1[class_1 > 0] = 1
            class_1[class_1 <= 0] = 0

            class_2[class_2 >= 0] = 0
            class_2[class_2 < 0] = 1

            # % of points correctly classified in both classes
            accuracy.append((sum(class_2) + sum(class_1))/(len(class_2) + len(class_1)))

    # take into account the '-' of the formula
    if distance == 'full':
        return accuracy
    else:
        return 1 - np.array(accuracy)
```

Judging from the points distribution inside classes, we may infer the following assumptions :

- classes follow a normal distribution of dimension l = 2, given by the following formula :
$ p(\textbf{x}|w_i) = \frac{1}{2\pi|\boldsymbol{\Sigma_i}|^{1/2}} e^{0.5(\textbf{x}-\boldsymbol{\mu_i})^T\boldsymbol{\Sigma_i^{-1}}(\textbf{x}-\boldsymbol{\mu_i})}$. This is actually proved above using the Henze-Zirkler normality test.

- classes a and b have the same covariance matrix

In order to separate classes and find a classification rule for new points, the Bayes rule is implemented. The boundary of two classes i and j is determined by:

- $p(\textbf{x}|w_i)p(w_i) = p(\textbf{x}|w_j)p(w_j)$

with $p(w_i) = n_i/n_{tot}$, the ratio of the number of elements in classe i and the total number of elements, under the assumption that points were randomly and uniformely sampled from reality, which we suppose to be the case.

The discriminant function, $g_i(\textbf{x}) = p(\textbf{x}|w_i)p(w_i)$, can be re-written under normal approximation of dimension l = 2 as
- $g_i(\textbf{x}) = -\frac{1}{2}(\textbf{x}-\boldsymbol{\mu_i})^T\boldsymbol{\Sigma_i^{-1}}(\textbf{x}-\boldsymbol{\mu_i}) + \ln p(w_i) + \ln \frac{1}{2\pi|\boldsymbol{\Sigma_i}|^{1/2}}$ for the boundary between class a/c and b/c

- $g_i(\textbf{x}) = (\boldsymbol{\Sigma_i^{-1}}\boldsymbol{\mu_i})^T \textbf{x} + \ln p(w_i) - \frac{1}{2}\boldsymbol{\mu_i}^T\boldsymbol{\Sigma_i^{-1}}\boldsymbol{\mu_i}$ for the boundary between class a/b

The decision boundaries quatrics are then computed as $g_i(\textbf{x}) - g_j(\textbf{x}) = 0$

The results below show the 2 quatratic boundaries between class a/c and b/c and the linear one between class a/b.
When displaying the analytical equation of quatratic decision boundaries, two solutions are coming up (which is completely normal). The one that is interesting for us (i.e. corresponding to the boundary displayed on the graph) is the second one.

```python
# stack data sets a, b and c in one big data set
data_set_ab = np.dstack((a, b))
data_set_abc = np.dstack((data_set_ab, c))

# initialization
grid_fact = 30
T_bound = 0.1
labels = ['a', 'b', 'c']
colors = ['r', 'g', 'b']
distance = 'full' # distance metrics corresponding to bayes rule

# run bayesian classification with 'full' distance metrics
labels_graph, boundary = bayesian_classifier(data_set_abc, labels, distance)

# compute classification accuracy
accuracy = compute_accuracy(data_set_abc, boundary, distance)

plt.figure(figsize = (8,6))

# plot data sets a, b and c
for i in range(len(labels)):
    plt.plot(data_set_abc[:,0,i], data_set_abc[:,1,i], color = colors[i], linestyle = '', marker = '+', label = labels[i])

x, y = symbols('x y')

# plot the decision boundary lines
for i in range(len(labels_graph)):
    p = plot_implicit(boundary[i][1], (x, -12, 12), (y, -12, 12), show = False)
    series = p[0]
    data, action = series.get_points()
    curve = np.array([(x_int.mid, y_int.mid) for x_int, y_int in data])
    plt.scatter(curve[:,0], curve[:,1],label = labels_graph[i], s = 10)

    # display boundary equation and classification accuracy
    print('{} boundary equation : y = {}' .format(labels_graph[i], boundary[i][0]))
    print('accuracy of class {} : {}\n'.format(labels_graph[i], accuracy[i]))

plt.legend(loc = 'lower right')
plt.title('Classification using Bayes rule')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.show()
```

### 1.2 Mahalanobis distance (5 pts)
For classes `a` and `b`, give the expression of the Mahalanobis distance used to classify a point in class `a` or `b`, and verify the obtained classification, in comparison with the "complete" Bayes classification, for a few points of the plane.

For classes a and b, follwing assumptions can be made (recalled from above) :

     - Normal distibution for both classes.
     - Same covariance matrix but not diagonal

Under that assumptions, the mahanalobis distance can be used to classify points : $d_{m_i} = ((\textbf{x}-\boldsymbol{\mu_i})\boldsymbol{\Sigma_i}^{-1}(\textbf{x}-\boldsymbol{\mu_i}))^{1/2}$. Therefore, the decision boundary is linear.

A comparison with the bayes formula for the decision boundary is shown on separate graphs. Assumptions look correct because results are identical ; boundaries have the same equation. This is completely normal under the assumptions we have made. Indeed,

- $p(a) = p(b)$ and $\Sigma_a = \Sigma_b$.
- So, $-\frac{1}{2}(\textbf{x}-\boldsymbol{\mu_a})^T\boldsymbol{\Sigma_a^{-1}}(\textbf{x}-\boldsymbol{\mu_a}) + \ln p(a) + \ln \frac{1}{2\pi|\boldsymbol{\Sigma_a}|^{1/2}} = -\frac{1}{2}(\textbf{x}-\boldsymbol{\mu_b})^T\boldsymbol{\Sigma_b^{-1}}(\textbf{x}-\boldsymbol{\mu_b}) + \ln p(b) + \ln \frac{1}{2\pi|\boldsymbol{\Sigma_b}|^{1/2}}$.
- $-\frac{1}{2}(\textbf{x}-\boldsymbol{\mu_a})^T\boldsymbol{\Sigma_a^{-1}}(\textbf{x}-\boldsymbol{\mu_a}) = -\frac{1}{2}(\textbf{x}-\boldsymbol{\mu_b})^T\boldsymbol{\Sigma_b^{-1}}(\textbf{x}-\boldsymbol{\mu_b})$ because of first conditions.
- $(\textbf{x}-\boldsymbol{\mu_a})^T\boldsymbol{\Sigma_a^{-1}}(\textbf{x}-\boldsymbol{\mu_a}) = (\textbf{x}-\boldsymbol{\mu_b})^T\boldsymbol{\Sigma_b^{-1}}(\textbf{x}-\boldsymbol{\mu_b})$
- $d_{m_a}^2 = d_{m_b}^2$

If we suppose now that the covariance matrices are diagonal (which is of course not the case), then the euclidean distance can be used as a point classifier : $d_e = ||\textbf{x}-\boldsymbol{\mu_i}||$. The boundary is still a line but perpendicular to the line passing through the mean's center of each of the two classes. The results is also presented below as comparison : the classification is not correct and the accuracy is low compared to the mahanalobis distance classification. This confirms that the covariance matrices are not diagonal.

```python
# initialization
labels = ['a', 'b']
colors = ['r', 'g']
distance_metric = ['full', 'mahanalobis', 'euclidean']
T_bound = 0.08

# plot the data sets a / b and boundary for full distance metrics
plt.figure(figsize = (18,12))

for i in range(len(distance_metric)) :
    # run bayesian classification for three types of distances for data sets a and b
    labels_graph, boundary = bayesian_classifier(data_set_ab, labels, distance_metric[i])

    # compute accuracy (% of points that are well classified)
    accuracy = compute_accuracy(data_set_ab, boundary, distance_metric[i])

    plt.subplot(2,2,i + 1)

    # plot original dataset (a and b)
    for class_i in range(len(labels)):
        plt.plot(data_set_ab[:,0,class_i], data_set_ab[:,1,class_i],
                 color = colors[class_i], linestyle = '', marker = '+', label = labels[class_i])

    # plot boundary
    for bound_i in range(len(labels_graph)):
        p = plot_implicit(boundary[bound_i][1], (x, -12, 12), (y, -12, 12), show = False)
        series = p[0]
        data, action = series.get_points()
        curve = np.array([(x_int.mid, y_int.mid) for x_int, y_int in data])
        plt.scatter(curve[:,0], curve[:,1],label = labels_graph[bound_i], s = 10)

        # display boundary equation and classification accuracy
        print('{} equation for {} distance metrics : y = {}' .format(labels_graph[bound_i], distance_metric[i],
                                                                     boundary[bound_i][0]))
        print('accuracy of class {} : {}\n'.format(labels_graph[bound_i], accuracy[bound_i]))

    plt.legend(loc = 'lower right')
    plt.title('Classification with {} distance metrics' .format(distance_metric[i]))
    plt.xlabel('Feature 1')
    plt.ylabel('Feature 2')

plt.show()
```

---

## Part 2
In this part, we aim to classify digits using the complete version of MNIST digits dataset.
The dataset consists of 60'000 training images and 10'000 test images of handwritten digits.
Each image has size 28x28, and has assigned a label from zero to nine, denoting the digits value.
Given this data, your task is to construct a Multilayer Perceptron (MLP) for supervised training and classification and evaluate it on the test images.

Download the MNIST dataset (all 4 files) from http://yann.lecun.com/exdb/mnist/ under `lab-03-data/part2`.
You can then use the script provided below to extract and load training and testing images in Python.

**! Warning**: When the lab was created the official MNIST repo was down, if it is still the case please use https://github.com/mkolod/MNIST.

### 2.1 Dataset loading
Here we first declare the methods `extract_data` and `extract_labels` so that we can reuse them later in the code.
Then we extract both the data and corresponding labels, and plot randomly some images and corresponding labels of the training set.

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

def extract_data(filename, image_shape, image_number):
    with gzip.open(filename) as bytestream:
        bytestream.read(16)
        buf = bytestream.read(np.prod(image_shape) * image_number)
        data = np.frombuffer(buf, dtype = np.uint8).astype(np.float32)
        data = data.reshape(image_number, image_shape[0], image_shape[1])
    return data

def extract_labels(filename, image_number):
    with gzip.open(filename) as bytestream:
        bytestream.read(8)
        buf = bytestream.read(1 * image_number)
        labels = np.frombuffer(buf, dtype = np.uint8).astype(np.int64)
    return labels
```

```python
image_shape = (28, 28)
train_set_size = 60000
test_set_size = 10000

data_part2_folder = os.path.join(data_base_path, data_folder, 'part2')

train_images_path = os.path.join(data_part2_folder, 'train-images-idx3-ubyte.gz')
train_labels_path = os.path.join(data_part2_folder, 'train-labels-idx1-ubyte.gz')
test_images_path = os.path.join(data_part2_folder, 't10k-images-idx3-ubyte.gz')
test_labels_path = os.path.join(data_part2_folder, 't10k-labels-idx1-ubyte.gz')

train_images = extract_data(train_images_path, image_shape, train_set_size)
test_images = extract_data(test_images_path, image_shape, test_set_size)
train_labels = extract_labels(train_labels_path, train_set_size)
test_labels = extract_labels(test_labels_path, test_set_size)
```

```python
prng = np.random.RandomState(seed = 123456789)  # seed to always re-draw the same distribution
plt_ind = prng.randint(low = 0, high = train_set_size, size = 10)

fig, axes = plt.subplots(1, 10, figsize = (12, 3))
for ax, im, lb in zip(axes, train_images[plt_ind], train_labels[plt_ind]):
    ax.imshow(im, cmap = 'gray')
    ax.axis('off')
    ax.set_title(lb)
```

### 2.2 MLP (10 pts)

To create an MLP you are free to choose any library.
In case you don't have any preferences, we encourage you to use the [scikit-learn] package; it is a simple, efficient and free tool for data analysis and machine learning.
In this [link][sklearn-example], you can find a basic example to see how to create and train an MLP using [scikit-learn].
Your network should have the following properties:
* Input `x`: 784-dimensional (i.e. 784 visible units representing the flattened 28x28 pixel images).
* 100 hidden units `h`.
* 10 output units `y`, i.e. the labels, with a value close to one in the i-th class representing a high probability of the input representing the digit `i`.

If you need additional examples you can borrow some code from image classification tutorials.
However, we recommend that you construct a minimal version of the network on your own to gain better insights.

[scikit-learn]: http://scikit-learn.org/stable/index.html
[sklearn-example]: http://scikit-learn.org/stable/modules/neural_networks_supervised.html

Images for training and testing set are coming from http://yann.lecun.com/exdb/mnist/.

Here, we chose to use the SKlearn package, and 'adam' optimizer to train the classifier and predict the classification.

```python
from sklearn.neural_network import MLPClassifier

training_vector = np.reshape(train_images, (len(train_images), -1))

# define the classifier
clf = MLPClassifier(solver = 'adam', alpha = 1e-5, hidden_layer_sizes = 100, random_state = 1)

# train the classifier
_ = clf.fit(training_vector, train_labels)
```

```python
print('Size of the training set (nb_samples x visible_units) : {} x {}' .format(len(training_vector), len(training_vector[0])))
print('Number of layers : {}' .format(clf.n_layers_))
print('Hidden layer size : {}' .format(clf.hidden_layer_sizes))
print('Number of output units : {}' .format(clf.n_outputs_))
print('Number of iterations : {}' .format(clf.n_iter_))
```

    Size of the training set (nb_samples x visible_units) : 60000 x 784
    Number of layers : 3
    Hidden layer size : 100
    Number of output units : 10
    Number of iterations : 77

```python
# predict classification for both training and testing set
testing_vector = np.reshape(test_images, (len(test_images), -1))
prediction_test = clf.predict(testing_vector)
prediction_train  = clf.predict(training_vector)

# compute the accuracy of classification (% of the points correctly classified)
acc_test = np.mean(prediction_test == test_labels)
acc_train = np.mean(prediction_train == train_labels)

print('Size of the testing set (nb_samples x visible_units) : {} x {}' .format(len(testing_vector), len(testing_vector[0])))
print('accuracy of the training set : {}' .format(acc_train))
print('accuracy of the testing set : {}' .format(acc_test))
```

    Size of the testing set (nb_samples x visible_units) : 10000 x 784
    accuracy of the training set : 0.9958
    accuracy of the testing set : 0.968

```python

```


```python
from __future__ import print_function
import os
import pandas as pd
%matplotlib inline
from matplotlib import pyplot as plt
```

```python
#Set current dir and work relative to it
os.chdir('../Data Files')
```

```python
#Read dataset into a pandas.DataFrame
beer_df = pd.read_csv('quarterly-beer-production-in-aus-March 1956-June 1994.csv')
beer_df.index = beer_df['Quarter']
```

```python
#Display shape of the dataset
print('Shape of the dataframe:', beer_df.shape)
```

    Shape of the dataframe: (155, 2)

```python
#Show top 10 rows
beer_df.head(10)
```

      Quarter
      Quarterly beer production in Australia: megalitres. March 1956 ? June 1994

      Quarter

      1956Q1
      1956Q1
      284.4

      1956Q2
      1956Q2
      212.8

      1956Q3
      1956Q3
      226.9

      1956Q4
      1956Q4
      308.4

      1957Q1
      1957Q1
      262.0

      1957Q2
      1957Q2
      227.9

      1957Q3
      1957Q3
      236.1

      1957Q4
      1957Q4
      320.4

      1958Q1
      1958Q1
      271.9

      1958Q2
      1958Q2
      232.8

```python
#Rename the 2nd column
beer_df.rename(columns={'Quarterly beer production in Australia: megalitres. March 1956 ? June 1994':
                        'Beer_Prod'
                       },
               inplace=True
              )
```

```python
#Remove missing values
missing = (pd.isnull(beer_df['Quarter'])) | (pd.isnull(beer_df['Beer_Prod']))
print('Number of rows with at least one missing values:', missing.sum())
beer_df = beer_df.loc[~missing, :]
print('Shape after removing missing values:', beer_df.shape)
```

    Number of rows with at least one missing values: 1
    Shape after removing missing values: (154, 2)

```python
#In order to remove seasonal patterns let us calculate 2X4 quarter moving average
MA4 = beer_df['Beer_Prod'].rolling(window=4).mean()
TwoXMA4 = MA4.rolling(window=2).mean()
TwoXMA4 = TwoXMA4.loc[~pd.isnull(TwoXMA4)]
```

```python
#Let's plot the original time series and the seasonal moving averages
fig = plt.figure(figsize=(16, 8))
ax = fig.add_subplot(1,1,1)
beer_df['Beer_Prod'].plot(ax=ax, color='b', linestyle='-')
TwoXMA4.plot(ax=ax, color='r', linestyle='-')
plt.xticks(rotation=60)
ax.set_title('Quaterly Beer Production between in Australia and 2X4 quarter MA')
plt.savefig('../plots/ch2/B07887_02_16.png', format='png', dpi=300)
```

```python
#Let's compute the residuals after removing the trend
residuals = beer_df['Beer_Prod']-TwoXMA4
residuals = residuals.loc[~pd.isnull(residuals)]
```

```python
#Let's plot the residuals
fig = plt.figure(figsize=(16, 8))
ax = fig.add_subplot(1,1,1)
residuals.plot(ax=ax, color='b', linestyle='-')
plt.xticks(rotation=60)
ax.set_title('Residuals in Quaterly Beer Production time series')
plt.savefig('../plots/ch2/B07887_02_17.png', format='png', dpi=300)
```

```python
from pandas.plotting import autocorrelation_plot
```

```python
#Let's plot the autocorrelation function of the residuals
fig = plt.figure(figsize=(16, 8))
ax = fig.add_subplot(1,1,1)
autocorrelation_plot(residuals, ax=ax)
ax.set_title('ACF of Residuals in Quaterly Beer Production time series')
plt.savefig('../plots/ch2/B07887_02_18.png', format='png', dpi=300)
autocorrelation_plot(residuals)
```

```python
#Let's compute quarterly differecing to remove quaterly seasonality
residuals_qtr_diff = residuals.diff(4)
```

```python
#Remove null values
residuals_qtr_diff = residuals_qtr_diff.loc[~pd.isnull(residuals_qtr_diff)]
```

```python
#Let's plot the autocorrelation function of the residuals
fig = plt.figure(figsize=(16, 8))
ax = fig.add_subplot(1,1,1)
autocorrelation_plot(residuals_qtr_diff, ax=ax)
ax.set_title('ACF of Quaterly Differenced Residuals')
plt.savefig('../plots/ch2/B07887_02_19.png', format='png', dpi=300)
```

\begin{equation}
\hat{F}_t = w_{t-k}x_{t-k} + w_{t-k+1}x_{t-k+1} + ... +w_tx_t +...+ w_{t+k-1}x_{t+k-1} + w_{t+k}x_{t+k}
\end{equation}

\begin{equation}
w_{t-k} + w_{t-k+1} + ... + w_{t+k-1} + w_{t+k} = 1
\end{equation}

```python

```


```python
import sympy as sym
import matplotlib.pyplot as plt
%matplotlib inline
sym.init_printing()
```

# Caracterización de Sistemas en el Dominio del Tiempo

## Respuesta al impulso

A continuación se discutirá sobre la respuesta $y(t)$ de un sistema $\mathcal{H}$, Lineal e Invariante en el Tiempo (LTI), ante una señal de entrada $x(t)$.

Tenga en cuenta que la señal de entrada puede representarse como una integral:

\begin{equation}
x(t) = \int_{-\infty}^{\infty} x(\tau) \cdot \delta(t-\tau) \; d \tau
\end{equation}

Si se introduce esta representación de la señal de entrada en la transformación que realiza el sistema $\mathcal{H}$ para formar la salida  $y(t) = \mathcal{H} \{ x(t) \}$, se obtiene:

\begin{equation}
y(t) = \mathcal{H} \left\{ \int_{-\infty}^{\infty} x(\tau) \cdot \delta(t-\tau) \; d \tau \right\}
\end{equation}

donde $\mathcal{H} \{ \cdot \}$ representa un operador que encuentra la salida del sistema. Como $x(\tau)$ puede verse como una constante ante la variable $t$, la expresión para $y(t)$ puede escribirse como:

\begin{equation}
y(t) = \int_{-\infty}^{\infty}  x(\tau) \cdot \mathcal{H} \left\{ \delta(t-\tau) \right\} \; d \tau
\end{equation}

Observe que $\mathcal{H} \{\cdot\}$ opera únicamente sobre el impulso de Dirac.

La respuesta de un sistema ante una señal impulso de Dirac es conocida como la
[*respuesta al impulso*](https://en.wikipedia.org/wiki/Impulse_response).

\begin{equation}
h(t) = \mathcal{H} \left\{ \delta(t) \right\}
\end{equation}

Si el sistema es invariante en el tiempo, la respuesta a un impulso desplazado es $\mathcal{H} \left\{ \delta(t-\tau) \right\} = h(t-\tau)$. Así, para un sistema LTI se obtiene que:

\begin{equation}
y(t) = \int_{-\infty}^{\infty}  x(\tau) \cdot h(t-\tau) \; d \tau
\end{equation}

Esta operación es conocida como [*convolución*](https://en.wikipedia.org/wiki/Convolution). Se simboliza mediante $*$, de manera que la integral se represente como $y(t) = x(t) * h(t)$ o, para algunos autores, $y(t) = (x*h)(t)$.

La respuesta $y(t)$ de un sistema LTI \mathcal{H} ante una entrada $x(t)$ se determina completamente a partir de su respuesta impulsional $h(t)$ como la convolución de la entrada $x(t)$ con la respuesta impulsional $h(t)$.

Un sistema LTI con $x(t)$ como entrada y $y(t)$ como salida es descrito mediante la siguiente Ecuación Diferencial Ordinaria (ODE):

\begin{equation}
y(t) + \frac{d}{dt} y(t) = x(t)
\end{equation}

Se desea encontrar la respuesta del sistema ante una entrada $x(t) = e^{- 2 t} \cdot \epsilon(t)$

### Solución A
Primero, se va a resolver la ODE teniendo en cuenta  que deben satisfacerse las condiciones iniciales $y(t)\big\vert_{t = 0-} = 0$ y $\frac{d}{dt}y(t)\big\vert_{t = 0-} = 0$ relacionadas con la causalidad.

a. Defina la ODE en`SymPy`

```python
t = sym.symbols('t', real=True)
x = sym.Function('x')(t)
y = sym.Function('y')(t)

ode = sym.Eq(y + y.diff(t), x)
ode
```

Se resuelve la ODE para la entrada establecida y considerando que las constantes de integración deben garantiza las condiciones inicales.

```python
ode.subs(x, xinput)
```

```python
xinput = sym.exp(-2*t)*sym.Heaviside(t)

solution = sym.dsolve(ode.subs(x, xinput))
solution
```

```python

integration_constants = sym.solve(
    (solution.rhs.limit(t, 0, '-'), solution.rhs.diff(t).limit(t, 0, '-')), 'C1')

y1 = solution.subs(integration_constants)
y1
```

Se grafica la señal de salida obtenida al resolver la ODE.

```python
plt.rcParams['figure.figsize'] = 7, 2
sym.plot(y1.rhs, (t,-1,10), ylabel=r'$y(t)$');
```

### Solución B
Ahora se va a resolver el problema a través de la respuesta impulsional.

La respuesta impulsional $h(t)$ se encuentra al resolver la ODE para una señal impulso de Dirac como entrada, $x(t) = \delta(t)$.

```python
ode
```

```python

```

```python
h = sym.Function('h')(t)

solution2 = sym.dsolve(ode.subs(x, sym.DiracDelta(t)).subs(y, h))

integration_constants = sym.solve((solution2.rhs.limit(
    t, 0, '-'), solution2.rhs.diff(t).limit(t, 0, '-')), 'C1')

h = solution2.subs(integration_constants)
h
```

La respuesta impulsional se grafica.

```python
plt.rcParams['figure.figsize'] = 7, 2
sym.plot(h.rhs, (t,-1,10), ylabel=r'$h(t)$');
```

Ahora se calcula la convolución $y(t) = x(t) * h(t)$ como:

\begin{equation}
y(t) = \int_{-\infty}^{\infty}  x(\tau) \cdot h(t-\tau) \; d \tau
\end{equation}

Debido a que $h(t)$ y $x(t)$ son señales causales, la expresión de convolución se convierte en:

\begin{equation}
y(t) = \int_{0}^{t} x(\tau) \cdot h(t - \tau) \; d\tau \; \forall t \geq 0
\end{equation}

Observe que $y(t) = 0 \; \forall t<0$.

```python
h
```

```python
h.rhs
```

```python
h.lhs
```

```python
tau = sym.symbols('tau', real=True)

y2 = sym.integrate(xinput.subs(t, tau) * h.rhs.subs(t, t-tau), (tau, 0, t))
y2
```

```python
y1
```

Se grafica la señal de salida obtenida mediante la convolución.

```python
plt.rcParams['figure.figsize'] = 7, 2
sym.plot(y2, (t,-1,10), ylabel=r'$y(t)$');
```

Las señales obtenidas con los dos métodos deben ser iguales.  Verifiquemos las dos señales.

```python
y1
```

```python
y2
```

Al comparar las dos señales graficamente se obtiene:

```python
plt.rcParams['figure.figsize'] = 7, 2
graf1 = sym.plot(y1.rhs, (t,-1,10), ylabel=r'$y(t)$', show=False, line_color='red',legend=True, label = 'ODE')
graf2 = sym.plot(y2, (t,-1,10), show=False, legend=True, label = 'convolucion')
graf1.extend(graf2)
graf1.show()
```


# Artificial Intelligence & Machine Learning

## Ujian Akhir Semester

### Mekanisme

Anda hanya diwajibkan untuk mengumpulkan file ini saja ke uploader yang disediakan di https://elearning.uai.ac.id/. Ganti nama file ini saat pengumpulan menjadi **uas_NIM.ipynb**.

**Keterlambatan**: Pengumpulan ujian yang melebihi tenggat yang telah ditentukan tidak akan diterima. **Keterlambatan akan berakibat pada nilai nol untuk ujian ini.**

**Kolaborasi**: Anda tidak diperbolehkan untuk berdiskusi dengan teman Anda. Dilarang keras menyalin kode maupun tulisan dari teman Anda. Kecurangan akan berakibat pada nilai nol untuk ujian ini.

### Petunjuk

Untuk kelancaran Anda, gunakan Python 3 dalam ujian ini. Anda diperbolehkan (jika dirasa perlu) untuk mengimpor modul tambahan untuk tugas ini. Namun, seharusnya modul yang tersedia sudah cukup untuk memenuhi kebutuhan Anda. Untuk kode yang Anda ambil dari sumber lain, **cantumkan URL menuju referensi tersebut jika diambil dari internet**!

## 1. Markov Decision Processes

Diberikan definisi permainan sebagai berikut:
Untuk setiap ronder $r = 1, 2, ...$

* Anda dapat memilih opsi **A** atau **B**.
* Jika Anda memilih **A**, Anda akan mendapatkan \$7 dan akan dilempar sebuah dadu enam muka.
    * Jika yang keluar adalah **angka 1, 2, 3, atau 4**, maka permainan berhenti.
    * Jika yang keluar adalah **angka 5 atau 6**, maka kita akan lanjut ke ronde berikutnya.
* Jika Anda memilih **B**, Anda akan mendapatkan \$3 dan akan dilempar sebuah koin dengan muka **angka** atau **gambar**.
    * Jika yang keluar adalah **angka**, maka permainan berhenti.
    * Jika yang keluar adalah **gambar**, maka kita akan lanjut ke ronde berikutnya.

Tugas Anda adalah mendapatkan uang sebanyak-banyaknya.

### Soal 1.1 (3 poin)

Gambarkan MDP-nya, i.e. *states*, *actions*, dan *rewards*-nya dengan pustaka `networkx`. *States* yang Anda bisa kontrol adalah **main** dan **selesai**.

Tips: Anda bisa memanfaatkan modul MultiDiGraph dari `networkx`.

```python
import networkx as nx

# Kode Anda di sini
```

### Soal 1.2.a (2 poin)

Diberikan $\pi(main) = A$. Formulasikan $V_{\pi}(main)$ dan $V_{\pi}(selesai)$.

$$
V_{\pi}(main) = ...
$$

$$
V_{\pi}(selesai) = ...
$$

### Soal 1.2.b (2 poin)

Implementasikan algoritma *value iteration* dari formula di atas untuk mendapatkan $V_{\pi}(main)$ dan $V_{\pi}(selesai)$.

```python
# Kode Anda di sini
```

### Soal 1.3 (2 poin)

Dengan $\pi(main) = A$, tuliskan formula $Q_{\pi}(main, B)$ dan tentukan nilainya.

*Jawaban Anda di sini*

### Soal 1.4 (1 poin)

Apa yang menjadi nilai $\pi_{opt}(main)$?

*Jawaban Anda di sini*

## 2. Game Playing

Diberikan permainan seperti di bawah ini.

1. Diberikan ambang batas $N$ dan permainan dimulai dari **nilai 1**.
2. Para pemain secara bergantian dapat memilih untuk **menambahkan nilai dengan 2** atau **mengalikan nilai dengan 1.1**.
3. Pemain yang **melebihi ambang batas** akan kalah.

```python
import numpy as np

class ExplodingGame(object):
    def __init__(self, N):
        self.N = N

    # state = (player, number)
    def start(self):
        return (+1, 1)

    def actions(self, state):
        player, number = state
        return ['+', '*']

    def succ(self, state, action):
        player, number = state
        if action == '+':
            return (-player, number + 2)
        elif action == '*':
            return (-player, np.ceil(number * 1.1))
        assert False

    def is_end(self, state):
        player, number = state
        return number > self.N

    def utility(self, state):
        player, number = state
        assert self.is_end(state)
        return player * float('inf')

    def player(self, state):
        player, number = state
        return player
```

```python
def add_policy(game, state):
    action = '+'
    print(f"add policy: state {state} => action {action}")
    return action

def multiply_policy(game, state):
    action = '*'
    print(f"multiply policy: state {state} => action {action}")
    return action
```

### Soal 2.1 (2 poin)

Implementasikan random policy yang akan memilih aksi dengan rasio peluang 50%:50%.

```python
def random_policy(game, state):
    pass
```

### Soal 2.2 (3 poin)

Implementasikan fungsi minimax policy.

```python
def minimax_policy(game, state):
    pass
```

### Soal 2.3 (2 poin)

Implementasikan fungsi expectimax policy untuk melawan random policy yang didefinisikan pada soal 2.1.

```python
def expectimax_policy(game, state):
    pass
```

```python
# Kasus uji
game = ExplodingGame(N=10)

policies = {
    +1: add_policy,
    -1: multiply_policy
}

state = game.start()
while not game.is_end(state):
    # Who controls this state?
    player = game.player(state)
    policy = policies[player]
    # Ask policy to make a move
    action = policy(game, state)
    # Advance state
    state = game.succ(state, action)
print(f"Utility di akhir permainan {game.utility(state)}")
```

    add policy: state (1, 1) => action +
    multiply policy: state (-1, 3) => action *
    add policy: state (1, 4.0) => action +
    multiply policy: state (-1, 6.0) => action *
    add policy: state (1, 7.0) => action +
    multiply policy: state (-1, 9.0) => action *
    add policy: state (1, 10.0) => action +
    Utility di akhir permainan -inf

### Soal 2.4 (3 poin)

Sebutkan policy terbaik untuk melawan:

1. random policy
2. expectimax policy
3. minimax policy

*Jawaban Anda di sini*

## 3. Bayesian Network

Bayangkan Anda adalah seorang klimatolog yang bekerja untuk BMKG di tahun 3021 yang sedang mempelajari kasus pemanasan global. Anda tidak mengetahui catatan cuaca di tahun 2021 di Jakarta, tapi Anda punya diari milik Pak Ali yang memberikan keterangan berapa jumlah es krim yang dimakan Pak Ali tiap harinya di musim kemarau. Tujuan Anda adalah mengestimasi perubahan cuaca dari hari ke hari - panas (H) atau sejuk (C). Dengan kata lain,

> Jika diberikan observasi $O$ (bilangan bulat yang merepresentasikan jumlah es krim yang dimakan Pak Ali di suatu hari), temukan urutan kondisi cuaca $Q$ dari hari-hari tersebut.

Definisi variabelnya adalah sebagai berikut: $Q \in \{H, C\}$ dan $O \in \{1, 2, 3\}$. Untuk bagian ini, Anda diminta untuk mengimplementasikan kode dengan memanfaatkan pustaka [pomegranate](https://pomegranate.readthedocs.io/en/latest/HiddenMarkovModel.html).

Masalah ini diadaptasi dari makalah oleh [Eisner (2002)](http://www.cs.jhu.edu/~jason/papers/#eisner-2002-tnlp).

*Petunjuk: Lihat kembali tugas 4 Anda. Penggunaan pustaka pomegranate sangat mirip dengan implementasi pada tugas tersebut.*

```python
!pip install pomegranate
```

```python
from pomegranate import *

observed = [2,3,3,2,3,2,3,2,2,3,1,3,3,1,1,1,2,1,1,1,3,1,2,1,1,1,2,3,3,2,3,2,2]
```

### Soal 3.1 (2 poin)

Jika diketahui bahwa
\begin{align}
P(1|H) = 0.2 \\
P(2|H) = 0.4 \\
P(3|H) = 0.4
\end{align}
dan
\begin{align}
P(1|C) = 0.5 \\
P(2|C) = 0.4 \\
P(3|C) = 0.1
\end{align}

Definisikan probabilitas emisinya.

```python
# Kode anda di sini
```

### Soal 3.2 (2 poin)

Diketahui bahwa
\begin{align}
P(Q_t=H|Q_{t-1}=H) &= 0.6 \\
P(Q_t=C|Q_{t-1}=H) &= 0.4 \\
P(Q_t=H|Q_{t-1}=C) &= 0.5 \\
P(Q_t=C|Q_{t-1}=C) &= 0.5 \\
\end{align}

Definisikan probabilitas transisinya.

```python
# Kode Anda di sini
```

### Soal 3.3 (2 poin)

Diketahui bahwa
$$
P(Q_1 = H) = 0.8
$$

Definisikan probabilitas inisiasinya.

```python
# Kode Anda di sini
```

### Soal 3.4 (2 poin)

Berapa log probability dari observasi (`observed`) di atas?

```python
# Kode Anda di sini
```

### Soal 3.5 (2 poin)

Tunjukkan urutan $Q$ yang paling mungkin.

```python
# Kode Anda di sini
```


# A Quantum distance-based classifier (part 1)

 Robert Wezeman, TNO

## Table of Contents

* [Introduction](#introduction)
* [Problem](#problem)
* [Amplitude Encoding](#amplitude)
* [Data preprocessing](#dataset)
* [Quantum algorithm](#algorithm)
* [Conclusion and further work](#conclusion)

```python
# Import external python file
import numpy as np
from data_plotter import get_bin, DataPlotter  # for easier plotting
DataPlotter = DataPlotter()
```

$$ \newcommand{\ket}[1]{\left|{#1}\right\rangle} $$

## Introduction

Consider the following scatter plot of the first two flowers in [the famous Iris flower data set](https://en.wikipedia.org/wiki/Iris_flower_data_set)

Notice that just two features, the sepal width and the sepal length, divide the two different Iris species into different regions in the plot. This gives rise to the question: given only the sepal length and sepal width of a flower can we classify the flower by their correct species? This type of problem, also known as [statistical classification](https://en.wikipedia.org/wiki/Statistical_classification), is a common problem in machine learning. In general, a classifier is constructed by letting it learn a function which gives the desired output based on a sufficient amount of data. This is called supervised learning, as the desired output (the labels of the data points) are known. After learning, the classifier can classify an unlabeled data point based on the learned function. The quality of a classifier improves if it has a larger training dataset it can learn on. The true power of this quantum classifier becomes clear when using extremely large data sets.
In this notebook we will describe how to build a distance-based classifier on the Quantum Inspire using amplitude encoding. It turns out that, once the system is initialized in the desired state, regardless of the size of training data, the actual algorithm consists of only 3 actions, one Hadamard gate and two measurements. This has huge implications for the scalability of this problem for large data sets. Using only 4 qubits we show how to encode two data points, both of a different class, to predict the label for a third data point. In this notebook we will demonstrate how to use the Quantum Inspire SDK using QASM-code, we will also provide the code to obtain the same results for the ProjectQ framework.

[Back to Table of Contents](#contents)

## Problem

We define the following binary classification problem: Given the data set
$$\mathcal{D} = \Big\{ ({\bf x}_1, y_1), \ldots ({\bf x}_M , y_M)  \Big\},$$

consisting of $M$ data points $x_i\in\mathbb{R}^n$ and corresponding labels $y_i\in \{-1, 1\}$, give a prediction for the label $\tilde{y}$ corresponding to an unlabeled data point $\bf\tilde{x}$. The classifier we shall implement with our quantum circuit is a distance-based classifier and is given by
\begin{equation}\newcommand{\sgn}{{\rm sgn}}\newcommand{\abs}[1]{\left\lvert#1\right\rvert}\label{eq:classifier} \tilde{y} = \sgn\left(\sum_{m=0}^{M-1} y_m \left[1-\frac{1}{4M}\abs{{\bf\tilde{x}}-{\bf x}_m}^2\right]\right). \hspace{3cm} (1)\end{equation}

This is a typical $M$-nearest-neighbor model, where each data point is given a weight related to the distance measure. To implement this classifier on a quantum computer, we need a way to encode the information of the training data set in a quantum state. We do this by first encoding the training data in the amplitudes of a quantum system, and then manipulate the amplitudes of then the amplitudes will be manipulated by quantum gates such that we obtain a result representing the above classifier. Encoding input features in the amplitude of a quantum system is known as amplitude encoding.

[Back to Contents](#contents)

## Amplitude encoding

Suppose we want to encode a classical vector $\bf{x}\in\mathbb{R}^N$ by some amplitudes of a quantum system. We assume $N=2^n$ and that $\bf{x}$ is normalised to unit length, meaning ${\bf{x}^T{x}}=1$. We can encode $\bf{x}$ in the amplitudes of a $n$-qubit system in the following way
\begin{equation}
{\bf x} = \begin{pmatrix}x^1 \\ \vdots \\ x^N\end{pmatrix} \Longleftrightarrow{} \ket{\psi_{{\bf x}}} = \sum_{i=0}^{N-1}x^i\ket{i},
\end{equation}
where $\ket{i}$ is the $i^{th}$ entry of the computational basis $\left\{\ket{0\ldots0},\ldots,\ket{1\ldots1}\right\}$. By applying an efficient quantum algorithm (resources growing polynomially in the number of qubits $n$), one can manipulate the $2^n$ amplitudes super efficiently, that is $\mathcal{O}\left(\log N\right)$. This follows as manipulating all amplitudes requires an operation on each of the $n = \mathcal{O}\left(\log N\right)$ qubits. For algorithms to be truly super-efficient, the phase where the data is encoded must also be at most polynomial in the number of qubits. The idea of quantum memory, sometimes referred as quantum RAM (QRAM), is a particular interesting one. Suppose we first run some quantum algorithm, for example in quantum chemistry, with as output some resulting quantum states. If these states could be fed into a quantum classifier, the encoding phase is not needed anymore. Finding efficient data encoding systems is still a topic of active research. We will restrict ourselves here to the implementation of the algorithm, more details can be found in the references.

The algorithm requires the $n$-qubit quantum system to be in the following state
\begin{equation}\label{eq:prepstate}
\ket{\mathcal{D}} = \frac{1}{\sqrt{2M}} \sum_{m=0}^{M-1} \ket{m}\Big(\ket{0}\ket{\psi_{\bf\tilde{{x}}}} + \ket{1}\ket{\psi_{\bf{x}_m}}\Big)\ket{y_m}.\hspace{3cm} (2)
\end{equation}
Here $\ket{m}$ is the $m^{th}$ state of the computational basis used to keep track of the $m^{th}$ training input. The second register is a single ancillary qubit entangled with the third register. The excited state of the ancillary qubit is entangled with the $m^{th}$ training state $\ket{\psi_{{x}_m}}$, while the ground state is entangled with the new input state $\ket{\psi_{\tilde{x}}}$. The last register encodes the label of the $m^{th}$ training data point by
\begin{equation}
\begin{split}
y_m = -1 \Longleftrightarrow& \ket{y_m} = \ket{0},\\
y_m = 1 \Longleftrightarrow& \ket{y_m} = \ket{1}.
\end{split}
\end{equation}
Once in this state the algorithm only consists of the following three operations:

1. Apply a Hadamard gate on the second register to obtain

  $$\frac{1}{2\sqrt{M}} \sum_{m=0}^{M-1} \ket{m}\Big(\ket{0}\ket{\psi_{\bf\tilde{x}+x_m}} + \ket{1}\ket{\psi_{\bf\tilde{x}-x_m}}\Big)\ket{y_m},$$

  where $\ket{\psi_{\bf\tilde{{x}}\pm{x}_m}} = \ket{\psi_{\tilde{\bf{x}}}}\pm \ket{\psi_{\bf{x}_m}}$.

2. Measure the second qubit. We restart the algorithm if we measure a $\ket{1}$ and only continue if we are in the $\ket{0}$ branch. We continue the algorithm with a probability $p_{acc} = \frac{1}{4M}\sum_M\abs{{\bf\tilde{x}}+{\bf x}_m}^2$, for standardised random data this is usually around $0.5$. The resulting state is given by

\begin{equation}
\frac{1}{2\sqrt{Mp_{acc}}}\sum_{m=0}^{M-1}\sum_{i=0}^{N-1} \ket{m}\ket{0}\left({\tilde{x}}^i + x_m^i\right)\ket{i}\ket{y_m}.
\end{equation}

3. Measure the last qubit $\ket{y_m}$. The probability that we measure outcome zero is given by
\begin{equation}
p(q_4=0) = \frac{1}{4Mp_{acc}}\sum_{m|y_m=0}\abs{\bf{\tilde{{x}}+{x}_m}}^2.
\end{equation}

In the special case where the amount of training data for both labels is equal, this last measurement relates to the classifier as described in previous section by
\begin{equation}
\tilde{y} = \left\{
\begin{array}{lr}
-1 & : p(q_4 = 0 ) > p(q_4 = 1)\\
+1 & : p(q_4 = 0 ) < p(q_4 = 1)
\end{array}
\right.
\end{equation}
By setting $\tilde{y}$ to be the most likely outcome of many measurement shots, we obtain the desired distance-based classifier.

[Back to Table of Contents](#contents)

## Data preprocessing

In the previous section we saw that for amplitude encoding we need a data set which is normalised. Luckily, it is always possible to bring data to this desired form with some data transformations. Firstly, we standardise the data to have zero mean and unit variance, then we normalise the data to have unit length. Both these steps are common methods in machine learning. Effectively, we only have to consider the angle between different data features.

To illustrate this procedure we apply it to the first two features of the famous Iris data set:

```python
# Plot the data
from sklearn.datasets import load_iris

iris = load_iris()
features = iris.data.T
data = [el[0:101] for el in features][0:2]  # Select only the first two features of the dataset

half_len_data = len(data[0]) // 2
iris_setosa = [el[0:half_len_data] for el in data[0:2]]
iris_versicolor = [el[half_len_data:-1] for el in data[0:2]]

DataPlotter.plot_original_data(iris_setosa, iris_versicolor);  # Function to plot the data

```

```python
# Rescale the data
from sklearn import preprocessing  # Module contains method to rescale data to have zero mean and unit variance

# Rescale whole data-set to have zero mean and unit variance
features_scaled = [preprocessing.scale(el) for el in data[0:2]]
iris_setosa_scaled = [el[0:half_len_data] for el in features_scaled]
iris_versicolor_scaled = [el[half_len_data:-1] for el in features_scaled]

DataPlotter.plot_standardised_data(iris_setosa_scaled, iris_versicolor_scaled);  # Function to plot the data

```

```python
# Normalise the data
def normalise_data(arr1, arr2):
    """Normalise data to unit length
        input: two array same length
        output: normalised arrays
    """
    for idx in range(len(arr1)):
        norm = (arr1[idx]**2 + arr2[idx]**2)**(1 / 2)
        arr1[idx] = arr1[idx] / norm
        arr2[idx] = arr2[idx] / norm
    return [arr1, arr2]

iris_setosa_normalised = normalise_data(iris_setosa_scaled[0], iris_setosa_scaled[1])
iris_versicolor_normalised = normalise_data(iris_versicolor_scaled[0], iris_versicolor_scaled[1])
# Function to plot the data
DataPlotter.plot_normalised_data(iris_setosa_normalised, iris_versicolor_normalised);

```

[Table of Contents](#contents)

## Quantum algorithm

Now we can start with our quantum algorithm on the Quantum Inspire. We describe how to build the algorithm for the simplest case with only two data points, each with two features, that is $M=N=2$. For this algorithm we need 4 qubits:

* One qubit for the index register $\ket{m}$
* One ancillary qubit
* One qubit to store the information of the two features of the data points
* One qubit to store the information of the classes of the data points

From the data set described in previous section we pick the following data set $\mathcal{D} = \big\{({\bf x}_1,y_1), ({\bf x}_2, y_2) \big\}$
where:

* ${\bf x}_1 = (0.9193, 0.3937)$, $y_1 = -1$,
* ${\bf x}_2 = (0.1411, 0.9899)$, $y_2 =  1$.

We are interested in the label $\tilde{y}$ for the data point ${\bf \tilde{x}} = (0.8670, 0.4984)$.
The amplitude encoding of these data points look like
\begin{equation}
\begin{split}
\ket{\psi_{\bf\tilde{x}}} & = 0.8670 \ket{0} + 0.4984\ket{1}, \\
\ket{\psi_{\bf x_1}} & = 0.9193 \ket{0} + 0.3937\ket{1},\\
\ket{\psi_{\bf x_2}} & = 0.1411 \ket{0} + 0.9899\ket{1}.
\end{split}
\end{equation}

Before we can run the actual algorithm we need to bring the system in the desired [initial state (equation 2)](#state) which can be obtain by applying the following combination of gates starting on $\ket{0000}$.

* **Part A:** In this part the index register is initialized and the ancilla qubit is brought in the desired state. For this we use the plain QASM language of the Quantum Inspire. Part A consists of two Hadamard gates:

```python
def part_a():
    qasm_a = """version 1.0
qubits 4
prep_z q[0:3]
.part_a
H q[0:1] #execute Hadamard gate on qubit 0, 1
"""
    return qasm_a
```

After this step the system is in the state
$$\ket{\mathcal{D}_A} = \frac{1}{2}\Big(\ket{0}+\ket{1}\Big)\Big(\ket{0}+\ket{1}\Big)\ket{0}\ket{0} $$

* **Part B:** In this part we encode the unlabeled data point $\tilde{x}$ by making use of a controlled rotation. We entangle the third qubit with the ancillary qubit. The angle $\theta$ of the rotation should be chosen such that $\tilde{x}=R_y(\theta)\ket{0}$. By the definition of $R_y$ we have

$$ R_y(\theta)\ket{0} = \cos\left(\frac{\theta}{2}\right)\ket{0} + \sin\left(\frac{\theta}{2}\right)\ket{1}.$$

Therefore, the angle needed to rotate to the state $\psi=a\ket{0} + b\ket{1}$ is given by $\theta = 2\cos^{-1}(a)\cdot sign(b)$.
Quantum Inspire does not directly support controlled-$R_y$ gates, however we can construct it from other gates as shown in the figure below. In these pictures $k$ stand for the angle used in the $R_y$ rotation.

```python
def part_b(angle):
    half_angle = angle / 2
    qasm_b = """.part_b # encode test value x^tilde
CNOT q[1], q[2]
Ry q[2], -{0}
CNOT q[1], q[2]
Ry q[2], {0}
X q[1]
""".format(half_angle)
    return qasm_b
```

After this step the system is in the state
$$\ket{\mathcal{D}_B} = \frac{1}{2} \Big(\ket{0}+\ket{1}\Big)\Big(\ket{0}\ket{\tilde{{x}}}+\ket{1}\ket{0}\Big)\ket{0}$$

* **Part C:** In this part we encode the first data point $x_1$. The rotation angle $\theta$ is such that $\ket{x_1}  = R_y(\theta)\ket{0}$. Now a double controlled-$R_y$ rotation is needed, and similar to Part B, we construct it from other gates as shown in the figure below.

```python
def part_c(angle):
    quarter_angle = angle / 4
    qasm_c = """.part_c # encode training x^0 value
toffoli q[0],q[1],q[2]
CNOT q[0],q[2]
Ry q[2], {0}
CNOT q[0],q[2]
Ry q[2], -{0}
toffoli q[0],q[1],q[2]
CNOT q[0],q[2]
Ry q[2], -{0}
CNOT q[0],q[2]
Ry q[2], {0}
X q[0]
""".format(quarter_angle)
    return qasm_c
```

After this step the system is in the state
$$\ket{\mathcal{D}_C} = \frac{1}{2}\Bigg(\ket{0}\Big(\ket{0}\ket{\tilde{{x}}} + \ket{1}\ket{{x_1}}\Big) + \ket{1}\Big(\ket{0}\ket{\tilde{{x}}} + \ket{1}\ket{0}\Big)\Bigg) \ket{0}$$

* **Part D:** This part is almost an exact copy of part C, however now with $\theta$ chosen such that $\ket{{x}_2}  = R_y(\theta)\ket{0}$.

```python
def part_d(angle):
    quarter_angle = angle / 4
    qasm_d = """.part_d # encode training x^1 value
toffoli q[0],q[1],q[2]
CNOT q[0],q[2]
Ry q[2], {0}
CNOT q[0],q[2]
Ry q[2], -{0}
toffoli q[0],q[1],q[2]
CNOT q[0],q[2]
Ry q[2], -{0}
CNOT q[0],q[2]
Ry q[2], {0}
""".format(quarter_angle)
    return qasm_d
```

After this step the system is in the state
$$\ket{\mathcal{D}_D} = \frac{1}{2}\Bigg(\ket{0}\Big(\ket{0}\ket{\tilde{{x}}} + \ket{1}\ket{{x_1}}\Big) + \ket{1}\Big(\ket{0}\ket{\tilde{{x}}} + \ket{1}\ket{{x}_2}\Big)\Bigg) \ket{0}$$

* **Part E:** The last step is to label the last qubit with the correct class, this can be done using a simple CNOT gate between the first and last qubit to obtain the desired initial state
$$\ket{\mathcal{D}_E} = \frac{1}{2}\ket{0}\Big(\ket{0}\ket{\tilde{{x}}} + \ket{1}\ket{{x_1}}\Big)\ket{0} + \ket{1}\Big(\ket{0}\ket{\tilde{{x}}} + \ket{1}\ket{{x}_2}\Big)\ket{1}.
$$

```python
def part_e():
    qasm_e = """.part_e # encode the labels
CNOT q[0], q[3]
"""
    return qasm_e
```

## The actual algorithm

Once the system is in this initial state, the algorithm itself only consists of one Hadamard gate and two measurements. If the first measurement gives the result $\ket{1}$, we have to abort the algorithm and start over again. However, these results can also easily be filtered out in a post-proecessing step.

```python
def part_f():
    qasm_f = """
.part_f
H q[1]
"""
    return qasm_f
```

The circuit for the whole algorithm now looks like:

We can send our QASM code to the Quantum Inspire with the following data points

\begin{equation}
\begin{split}
\ket{\psi_{\tilde{x}}} & = 0.8670 \ket{0} + 0.4984\ket{1},\\
\ket{\psi_{x_1}} & = 0.9193 \ket{0} + 0.3937\ket{1},\\
\ket{\psi_{x_2}} & = 0.1411 \ket{0} + 0.9899\ket{1}.
\end{split}
\end{equation}

```python
import os
from quantuminspire.api import QuantumInspireAPI
from quantuminspire.credentials import get_authentication
from math import acos
from math import pi

QI_URL = os.getenv('API_URL', 'https://api.quantum-inspire.com/')

# input data points:
angle_x_tilde = 2 * acos(0.8670)
angle_x0 = 2 * acos(0.1411)
angle_x1 = 2 * acos(0.9193)

authentication = get_authentication()
qi = QuantumInspireAPI(QI_URL, authentication)

# Build final QASM
final_qasm = part_a() + part_b(angle_x_tilde) + part_c(angle_x0) + part_d(angle_x1) + part_e() + part_f()

backend_type = qi.get_backend_type_by_name('QX single-node simulator')
result = qi.execute_qasm(final_qasm, backend_type=backend_type, number_of_shots=1, full_state_projection=True)

print(result['histogram'])

```

    OrderedDict([('9', 0.3988584), ('4', 0.2768809), ('0', 0.1270332), ('13', 0.099428), ('2', 0.0658663), ('6', 0.0302195), ('15', 0.0013716), ('11', 0.0003419)])

```python
import matplotlib.pyplot as plt

def bar_plot(result_data):
    res = [get_bin(el, 4) for el in range(16)]
    prob = [0] * 16

    for key, value in result_data['histogram'].items():
        prob[int(key)] = value

    # Set color=light grey when 2nd qubit = 1
    # Set color=blue when 2nd qubit = 0, and last qubit = 1
    # Set color=red when 2nd qubit = 0, and last qubit = 0
    color_list = [
        'red', 'red', (0.1, 0.1, 0.1, 0.1), (0.1, 0.1, 0.1, 0.1),
        'red', 'red', (0.1, 0.1, 0.1, 0.1), (0.1, 0.1, 0.1, 0.1),
        'blue', 'blue', (0.1, 0.1, 0.1, 0.1), (0.1, 0.1, 0.1, 0.1),
        'blue', 'blue', (0.1, 0.1, 0.1, 0.1), (0.1, 0.1, 0.1, 0.1)
                 ]
    plt.bar(res, prob, color=color_list)
    plt.ylabel('Probability')
    plt.title('Results')
    plt.ylim(0, 1)
    plt.xticks(rotation='vertical')
    plt.show()
    return prob

prob = bar_plot(result)
```

We only consider the events where the second qubit equals 0, that is, we only consider the events in the set $$\{0000, 0001, 0100, 0101, 1000, 1001, 1100, 1101\}$$

The label $\tilde{y}$ is now given by

\begin{equation}
\tilde{y} = \left\{
\begin{array}{lr}
-1 & : \#\{0000, 0001, 0100, 0101\} > \#\{1000, 1001, 1100, 1101\}\\
+1 & : \#\{1000, 1001, 1100, 1101\} > \#\{0000, 0001, 0100, 0101\}
\end{array}
\right.
\end{equation}

```python
def summarize_results(prob, display=1):
    sum_label0 = prob[0] + prob[1] + prob[4] + prob[5]
    sum_label1 = prob[8] + prob[9] + prob[12] + prob[13]

    def y_tilde():
        if sum_label0 > sum_label1:
            return 0, ">"
        elif sum_label0 < sum_label1:
            return 1, "<"
        else:
            return "undefined", "="
    y_tilde_res, sign = y_tilde()
    if display:
        print("The sum of the events with label 0 is: {}".format(sum_label0))
        print("The sum of the events with label 1 is: {}".format(sum_label1))
        print("The label for y_tilde is: {} because sum_label0 {} sum_label1".format(y_tilde_res, sign))
    return y_tilde_res

summarize_results(prob);
```

    The sum of the events with label 0 is: 0.4039141
    The sum of the events with label 1 is: 0.4982864
    The label for y_tilde is: 1 because sum_label0 < sum_label1

The following code will randomly pick two training data points and a random test point for the algorithm. We can compare the prediction for the label by the Quantum Inspire with the true label.

```python
from random import sample
from numpy import sign

def grab_random_data():
    one_random_index = sample(range(50), 1)
    two_random_index = sample(range(50), 2)
    random_label = sample([1,0], 1) # random label

    # iris_setosa_normalised  # Label 0
    # iris_versicolor_normalised  # Label 1
    if random_label[0]:
        # Test data has label = 1, iris_versicolor
        data_label0 = [iris_setosa_normalised[0][one_random_index[0]],
                       iris_setosa_normalised[1][one_random_index[0]]]
        data_label1 = [iris_versicolor_normalised[0][two_random_index[0]],
                       iris_versicolor_normalised[1][two_random_index[0]]]
        test_data = [iris_versicolor_normalised[0][two_random_index[1]],
                     iris_versicolor_normalised[1][two_random_index[1]]]
    else:
        # Test data has label = 0, iris_setosa
        data_label0 = [iris_setosa_normalised[0][two_random_index[0]],
                       iris_setosa_normalised[1][two_random_index[0]]]
        data_label1 = [iris_versicolor_normalised[0][one_random_index[0]],
                       iris_versicolor_normalised[1][one_random_index[0]]]
        test_data = [iris_setosa_normalised[0][two_random_index[1]],
                     iris_setosa_normalised[1][two_random_index[1]]]
    return data_label0, data_label1, test_data, random_label

data_label0, data_label1, test_data, random_label = grab_random_data()

print("Data point {} from label 0".format(data_label0))
print("Data point {} from label 1".format(data_label1))
print("Test point {} from label {} ".format(test_data, random_label[0]))

def run_random_data(data_label0, data_label1, test_data):
    angle_x_tilde = 2 * acos(test_data[0]) * sign(test_data[1]) % (4 * pi)
    angle_x0 = 2 * acos(data_label0[0]) * sign(data_label0[1]) % (4 * pi)
    angle_x1 = 2 * acos(data_label1[0])* sign(data_label1[1]) % (4 * pi)

    # Build final QASM
    final_qasm = part_a() + part_b(angle_x_tilde) + part_c(angle_x0) + part_d(angle_x1) + part_e() + part_f()
    result_random_data = qi.execute_qasm(final_qasm, backend_type=backend_type, number_of_shots=1, full_state_projection=True)
    return result_random_data

result_random_data = run_random_data(data_label0, data_label1, test_data);

# Plot data points:
plt.rcParams['figure.figsize'] = [16, 6]  # Plot size
plt.subplot(1, 2, 1)
DataPlotter.plot_normalised_data(iris_setosa_normalised, iris_versicolor_normalised);
plt.scatter(test_data[0], test_data[1], s=50, c='green');  # Scatter plot data class ?
plt.scatter(data_label0[0], data_label0[1], s=50, c='orange');  # Scatter plot data class 0
plt.scatter(data_label1[0], data_label1[1], s=50, c='orange');  # Scatter plot data class 1
plt.legend(["Iris Setosa (label 0)", "Iris Versicolor (label 1)", "Test point", "Data points"])
plt.subplot(1, 2, 2)
prob_random_points = bar_plot(result_random_data);
summarize_results(prob_random_points);

```

To get a better idea how well this quantum classifier works we can compare the predicted label to the true label of the test datapoint. Errors in the prediction can have two causes. The quantum classifier does not give the right classifier prediction or the quantum classifier gives the right classifier prediction which for the selected data gives the wrong label. in general, the first type of errors can be reduced by increasing the number of times we run the algorithm. In our case, as we work with the simulator and our gates are deterministic ([no conditional gates](https://www.quantum-inspire.com/kbase/optimization-of-simulations/)), we do not have to deal with this first error if we use the true probability distribution. This can be done by using only a single shot without measurements.

```python
quantum_score = 0
error_prediction = 0
classifier_is_quantum_prediction = 0
classifier_score = 0
no_label = 0

def true_classifier(data_label0, data_label1, test_data):
    if np.linalg.norm(np.array(data_label1) - np.array(test_data)) < np.linalg.norm(np.array(data_label0) -
                                                                                np.array(test_data)):
        return 1
    else:
        return 0

for idx in range(100):
    data_label0, data_label1, test_data, random_label = grab_random_data()
    result_random_data = run_random_data(data_label0, data_label1, test_data)
    classifier = true_classifier(data_label0, data_label1, test_data)

    sum_label0 = 0
    sum_label1 = 0
    for key, value in result_random_data['histogram'].items():
        if int(key) in [0, 1, 4, 5]:
            sum_label0 += value
        if int(key) in [8, 9, 12, 13]:
            sum_label1 += value
    if sum_label0 > sum_label1:
        quantum_prediction = 0
    elif sum_label1 > sum_label0:
        quantum_prediction = 1
    else:
        no_label += 1
        continue

    if quantum_prediction == classifier:
        classifier_is_quantum_prediction += 1

    if random_label[0] == classifier:
        classifier_score += 1

    if quantum_prediction == random_label[0]:
        quantum_score += 1
    else:
        error_prediction += 1

print("In this sample of 100 data points:")
print("the classifier predicted the true label correct", classifier_score, "% of the times")
print("the quantum classifier predicted the true label correct", quantum_score, "% of the times")
print("the quantum classifier predicted the classifier label correct",
      classifier_is_quantum_prediction, "% of the times")
print("Could not assign a label ", no_label, "times")
```

    In this sample of 100 data points:
    the classifier predicted the true label correct 93 % of the times
    the quantum classifier predicted the true label correct 93 % of the times
    the quantum classifier predicted the classifier label correct 100 % of the times
    Could not assign a label  0 times

## Conclusion and further work

How well the quantum classifier performs, hugely depends on the chosen data points. In case the test data point is significantly closer to one of the two training data points the classifier will result in a one-sided prediction. The other case, where the test data point has a similar distance to both training points, the classifier struggles to give an one-sided prediction. Repeating the algorithm on the same data points, might sometimes give different measurement outcomes. This type of error can be improved by running the algorithm using more shots. In the examples above we only used the true probability distribution (as if we had used an infinite number of shots). By running the algorithm instead with 512 or 1024 shots this erroneous behavior can be observed. In case of an infinite number of shots, we see that the quantum classifier gives the same prediction as classically expected.

The results of this toy example already shows the potential of a quantum computer in machine learning. Because the actual algorithm consists of only three operations, independent of the size of the data set, it can become extremely useful for tasks such as pattern recognition on large data sets. The next step is to extend this toy model to contain more data features and a larger training data set to improve the prediction. As not all data sets are best classified by a distance-based classifier, implementations of other types of classifiers might also be interesting. For more information on this particular classifier see the reference [ref](https://arxiv.org/abs/1703.10793).

[Back to Table of Contents](#contents)

### References

* Book: [Schuld and Petruccione, Supervised learning with Quantum computers, 2018](https://www.springer.com/us/book/9783319964232)
* Article: [Schuld, Fingerhuth and Petruccione, Implementing a distance-based classifier with a quantum interference circuit, 2017](https://arxiv.org/abs/1703.10793)

## The same algorithm for the projectQ framework

```python
from math import acos
import os

from quantuminspire.api import QuantumInspireAPI
from quantuminspire.projectq.backend_qx import QIBackend

from projectq import MainEngine
from projectq.backends import ResourceCounter
from projectq.ops import CNOT, CZ, H, Toffoli, X, Ry, C
from projectq.setups import restrictedgateset

from quantuminspire.credentials import get_authentication

QI_URL = os.getenv('API_URL', 'https://api.quantum-inspire.com/')

# Remote Quantum Inspire backend #
authentication = get_authentication()
qi_api = QuantumInspireAPI(QI_URL, authentication)

compiler_engines = restrictedgateset.get_engine_list(one_qubit_gates="any",
                                                     two_qubit_gates=(CNOT, CZ, Toffoli))
compiler_engines.extend([ResourceCounter()])

qi_backend = QIBackend(quantum_inspire_api=qi_api)
qi_engine = MainEngine(backend=qi_backend, engine_list=compiler_engines)

# angles data points:
angle_x_tilde = 2 * acos(0.8670)
angle_x0 = 2 * acos(0.1411)
angle_x1 = 2 * acos(0.9193)

qubits = qi_engine.allocate_qureg(4)

# part_a
for qubit in qubits[0:2]:
    H | qubit

# part_b
C(Ry(angle_x_tilde), 1) | (qubits[1], qubits[2]) # Alternatively build own CRy gate as done above
X | qubits[1]

# part_c
C(Ry(angle_x0), 2) | (qubits[0], qubits[1], qubits[2]) # Alternatively build own CCRy gate as done above
X | qubits[0]

# part_d
C(Ry(angle_x1), 2) | (qubits[0], qubits[1], qubits[2]) # Alternatively build own CCRy gate as done above

# part_e
CNOT | (qubits[0], qubits[3])

# part_f
H | qubits[1]

qi_engine.flush()

# Results:
temp_results = qi_backend.get_probabilities(qubits)

res = [get_bin(el, 4) for el in range(16)]
prob = [0] * 16
for key, value in temp_results.items():
    prob[int(key[::-1], 2)] = value  # Reverse as projectQ has a different qubit ordering

color_list = [
    'red', 'red', (0.1, 0.1, 0.1, 0.1), (0.1, 0.1, 0.1, 0.1),
    'red', 'red', (0.1, 0.1, 0.1, 0.1), (0.1, 0.1, 0.1, 0.1),
    'blue', 'blue', (0.1, 0.1, 0.1, 0.1), (0.1, 0.1, 0.1, 0.1),
    'blue', 'blue', (0.1, 0.1, 0.1, 0.1), (0.1, 0.1, 0.1, 0.1)
             ]
plt.bar(res, prob, color=color_list)
plt.ylabel('Probability')
plt.title('Results')
plt.ylim(0, 1)
plt.xticks(rotation='vertical')
plt.show()
print("Results:")
print(temp_results)
```

```python

```


```python
# "magic" commands, prefaced with "%", changes settings in the notebook

# this ensures plots are embedded in notebook web page
%matplotlib inline

# pdb = Python debugger, so this command turns the debugger OFF
%pdb off
```

    Automatic pdb calling has been turned OFF

```python
# numpy = numerical Python, implements arrays (/ matrices)
import numpy as np
# limit number of decimal places printed for floating-point numbers
np.set_printoptions(precision=3)

# scipy = scientific Python, implements operations on arrays / matrices
import scipy as sp
# linalg = linear algebra, implements eigenvalues, matrix inverse, etc
from scipy import linalg as la
# optimize = optimization, root finding, etc
from scipy import optimize as op

# produce matlab-style plots
import matplotlib as mpl
# increase font size on plots
mpl.rc('font',**{'size':18})
# use LaTeX to render symbols
mpl.rc('text',usetex=False)
# animation
from matplotlib import animation as ani
# Matlab-style plotting
import matplotlib.pyplot as plt

# symbolic computation, i.e. computer algebra (like Mathematica, Wolfram Alpha)
import sympy as sym

```

```python
def numerical_simulation(f,t,x,t0=0.,dt=1e-4,ut=None,ux=None,utx=None,return_u=False):
  """
  simulate x' = f(x,u)

  input:
    f : R x X x U --> X - vector field
      X - state space (must be vector space)
      U - control input set
    t - scalar - final simulation time
    x - initial condition; element of X

    (optional:)
    t0 - scalar - initial simulation time
    dt - scalar - stepsize parameter
    return_u - bool - whether to return u_

    (only one of:)
    ut : R --> U
    ux : X --> U
    utx : R x X --> U

  output:
    t_ - N array - time trajectory
    x_ - N x X array - state trajectory
    (if return_u:)
    u_ - N x U array - state trajectory
  """
  t_,x_,u_ = [t0],[x],[]

  inputs = sum([1 if u is not None else 0 for u in [ut,ux,utx]])
  assert inputs <= 1, "more than one of ut,ux,utx defined"

  if inputs == 0:
    assert not return_u, "no input supplied"
  else:
    if ut is not None:
      u = lambda t,x : ut(t)
    elif ux is not None:
      u = lambda t,x : ux(x)
    elif utx is not None:
      u = lambda t,x : utx(t,x)

  while t_[-1]+dt < t:
    if inputs == 0:
      _t,_x = t_[-1],x_[-1]
      dx = f(t_[-1],x_[-1]) * dt
    else:
      _t,_x,_u = t_[-1],x_[-1],u(t_[-1],x_[-1])
      dx = f(_t,_x,_u) * dt
      u_.append( _u )

    x_.append( _x + dx )
    t_.append( _t + dt )

  if return_u:
    return np.asarray(t_),np.asarray(x_),np.asarray(u_)
  else:
    return np.asarray(t_),np.asarray(x_)
```

## phase portrait

```python
g = 9.81
m = 75000.0 # kg
ell = 40 # m
alpha = 0.1# friction
ut = lambda t : 0

dt = .01

def f(t,x,u):
  q, dq = x
  # u = 0
  ddq = g/ell*np.sin(q)-alpha*dq#+1/(m*ell)*u*np.cos(q)
  return np.asarray([q,dq])

plt.figure(figsize=(5,5))

# simulation time
t = 15

# phase portrait / "quiver" plot
ax = plt.subplot(1,1,1)
X, Y = np.meshgrid(np.linspace(-1,+1, 11), np.linspace(-1, +1, 11))
dX,dY = np.asarray([f(0.,(x,y),0.).flatten() for x,y in zip(X.flatten(),Y.flatten())]).T
dX,dY = dX.reshape(X.shape),dY.reshape(Y.shape)
#ax.quiver(X,Y,dX,dY)
ax.streamplot(X,Y,dX,dY,density=2.,color=(0,0,0,.5))
ax.set_xlabel(r'$x_1$')
ax.set_ylabel(r'$x_2$')

for _ in range(10):
  # random initial condition in [-1,+1] x [-1,+1] square
  x0 = 2*(np.random.rand(2) - .5)
  t_,x_ = numerical_simulation(f,t,x0,dt=dt,ut=ut)
  ax.plot(x_[:,0],x_[:,1])

t_,x_ = numerical_simulation(f,t,[.5,0],dt=dt,ut=ut)
ax.plot(x_[:,0],x_[:,1],'r',lw=4)
t_,x_ = numerical_simulation(f,t,[0,.5],dt=dt,ut=ut)
ax.plot(x_[:,0],x_[:,1],'b',lw=4)

ax.set_xlim((-1,+1))
ax.set_ylim((-1,+1))

plt.tight_layout()
```

```python
A = np.array([[0,1],[g/ell, -alpha/ell**2]])
print('A')
print(A)
eigenvalue, _ = np.linalg.eig(A)
print('eigenvalue of A')
print(eigenvalue)
```

    A
    [[ 0.000e+00  1.000e+00]
     [ 2.453e-01 -6.250e-05]]
    eigenvalue of A
    [ 0.495 -0.495]

```python
sym.var("k_P s")
C = k_P+1/s
P = 1/(s-1)
Gs = P*C/(1+P*C)
Gs = Gs.simplify().cancel()
Gs
```

$\displaystyle \frac{k_{P} s + 1}{k_{P} s + s^{2} - s + 1}$

```python

```


# Tutoriel MID Contrôle

On traite le quasipolynôme suivant :
$$
Q(s) = s^n+a_{n-1}s^{n-1}+...+a_0 + \left[\alpha_ms^m+...+\alpha_0\right]e^{-s\tau}
$$

Cette fois-ci, on considère que l'on connait les paramètres du systèmes $a_i$ ainsi que l'un des hyperparamètres $s_0$ ou $\tau$. Ainsi, nous allons utiliser la MID pour trouver les $\alpha_j$ et l'hyperparamètre manquant.

## Packages nécessaires

On utilise numpy pour les opérations mathématiques numériques telles que les opérations matricielles (produit matriciel par exemple).

Sympy permet de réaliser des calculs en symbolique sur Python et donc de travailler avec des expressions mathématiques et en numérique.

```python
# pip install sympy si nécessaire (module not found), pareil avec cxroots
%matplotlib notebook
import numpy as np
import sympy as sp
import cxroots as cx
import matplotlib.pyplot as plt
import traceback

```

## Paramètres
$n$ est le degré du polynôme non retardé, $m$ est celui du polynôme retardé (celui multiplié par l'exponentielle).

$s_0$ est le taux de décroissance exponentiel. Il s'agit de la racine dominante que l'on souhaite assigner (celle la plus à droite lorsqu'on plot les racines). Celui-ci doit être négatif pour que le système soit stable. Un $s_0$ positif signifie donc que le système est instable.

$\tau$ est le retard considéré.

```python
n = 2
m = 1

```

## Création de symboles

Les variables s, tau, a et alpha vont désormais correspondre à des symboles et non des variables numériques.

On considère dans nos cas concrets que les $a_i$ correspondent aux paramètres du système (de son équation) et les $\alpha_j$ correspondent aux gains de la loi de commande retardée.

```python
s = sp.symbols('s')  # define variable s for our problem to be solved
tau = sp.symbols('tau')  # define variable tau : delay

a = sp.symbols(["a{:d}".format(i) for i in range(n)], real=True)
alpha = sp.symbols(["alpha{:d}".format(i) for i in range(m + 1)], real=True)
avalue = [2, 1]

```

## Création du quasipolynome

```python
Polynomial = s**n + np.array(a).dot([s**i for i in range(n)]) # Revient à faire s^n + a_{n-1}^{n-1}...
Delayed = np.array(alpha).dot([s**i for i in range(m+1)])*sp.exp(-s*tau) # Revient à faire
#b^m*s^m + b_{m-1}^{m-1}...
Q = Polynomial + Delayed
Q
```

$\displaystyle a_{0} + a_{1} s + s^{2} + \left(\alpha_{0} + \alpha_{1} s\right) e^{- s \tau}$

## Créer le système à résoudre

Dans ce cadre de MID orientée contrôle, on a $m+2$ inconnues qui sont les $\alpha_j$ ($m+1$ variables de $0$ à $m$ inclus) et l'hyperparamètre manquant

On parle donc de multiplicité de $m+2$ et on souhaite donc avoir $m+2$ équations. Pour l'instant nous avons une première équation qui correspond à notre quasipolynôme (=0).

Il y a deux possibilités :

- $s_0$ est donné
- $\tau$ est donné

Dans ces deux cas, il faudra procéder en deux étapes en considérant d'abord les $m+1$ premières équations (toutes sauf la dernière) que l'on résoudra facilement en $\alpha_j$ (système linéaire en $\alpha$) et on remplacera ensuite les $\alpha_j$ dans la dernière équation avec les expressions trouvées ce qui nous fera une équation permettant de trouver l'hyperparamètre manquant.

Il faut donc calculer les $m+1$ dérivées suivantes ce qui se fait très simplement avec Sympy en utilisant la méthode <i>diff</i> en spécifiant en argument la variable de différenciation.

```python
SysDerivatif = [Q]
print(Q)
for i in range(m+1):
    DerniereDerivee = SysDerivatif[-1]
    SysDerivatif.append(DerniereDerivee.diff(s)) # Dérivée par rapport à s
    print(SysDerivatif[-1])
```

    a0 + a1*s + s**2 + (alpha0 + alpha1*s)*exp(-s*tau)
    a1 + alpha1*exp(-s*tau) + 2*s - tau*(alpha0 + alpha1*s)*exp(-s*tau)
    -2*alpha1*tau*exp(-s*tau) + tau**2*(alpha0 + alpha1*s)*exp(-s*tau) + 2

## Résolution du système

Après avoir créé le système dérivatif, il suffit de le résoudre avec Sympy en spécifiant bien que l'on souhaite obtenir les expressions des $a_i$ et des $\alpha_j$.

Ici, on a bien $n+m+1 = 4$ ce qui nous retourne $a_0, a_1, \alpha_0$ et $\alpha_1$ en gardant bien à l'esprit que $a_2=1$.

Le ".args[0]" sert à extraire les solutions à partir du solveur qui retourne un set.

```python
sol = sp.linsolve(SysDerivatif[:-1], alpha).args[0] # Solveur selon les alpha et les a
sol
```

$\displaystyle \left( \left(a_{0} s \tau - a_{0} + a_{1} s^{2} \tau + s^{3} \tau + s^{2}\right) e^{s \tau}, \  - \left(a_{0} \tau + a_{1} s \tau + a_{1} + s^{2} \tau + 2 s\right) e^{s \tau}\right)$

### $\tau$ connu

On cherche donc à trouver $s_0$ et on choisit le $s_0$ réel **négatif** qui est le **plus proche de $0$** s'il existe.

```python
def solve_tau_connu(tau_val, acoef, Q):
    sys = [Q]
    for i in range(m+1):
        DerniereDerivee = sys[-1]
        sys.append(DerniereDerivee.diff(s)) # Dérivée par rapport à s
    sol = sp.linsolve(SysDerivatif[:-1], alpha).args[0]
    finaleq = SysDerivatif[-1].subs({alph : alphacoef for alph, alphacoef in zip(alpha, sol)})
    finaleq = finaleq.subs({asymb: aval for asymb, aval in zip(a, acoef)})
    solS0 = finaleq.subs({tau : tau_val})
    solS0 = sp.solve(solS0)
    solS0eval = [i.evalf() for i in solS0]
    try :
        solution = max([i for i in solS0eval if i<0])
    except Exception :
        traceback.print_exc()
    return solution
```

```python
value_tau = 0.738

finaleq = SysDerivatif[-1].subs({alph : alphacoef for alph, alphacoef in zip(alpha, sol)}) #remplace les coeffs
finaleq = finaleq.subs({asymb: aval for asymb, aval in zip(a, avalue)})
print(finaleq)
solS0 = finaleq.subs({tau : value_tau})
solS0 = sp.solve(solS0)
print(solS0)
solS0eval = [i.evalf() for i in solS0]
```

    tau**2*(-s*(s**2*tau + s*tau + 2*s + 2*tau + 1)*exp(s*tau) + (s**3*tau + s**2*tau + s**2 + 2*s*tau - 2)*exp(s*tau))*exp(-s*tau) + 2*tau*(s**2*tau + s*tau + 2*s + 2*tau + 1) + 2
    [-4.59643376575614, -1.82362043478586]

```python
solve_tau_connu(0.738, [2, 1], Q) # Safety check on function
```

$\displaystyle -1.82362043478586$

```python
computedS0 = solS0[1]
print(computedS0)
alpha_num = sol.subs({asymb: aval for asymb, aval in zip(a, avalue)})
alpha_num = alpha_num.subs({s : computedS0})
alpha_num = alpha_num.subs({tau : value_tau})
alpha_num_eval = [i.evalf() for i in alpha_num]
alpha_num_eval
print (alpha_num_eval)
alpha_sens = alpha_num_eval
```

    -1.82362043478586
    [-0.881837285449719, 0.0163447037006065]

### $s_0$ connu

On cherche donc à trouver $\tau$ et on choisit le $\tau$ réel **positif** qui est le **plus proche de $0$** s'il existe.

```python
# value_s0 = -1.8236
value_s0 = -1.8

finaleq = SysDerivatif[-1].subs({alph : alphacoef for alph, alphacoef in zip(alpha, sol)}) #remplace les coeffs
finaleq = finaleq.subs({asymb: aval for asymb, aval in zip(a, avalue)})
solTau = finaleq.subs({s : value_s0})
solTau = sp.solve(solTau)
print(solTau)
```

    [0.755813953488372 - 0.100700628347028*I, 0.755813953488372 + 0.100700628347028*I]

```python
computedTau = solTau[0]
print (computedTau)

if type(computedTau).__name__ == "Add":

    computedTau = computedTau.as_real_imag()[0].evalf(10)
    print ("computedTau apres transformation add ",computedTau)

elif type(computedTau).__name__ == "Float":

    computedTau = float(computedTau.evalf(10))
    print ("computedTau apres transformation float ",computedTau)

computedTau = round(computedTau,4)
print ("computedTau apres arrondissement ",computedTau)

alpha_num = sol.subs({asymb: aval for asymb, aval in zip(a, avalue)})
alpha_num = alpha_num.subs({tau : computedTau})
alpha_num = alpha_num.subs({s : value_s0})
alpha_num_eval = [i.evalf() for i in alpha_num]
print (alpha_num_eval)
```

    0.755813953488372 - 0.100700628347028*I
    computedTau apres transformation add  0.7558139535
    computedTau apres arrondissement float  0.7558
    [-0.882502510615450, 1.37794678445848e-5]

On retrouve logiquement les même valeurs de gain lorsque nous avons le couple $s_0$ et $\tau$ correspondant. Dans cet exemple on a soit fixé $\tau$ à $0.738$ et calculé $s_0$ qui est environ égal à $-1.5$ selon nos critères soit, nous avons fixé $s_0$ à $-1.5$ et le $\tau$ calculé est environ égal à $0.738$.

Le choix de fixer $s_0$ ou $\tau$ se fait selon si l'on connait le retard de commande ou si l'on souhaite fixer la vitesse de convergence $s_0$.

Ces hyperparamètres ($s_0$ ou $\tau$) dépend évidemment des coefficients du système $a_i$ et donc, selon les valeurs des coefficients, certaines valeurs d'hyperparamètres sont possibles ou non. Pour cela, on peut tracer le graphe d'admissibilité.

```python
polyAdm = SysDerivatif[-1].subs({alph : alphacoef for alph, alphacoef in zip(alpha, sol)})
polyAdm = polyAdm.subs({asymb: aval for asymb, aval in zip(a, avalue)})
sp.simplify(polyAdm)
```

$\displaystyle s^{2} \tau^{2} + s \tau^{2} + 4 s \tau + 2 \tau^{2} + 2 \tau + 2$

# Admissibilité

```python
s0range = np.arange(-10, 0, 0.01)
taurange = np.arange(0, 10, 0.01)

func = sp.lambdify([s, tau], polyAdm)

fig, ax = plt.subplots()
X, Y = np.meshgrid(s0range, taurange)
z = func(X, Y)
CS = ax.contour(X, Y, z, [0])
ax.grid()
plt.xlabel(r"$s_0$")
plt.ylabel(r"$\tau$")
plt.title("Plot Admissibilité")
```

    <IPython.core.display.Javascript object>

    Text(0.5, 1.0, 'Plot Admissibilité')

# Sensibilité en $\tau$

```python
tau_nominal = value_tau
step = 1e-2
nbIt = 10
```

```python
values = [0] + [-step * i for i in range(1, nbIt + 1)] + [step * i for i in range(1, nbIt + 1)]
```

```python
values.sort()
```

```python
tau_sens = []
s0_sens = []
for value in values :
    tau_sens.append(tau_nominal + value)
    s0_sens.append(solve_tau_connu(tau_sens[-1], [2, 1], Q))
```

```python
s0_sens
```

    [-1.85618056091116,
     -1.85062211334813,
     -1.84560644930057,
     -1.84111940350753,
     -1.83714828538220,
     -1.83368182726193,
     -1.83071014356143,
     -1.82822470069588,
     -1.82621829782361,
     -1.82468505865446,
     -1.82362043478586,
     -1.82302122127399,
     -1.82288558543080,
     -1.82321311017491,
     -1.82400485367054,
     -1.82526342748842,
     -1.82699309614456,
     -1.82919990165969,
     -1.83189181778737,
     -1.83507893986148,
     -1.83877371791975]

```python
def getRoots(m, Q, dominancy, delay, avalue, alphavalue, xwindow, ywindow):
    derivees = [Q, Q.diff(s)]
    for i in range(len(derivees)) :
        derivees[i] = derivees[i].subs({ai: ai_num for ai, ai_num in zip(a, avalue)})
        derivees[i] = derivees[i].subs({alphai: alphai_num for alphai, alphai_num in zip(alpha, alphavalue)})
        derivees[i] = derivees[i].subs({tau : delay})
    func = [sp.lambdify(s, i) for i in derivees]
    rect = cx.Rectangle(xwindow, ywindow)
    root_count = rect.count_roots(func[0])
    roots = rect.roots(func[0], func[1], rootErrTol=1e-5, absTol=1e-5, M=m+2)
    xroot, yroot = np.real(roots[0]), np.imag(roots[0])
    return xroot, yroot
```

```python
sensIterations = len(tau_sens)
bList = np.linspace(start=255, stop=0, num=sensIterations//2) #Blue to black
rList = np.linspace(start=0, stop=255, num=sensIterations//2) #black to red
if len(bList)+len(rList)==sensIterations-1 :
    zerosToFill = [0] * (sensIterations//2 + 1)
else :
    zerosToFill = [0] * (sensIterations//2 )
bList = [*bList, *zerosToFill]
rList = [*zerosToFill, *rList]

```

```python
colorArray = []
for i in range(len(tau_sens)):
    colorArray.append([rList[i], 0, bList[i]])
colorArray = np.array(colorArray)
```

```python
import matplotlib.cm as cm
scaler = lambda x : [i / 255 for i in x]
plt.figure()
for i in range(len(tau_sens)):
    xroot, yroot = getRoots(1, Q, s0_sens[i], tau_sens[i],
                            avalue, alpha_sens, [-5, 5], [-5, 5])
    plt.scatter(xroot, yroot, c=[scaler(colorArray[i])])
plt.grid()
plt.xlabel(r"$Re(s)$")
plt.ylabel(r"$Im(s)$")
tau_sens.sort()
plt.title(r"Sensitivity plot, $\tau \in $"+f"[{tau_sens[0]}, {tau_sens[-1]}]")
```

    <IPython.core.display.Javascript object>

    Text(0.5, 1.0, 'Sensitivity plot, $\\tau \\in $[0.638, 0.838]')


# Statistics

## 1.1 Sales for the past week was the following amounts: [3505, 2400, 3027, 2798, 3700, 3250, 2689]. Without using library functions, what is the mean, variance, and standard deviation of of sales from last week? (for extra bonus points, write your own function that can calculate these two values for any sized list)

```
import pandas as pd
import numpy as np
```

```
sales = [3505, 2400, 3027, 2798, 3700, 3250, 2689]

def calc_mvs(somelist):

  total = sum(somelist)
  mean = total/len(somelist)
  distances = []

  for item in somelist:
    distance = item - mean
    distances.append(distance**2)

  variance = sum(distances)/ len(distances)

  std_dev = variance**.5

  return mean, variance, std_dev

print(np.std(sales))
calc_mvs(sales)

```

    428.67360686714756

    (3052.714285714286, 183761.06122448976, 428.67360686714756)

## 1.2 Find the covariance between last week's sales numbers and the number of customers that entered the store last week: [127, 80, 105, 92, 120, 115, 93] (you may use librray functions for calculating the covariance since we didn't specifically talk about its formula)

```
customer_tally = [127, 80, 105, 92, 120, 115, 93]

df_0 = pd.DataFrame({1: sales, 2: customer_tally})
df.columns = ['sales', 'customer_tally']

df.cov()

```

      sales
      customer_tally

      sales
      214387.904762
      7604.357143

      customer_tally
      7604.357143
      290.952381

## 1.3 Find the standard deviation of customers who entered the store last week. Then, use the standard deviations of both sales and customers to standardize the covariance to find the correlation coefficient that summarizes the relationship between sales and customers. (You may use library functions to check your work.)

```
print(calc_mvs(sales)[2])
calc_mvs(customer_tally)[2]
```

    428.67360686714756

    15.792015549069118

```

def calc_corr_coef(list1, list2):
  x = np.mean(list1)
  y = np.mean(list2)

  sx = calc_mvs(list1)[2]
  sy = calc_mvs(list2)[2]

  df = pd.DataFrame({1:list1, 2: list2})
  df.columns = ['list_1', 'list_2']

  df['x_less_mean'] = df['list_1'] - x
  df['y_less_mean'] = df['list_2'] - y
  df['numerator_val'] = df['x_less_mean']*df['y_less_mean']

  numer_totals = df.numerator_val.sum()

  by_std_dev = numer_totals/(sx*sy)

  r = by_std_dev/ len(list1)

  print(f"r = {r.round(5)}")

calc_corr_coef(sales, customer_tally)

```

    r =0.96283

```
df_0.corr()
```

      1
      2

      1
      1.000000
      0.962834

      2
      0.962834
      1.000000

## 1.4 Use pandas to import a cleaned version of the titanic dataset from the following link: [Titanic Dataset](https://raw.githubusercontent.com/Geoyi/Cleaning-Titanic-Data/master/titanic_clean.csv)

## Calculate the variance-covariance matrix and correlation matrix for the titanic dataset's numeric columns. (you can encode some of the categorical variables and include them as a stretch goal if you finish early)

```
titanic_clean = pd.read_csv('https://raw.githubusercontent.com/Geoyi/Cleaning-Titanic-Data/master/titanic_clean.csv')
```

```
titanic_clean.cov()
```

      Unnamed: 0
      pclass
      survived
      age
      sibsp
      parch
      fare
      body
      has_cabin_number

      Unnamed: 0
      143117.500000
      284.357034
      -53.967125
      -1442.939812
      25.828746
      1.172783
      -9410.735123
      591.579132
      -95.438885

      pclass
      284.357034
      0.701969
      -0.127248
      -3.954605
      0.053090
      0.013287
      -24.227788
      -2.876653
      -0.249992

      survived
      -53.967125
      -0.127248
      0.236250
      -0.314343
      -0.014088
      0.034776
      6.146023
      0.000000
      0.061406

      age
      -1442.939812
      -3.954605
      -0.314343
      165.850021
      -2.559806
      -1.459378
      114.416613
      81.622922
      1.463138

      sibsp
      25.828746
      0.053090
      -0.014088
      -2.559806
      1.085052
      0.336833
      8.641768
      -8.708471
      -0.003946

      parch
      1.172783
      0.013287
      0.034776
      -1.459378
      0.336833
      0.749195
      9.928031
      4.237190
      0.013316

      fare
      -9410.735123
      -24.227788
      6.146023
      114.416613
      8.641768
      9.928031
      2678.959738
      -179.164684
      10.976961

      body
      591.579132
      -2.876653
      0.000000
      81.622922
      -8.708471
      4.237190
      -179.164684
      9544.688567
      3.625689

      has_cabin_number
      -95.438885
      -0.249992
      0.061406
      1.463138
      -0.003946
      0.013316
      10.976961
      3.625689
      0.174613

```
titanic_clean.corr()
```

      Unnamed: 0
      pclass
      survived
      age
      sibsp
      parch
      fare
      body
      has_cabin_number

      Unnamed: 0
      1.000000
      0.897822
      -0.293717
      -0.296172
      0.065594
      0.003584
      -0.481215
      0.015558
      -0.603727

      pclass
      0.897822
      1.000000
      -0.312469
      -0.366370
      0.060832
      0.018322
      -0.558629
      -0.034642
      -0.713857

      survived
      -0.293717
      -0.312469
      1.000000
      -0.050199
      -0.027825
      0.082660
      0.244265
      NaN
      0.302250

      age
      -0.296172
      -0.366370
      -0.050199
      1.000000
      -0.190747
      -0.130872
      0.171892
      0.059059
      0.271887

      sibsp
      0.065594
      0.060832
      -0.027825
      -0.190747
      1.000000
      0.373587
      0.160238
      -0.099961
      -0.009064

      parch
      0.003584
      0.018322
      0.082660
      -0.130872
      0.373587
      1.000000
      0.221539
      0.051099
      0.036806

      fare
      -0.481215
      -0.558629
      0.244265
      0.171892
      0.160238
      0.221539
      1.000000
      -0.043110
      0.507253

      body
      0.015558
      -0.034642
      NaN
      0.059059
      -0.099961
      0.051099
      -0.043110
      1.000000
      0.083796

      has_cabin_number
      -0.603727
      -0.713857
      0.302250
      0.271887
      -0.009064
      0.036806
      0.507253
      0.083796
      1.000000

# Orthogonality

## 2.1 Plot two vectors that are orthogonal to each other. What is a synonym for orthogonal?

```
import matplotlib.pyplot as plt

vector_1 = [0, 5]
vector_2 = [5, 0]

plt.arrow(0,0, vector_1[0], vector_1[1],head_width=.05, head_length=0.05, color ='red')
plt.arrow(0,0, vector_2[0], vector_2[1],head_width=.05, head_length=0.05, color ='green')
plt.xlim(-10,10)
plt.ylim(-10,10)
plt.title("Orthogonal Vectors")
plt.show()
```

## 2.2 Are the following vectors orthogonal? Why or why not?

\begin{align}
a = \begin{bmatrix} -5 \\ 3 \\ 7 \end{bmatrix}
\qquad
b = \begin{bmatrix} 6 \\ -8 \\ 2 \end{bmatrix}
\end{align}

```
a = np.array([-5, 3, 7])
b = np.array([6, -8, 2])

a.dot(b)

# They are not orthogonal because the dot product of the two vectors does not equal 0
```

    -40

## 2.3 Compute the following values: What do these quantities have in common?

## What is $||c||^2$?

## What is $c \cdot c$?

## What is $c^{T}c$?

\begin{align}
c = \begin{bmatrix} 2 & -15 & 6 & 20 \end{bmatrix}
\end{align}

```
c = np.array([2, -15, 6, 20])

norm_sq = np.linalg.norm(c)**2

cc = c.dot(c)

ctc = c.T.dot(c)

```

```
norm_sq
```

    665.0

```
cc
```

    665

```
ctc
```

    665

# Unit Vectors

## 3.1 Using Latex, write the following vectors as a linear combination of scalars and unit vectors:

\begin{align}
d = \begin{bmatrix} 7 \\ 12 \end{bmatrix}
\qquad
e = \begin{bmatrix} 2 \\ 11 \\ -8  \end{bmatrix}
\end{align}

\begin{align}
d = \begin{bmatrix} 7 \\ 12 \end{bmatrix} = 7\begin{bmatrix} 1 \\ 0 \end{bmatrix} + 12\begin{bmatrix} 0 \\ 1 \end{bmatrix} = 7\hat{i} + 12\hat{j}
\end{align}

\begin{align}
e = \begin{bmatrix} 2 \\ 11 \\ -8 \end{bmatrix} = 2\begin{bmatrix} 1 \\ 0 \\ 0 \end{bmatrix} + 11\begin{bmatrix} 0 \\ 1 \\ 0\end{bmatrix} - 8\begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix} = 2\hat{i} + 11\hat{j} - 8\hat{k}
\end{align}

## 3.2 Turn vector $f$ into a unit vector:

\begin{align}
f = \begin{bmatrix} 4 & 12 & 11 & 9 & 2 \end{bmatrix}
\end{align}

\begin{align}
 ||f|| = \sqrt{4^2 + 12^2 + 11^2 + 9^2 +4^2 } = \sqrt{16 + 144 + 121 +81 +4} = \sqrt{366}
\end{align}

 \begin{align}
 \hat{f} = \frac{1}{||f||}f = \frac{1}{\sqrt 366}\begin{bmatrix} 4 \\ 12 \\ 11 \\ 9 \\ 2 \end{bmatrix} = \begin{bmatrix} \frac{4}{\sqrt 366} \\ \frac{12}{\sqrt 366} \\  \frac{11}{\sqrt 366} \\ \frac{9}{\sqrt 366} \\ \frac {2}{\sqrt 366} \end{bmatrix} = \begin{bmatrix} \frac{2 \sqrt 2}{\sqrt 183} \\ \frac{2 \sqrt 6}{\sqrt 61} \\ \frac {11}{\sqrt 366} \\ \frac{3 \sqrt 3}{\sqrt 122} \\ \sqrt \frac{2}{183} \end{bmatrix}
\end{align}

# Linear Independence / Dependence

## 4.1 Plot two vectors that are linearly dependent and two vectors that are linearly independent (bonus points if done in $\mathbb{R}^3$).

```
# Axis Bounds
plt.xlim(0,10)
plt.ylim(0,10)

# Original Vector
v = [3,6]

# Scaled Vector
v2 = np.multiply(3, v)

plt.arrow(0,0, v2[0], v2[1], linewidth=6, head_width=.05, head_length=0.05, color ='r')
plt.arrow(0,0, v[0], v[1], linewidth=3, head_width=.05, head_length=0.05, color ='b')

plt.title("Linearly Dependent Vectors")
plt.show()
```

```
plt.xlim(-20,20)
plt.ylim(-20,20)

a = [-7,-10]
b = [-5, 13]

plt.arrow(0,0, a[0], a[1], linewidth=3, head_width=.05, head_length=0.05, color ='b')
plt.arrow(0,0, b[0], b[1], linewidth=3, head_width=.05, head_length=0.05, color ='r')

plt.title("Linearly Independent Vectors")
plt.show()
```

# Span

## 5.1 What is the span of the following vectors?

\begin{align}
g = \begin{bmatrix} 1 & 2 \end{bmatrix}
\qquad
h = \begin{bmatrix} 4 & 8 \end{bmatrix}
\end{align}

```
# Vectors are linearly dependent so their span is 1
```

## 5.2 What is the span of $\{l, m, n\}$?

\begin{align}
l = \begin{bmatrix} 1 & 2 & 3 \end{bmatrix}
\qquad
m = \begin{bmatrix} -1 & 0 & 7 \end{bmatrix}
\qquad
n = \begin{bmatrix} 4 & 8  & 2\end{bmatrix}
\end{align}

```
# Independent vectors span 3
```

# Basis

## 6.1 Graph two vectors that form a basis for $\mathbb{R}^2$

```
a = [2, 8]
b = [1, 3]
plt.grid()
plt.arrow(0,0, a[0], a[1], linewidth=3, head_width=0.5, head_length=0.5)
plt.arrow(0,0,b[0],  b[1], color = 'r', linewidth=3, head_width=0.5, head_length=0.5)

plt.xlim(-10, 10)
plt.ylim(-10, 10)
plt.show()
```

## 6.2 What does it mean to form a basis?

A basis is n linearly independent vectors n being the dimensions of the space

# Rank

## 7.1 What is the Rank of P?

\begin{align}
P = \begin{bmatrix}
1 & 2 & 3 \\
 -1 & 0 & 7 \\
4 & 8  & 2
\end{bmatrix}
\end{align}

```
p = np.array([[1,2,3], [-1,0,7], [4,8,2]])
np.linalg.matrix_rank(p)

```

    3

## 7.2 What does the rank of a matrix tell us?

The rank of a matrix is the number of dimensions it can span in space

# Linear Projections

## 8.1 Line $L$ is formed by all of the vectors that can be created by scaling vector $v$
\begin{align}
v = \begin{bmatrix} 1 & 3 \end{bmatrix}
\end{align}

\begin{align}
w = \begin{bmatrix} -1 & 2 \end{bmatrix}
\end{align}

## find $proj_{L}(w)$

## graph your projected vector to check your work (make sure your axis are square/even)

```
 # Bounds
plt.xlim(-2, 2)
plt.ylim(-1, 4)

axes = plt.gca()
plt.axes().set_aspect('equal')

v = np.array([1, 3])
w = np.array([-1, 2])
proj = np.multiply((np.vdot(w, v)/np.vdot(v,v)), v)

plt.grid()

plt.arrow(0,0,
          v[0], v[1],
          linewidth=8,
          head_width=0.05,
          head_length=0.05,
          color = 'g')
plt.arrow(0,0,
          w[0], w[1],
          color = 'r',
          linewidth=8,
          head_width=0.05,
          head_length=0.05)
plt.arrow(0,0,
          proj[0],
          proj[1],
          linewidth=8,
          head_width=0.05,
          head_length=0.05)

```

# Stretch Goal

## For vectors that begin at the origin, the coordinates of where the vector ends can be interpreted as regular data points. (See 3Blue1Brown videos about Spans, Basis, etc.)

## Write a function that can calculate the linear projection of each point (x,y) (vector) onto the line y=x. run the function and plot the original points in blue and the new projected points on the line y=x in red.

## For extra points plot the orthogonal vectors as a dashed line from the original blue points to the projected red points.

```
import pandas as pd
import matplotlib.pyplot as plt

# Creating a dataframe for you to work with -Feel free to not use the dataframe if you don't want to.
x_values = [1, 4, 7, 3, 9, 4, 5 ]
y_values = [4, 2, 5, 0, 8, 2, 8]

data = {"x": x_values, "y": y_values}

df = pd.DataFrame(data)

df.head()

plt.scatter(df.x, df.y)
plt.show()
```

```

```