# 1. K-Armed Bandit Problem
Bandit problems are reinforcement learning (RL) problems in which there is only a single state in which multiple actions can potentially be taken. In the k-armed bandit problem, you are repeatedly faced with a choice among k different options/actions. After selecting an action, you receive a reward chosen from a stationary probability distribution that is dependent on the selected action.
#### Objective: Maximize the expected total reward over some time period

Although the rewards for actions are chosen from a probability distribution, each action has a mean reward value. We start out with estimates of the rewards for each action, but with more selections/experience, the estimates converge to the mean. If we have a 'way' to quantify the value of taking each action (the expected reward), then to achieve the objective, we would simply always take the action with the highest value. Mathematically speaking,

$Q_t(a) = E[R_t | A_t = a]$

This says that the value of an arbitrary action **a** is the expected reward given that **a** is selected

* If we keep estimates of the action values, then at each step, there is at least one action whose estimate is the greatest. These actions are called **greedy actions** and if selected, we are said to be **exploting** our current knowledge of the values of actions
* If instaed we select a non-greedy action, then we are said to be **exploring** because it enables us to improve our estimates of the non-greedy action values

##  Sample-Average Action-Value Estimation
A simple method of estimating the value of an action is by averaging the rewards previosly received from taking that
action. i.e.

$$Q_t(a) = \frac{\text{sum of rewards when a is taken prior to t}}{\text{number of times a has been taken prior to t}}$$

The next step is then to use the estimates to select actions. The simplest rule is to select the action (or one of the actions) with the highest estimated values. This is called the **greedy action selection** method and is denoted:

$A_t = argmax_a Q_t(a)$

where $argmax_a$ denotes the value of a at which the expression is maximized
* If multiple actions maximize the expression, then it is important that the tie is broken **arbitrarily**

You may have guessed that being greedy all the time is probably not the best way to go - there may be an unexplored action of higher value than our current greedy choice. An alternative is to be greedy most of the time, but every once in a while, with probability $\epsilon$, select a random action. Methods with this action selection rule are called **$\epsilon$-greedy methods**. This means that with probability $\epsilon$ we select a random action and with probability $1-\epsilon$ we select a greedy action.

## Problem Description
We have a k-armed bandit problem with k = 10. The actual action values, $q_*(a)$ are selected according to a normal distribution with mean 0 and variance 1. When a learning method selects action $A_t$ at time t, the actual reward $R_t$ is selected from a normal distribution with mean $q_*(A_t)$ and variance 1. We'll measure the behavior as it improves over 1000 steps. This makes up one run. We'll execute 2000 independent runs to obtain the learning algorithm's average behavior.

### Environment
* Python 3.5
* numpy
* matplotlib

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

%matplotlib inline
```

```python
def sample_average(actions):
    """
    Returns the action-value for each action using the sample-average method
    :param actions: actions[0] is a tuple for each action of the form (sum of rewards received, no. of times taken)
    """
    results = [0.0 if actions[i][1] == 0 else actions[i][0] / float(actions[i][1]) for i in range(len(actions))]
    return results
```

```python
def get_reward(true_values, a):
    """
    Returns the reward for selecting action a.
    Reward is selected around true_values[a] with unit variance (as in problem description)
    :param true_values: list of expected reward for each action
    :param a: index of action to return reward for
    """
    reward = np.random.normal(true_values[a], size=1)[0]
    return reward
```

```python
def k_armed_bandit(k, epsilon, iterations):
    """
    Performs a single run of the k-armed bandit experiment
    :param k: the number of arms
    :param epsilon: value of epsilon for epoch-greedy action selection
    :param iterations: number of steps in a single run
    """
    # Randomly assign true values of reward for each action with mean 0 and variance 1
    true_values = np.random.normal(size=k)

    # actions[i] is the ith action
    # actions[i][0] is the sum of received rewards for taking action i
    # actions[i][1] is the number of times action i has been taken
    actions = [[0.0, 0] for _ in range(k)]

    # Store the rewards received for this experiment
    rewards = []

    # Track how often the optimal action was selected
    optimal = []
    optimal_action = true_values.argmax()

    for _ in range(iterations):
        prob = np.random.rand(1)

        if prob > epsilon:
            # Greedy (exploit current knowledge)
            action_values = np.array(sample_average(actions))

            # Break ties arbitrarily (reference: http://stackoverflow.com/questions/42071597/numpy-argmax-random-tie-breaking)
            a = np.random.choice(np.flatnonzero(action_values == action_values.max()))
        else:
            # Explore (take random action)
            a = np.random.randint(0, k)

        reward = get_reward(true_values, a)

        # Update statistics for executed action
        actions[a][0] += reward
        actions[a][1] += 1

        rewards.append(reward)
        optimal.append(1 if a == optimal_action else 0)

    return rewards, optimal
```

```python
def experiment(k, epsilon, iters, epochs):
    """
    Runs the k-armed bandit experiment
    :param k: the number of arms
    :param epsilon: the value of epsilon for epoch-greedy action selection
    :param iters: the number of steps in a single run
    :param epochs: the number of runs to execute
    """
    rewards = []
    optimal = []

    for i in range(epochs):
        r, o = k_armed_bandit(k, epsilon, iters)
        rewards.append(r)
        optimal.append(o)

    print('Experiment with \u03b5 = {} completed.'.format(epsilon))

    # Compute the mean reward for each iteration
    r_means = np.mean(rewards, axis=0)
    o_means = np.mean(optimal, axis=0)

    return r_means, o_means
```

```python
k = 10
iters = 1000
runs = 2000

# We experiment with values 0.01, 0.1 and 0 (always greedy)
r_exp1, o_exp1 = experiment(k, 0, iters, runs)
r_exp2, o_exp2 = experiment(k, 0.01, iters, runs)
r_exp3, o_exp3 = experiment(k, 0.1, iters, runs)
```

    Experiment with ε = 0 completed.
    Experiment with ε = 0.01 completed.
    Experiment with ε = 0.1 completed.

```python
x = range(iters)
plt.plot(x, r_exp1, c='green', label='\u03b5 = 0')
plt.plot(x, r_exp2, c='red', label='\u03b5 = 0.01')
plt.plot(x, r_exp3, c='black', label='\u03b5 = 0.1')
plt.xlabel('Steps')
plt.ylabel('Average reward')
plt.legend()
plt.show()
```

```python
plt.plot(x, o_exp1, c='green', label='\u03b5 = 0')
plt.plot(x, o_exp2, c='red', label='\u03b5 = 0.01')
plt.plot(x, o_exp3, c='black', label='\u03b5 = 0.1')
plt.xlabel('Steps')
plt.ylabel('% Optimal action')
plt.legend()
plt.show()
```

From these results we can see that the greedy method ($\epsilon$ = 0) performs the worst. This is because it simply selects the same action each time (the first one that gives a positive reward, remember, all are initialized to 0.0 so have equal liklihood initially). The other experiments involve exploration of varying degrees and so can be seen improving wiith time.

### Incremental Implementation
In the code listing above, we computed the values of actions by summing rewards and then dividing by the number of times a particular action was taken. i.e.
$Q_n = \frac{R_1 + R_2 + ... + R_{n-1}}{n - 1}$

An obvious implemtattion would be to maintain a record of all the rewards and then perform this computation when needed. This can be memory intensive and is unnecessary. We can show that that computation can be computed incrementally as:
$Q_{n+1} = Q_n + \frac{1}{n}[R_n - Q_n]$

#### Proof
\begin{align}
Q_{n+1} & = \frac{1}{n}\sum_{i = 1}^n R_i \\
& = \frac{1}{n}\left(R_n + \sum_{i=1}^{n-1} R_i\right) \\
& = \frac{1}{n}\left(R_n + (n-1)\frac{1}{n-1} \sum_{i = 1}^{n-1} R_i\right) \\
& = \frac{1}{n}\left(R_n + (n-1)Q_n\right) \\
& = \frac{1}{n}\left(R_n + nQ_n - Q_n\right) \\
& = Q_n + \frac{1}{n} \left(R_n - Q_n\right)
\end{align}

```python
# We get rid of the sample_average() method and modify k_armed_bandit as follows:
# Lines marked ** indicate changes to the code
def k_armed_bandit(k, epsilon, iterations):
    """
    Performs a single run of the k-armed bandit experiment
    :param k: the number of arms
    :param epsilon: Value of epsilon for epoch-greedy action selection
    """
    # Randomly assign true values of reward for each action with mean 0 and variance 1
    true_values = np.random.normal(size=k)

    # Estimates of action values **
    Q = np.zeros(k)

    # N[i] is the no. of times action i has been taken **
    N = np.zeros(k)

    # Store the rewards received for this experiment
    rewards = []

    # Track how often the optimal action was selected
    optimal = []
    optimal_action = true_values.argmax()

    for _ in range(iterations):
        prob = np.random.rand(1)

        if prob > epsilon:
            # Greedy (exploit current knowledge) **
            a = np.random.choice(np.flatnonzero(Q == Q.max()))
        else:
            # Explore (take random action)
            a = np.random.randint(0, k)

        reward = get_reward(true_values, a)

        # Update statistics for executed action **
        N[a] += reward
        Q[a] += (1.0 / N[a]) * (reward - Q[a])

        rewards.append(reward)
        optimal.append(1 if a == optimal_action else 0)

    return rewards, optimal
```

## References
1. Richard S. Sutton, Andrew G. Barto (1998). Reinforcement Learning: An Introduction. MIT Press.


## Problem
You are faced repeatedly with a choice among n different options, or actions. After each choice you receive a
numerical reward chosen from a stationary probability distribution that depends on the action you selected. Your objective is to maximize the expected total reward over some time period, for example, over 1000 action selections,
or time steps.

## Exploring and exploiting
Let expected or mean of reward of each action be called its *value*. At any time step if we choose best possible action as highest of our observed values of all actions. We call such action *greedy* and this process is called *exploiting*. If we choose one of the nongreedy action, this process will be called *exploration* because this may improve observed values of nongreedy actions which may be optimal.

## Action value method
we denote the true (actual) value of action a as $q∗(a)$, and the estimated value on the tth time step as $Q_t(a)$. If by the $t^{th}$ time step action $a$ has been chosen $K_a$ times prior to $t$, yielding rewards $R_1, R_2,...R_{K_a}$, then its value is estimated to be:
$$Q_t(a) = \frac{R_1 + R_2 + .... + R_{K_a}}{K_a}$$

We choose explore with probability $\epsilon$ and choose greedy action with probability $(1-\epsilon)$ i.e Action with max $Q_t(a)$. This way of near greedy selection is called $\epsilon-$greedy method

## Softmax action selection

In Softmax action selection we choose *Exploration* action based on ranking given by *softmax action selection rules*. One method of ranking is *Gibbs, or Boltzmann, distribution*. It
chooses action $a$ on the $t^{th}$ time step with probability, $$\frac{e^{Q_t(a)/\tau}}{\sum_{i=1}^{n}{e^{Q_t(i)/\tau}}}$$

#### Optimized way to update mean
Let mean at time t is $m_t$. Let new observation at time $t+1$ is $x_{t+1}$.
$$
\begin{align}
m_t &= \frac{x_1 + x_2 + ....... + x_t}{t} \tag{1} \\
m_{t+1} &= \frac{x_1 + x_2 + ....... + x_t + x_{t+1}}{t+1} \\
&= \frac{m_t*t + x_{t+1}}{t+1} && \text{by (1)} \\
&= \frac{m_t*t + m_t -m_t + x_{t+1}}{t+1}  \\
&= \frac{m_t(t + 1) + (x_{t+1}-m_t)}{t+1}  \\
m_{t+1} &= m_t + \frac{x_{t+1}-m_t}{t+1}
\end{align}
$$

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

```python
#np.random.seed(seed=2017)

# Environment code
class Bandit:
    def __init__(self,n):
        self.n = n
        self.q_star_arr = np.random.normal(size=self.n)

    # get reward R
    def reward(self,a):
        return np.random.normal(loc=self.q_star_arr[a])

    def getOptimal(self):
        return self.q_star_arr.argmax()
```

```python
class ActionValue:
    def __init__(self,n,epsilon,bandit,max_t = 1000):
        self.n = n
        self.bandit = bandit
        self.max_t = max_t
        self.Qta = np.zeros(n)
        self.Ka = np.zeros(n,dtype=int)
        self.optimalHistory = np.zeros(max_t)
        self.epsilon = epsilon
        self.rewardHistory = np.zeros(max_t)
        self.algoRun()

    def algoRun(self):
        for t in range(self.max_t):
            greedyAction = self.Qta.argmax()

            if np.random.rand() < self.epsilon:#exploring
                curAction = np.random.randint(self.n -1)
                #not to use greedyAction so
                if curAction>=greedyAction:
                    curAction += 1
            else:#exploiting
                curAction = greedyAction

            self.optimalHistory[t] = 1 if curAction == self.bandit.getOptimal() else 0
            curActionReward = self.bandit.reward(curAction)
            self.rewardHistory[t] = curActionReward
            self.Ka[curAction] += 1
            #update Qt(a)
            self.Qta[curAction] += (curActionReward - self.Qta[curAction])/self.Ka[curAction]

# n =10
# bandit = Bandit(10)
# print(bandit.q_star_arr)
# max_t = 1000
# a1 = ActionValue(n,0.1,bandit,max_t=max_t)
# a1.algoRun()
# print(a1.Ka)
# a2 = ActionValue(n,0.01,bandit,max_t=max_t)
# a2.algoRun()
# print(a2.Ka)
# a3 = ActionValue(n,0.0,bandit,max_t=max_t)
# a3.algoRun()
# print(a3.Ka)
```

```python
class SoftmaxAction:
    def __init__(self,n,tau,bandit,max_t = 1000):
        self.n = n
        self.tau = tau
        self.bandit = bandit
        self.max_t = max_t
        self.Qta = np.zeros(n)
        self.Ka = np.zeros(n,dtype=int)
        self.optimalHistory = np.zeros(max_t)
        self.rewardHistory = np.zeros(max_t)
        self.algoRun()

    def algoRun(self):
        for t in range(self.max_t):
            probArray = np.exp(self.Qta/ self.tau)
            probArray = probArray / sum(probArray)
            actionList = list(range(len(self.Qta)))
            curAction = np.random.choice(actionList,p=probArray)
            self.optimalHistory[t] = 1 if curAction == self.bandit.getOptimal() else 0
            curActionReward = self.bandit.reward(curAction)
            self.rewardHistory[t] = curActionReward
            self.Ka[curAction] += 1
            #update Qt(a)
            self.Qta[curAction] += (curActionReward - self.Qta[curAction])/self.Ka[curAction]

```

```python
class SoftmaxEpsilonGreedyAction:
    def __init__(self,n,epsilon,tau,bandit,max_t = 1000):
        self.n = n
        self.tau = tau
        self.bandit = bandit
        self.max_t = max_t
        self.Qta = np.zeros(n)
        self.Ka = np.zeros(n,dtype=int)
        self.optimalHistory = np.zeros(max_t)
        self.epsilon = epsilon
        self.rewardHistory = np.zeros(max_t)
        self.algoRun()

    def algoRun(self):
        for t in range(self.max_t):
            greedyAction = self.Qta.argmax()

            if np.random.rand() < self.epsilon:#exploring
                probArray = np.exp(self.Qta/ self.tau)
                actionList = list(range(len(self.Qta)))
                #not to use greedyAction so
                actionList.remove(greedyAction)
                probArray = np.delete(probArray, greedyAction)

                probArray = probArray / sum(probArray)
                curAction = np.random.choice(actionList,p=probArray)
            else:#exploiting
                curAction = greedyAction

            self.optimalHistory[t] = 1 if curAction == self.bandit.getOptimal() else 0
            curActionReward = self.bandit.reward(curAction)
            self.rewardHistory[t] = curActionReward
            self.Ka[curAction] += 1
            #update Qt(a)
            self.Qta[curAction] += (curActionReward - self.Qta[curAction])/self.Ka[curAction]

```

```python
class TestBed:
    def __init__(self, n, epsilon,tau,algoType,max_t = 1000):
        self.n = n
        self.maxExp = 2000
        self.max_t = max_t
        self.epsilon = epsilon
        self.tau = tau
        self.averageRewardHistory = np.zeros(self.max_t)
        self.averageOptimalHistory = np.zeros(self.max_t)

        for i in range(self.maxExp):
            bandit = Bandit(n)
            if algoType=="ActionValue":
                exp = ActionValue(self.n,self.epsilon,bandit,max_t=self.max_t)
            elif algoType=="SoftmaxAction":
                exp = SoftmaxAction(self.n,self.tau,bandit,max_t=self.max_t)
            elif algoType=="SoftmaxEpsilonGreedyAction":
                exp = SoftmaxEpsilonGreedyAction(self.n,self.epsilon,self.tau,bandit,max_t=self.max_t)

            self.averageRewardHistory += exp.rewardHistory
            self.averageOptimalHistory += exp.optimalHistory

        self.averageRewardHistory /= self.maxExp
        self.averageOptimalHistory *= 100.0/self.maxExp
```

```python
n =10
max_t =1000
#a1 = TestBed(n,0.1,None,"ActionValue",max_t)
a2 = TestBed(n,0.01,None,"ActionValue",max_t)
a3 = TestBed(n,0.0,None,"ActionValue",max_t)
a4 = TestBed(n,None,1.0,"SoftmaxAction",max_t)
a5 = TestBed(n,None,10.0,"SoftmaxAction",max_t)
#a6 = TestBed(n,0.01,1.0,"SoftmaxEpsilonGreedyAction",max_t)
#a7 = TestBed(n,0.0,10.0,"SoftmaxEpsilonGreedyAction",max_t)
#a8 = TestBed(n,0.0,10.0,"SoftmaxEpsilonGreedyAction",max_t)

```

```python
t = range(max_t)
#plt.plot(t,a1.averageRewardHistory,label='epsilon=0.1')
plt.plot(t,a2.averageRewardHistory,label='ActionValue: epsilon=0.01')
plt.plot(t,a3.averageRewardHistory,label='ActionValue: epsilon=0.0')
plt.plot(t,a4.averageRewardHistory,label='SA: tau=1.0')
plt.plot(t,a5.averageRewardHistory,label='SA: tau=10.0')
# plt.plot(t,a4.averageRewardHistory,label='SEGA: epsilon=0.01 tau=1.0')
# plt.plot(t,a5.averageRewardHistory,label='SEGA: epsilon=0.0 tau=10.0')
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
plt.show()
```

```python
plt.plot(t,a2.averageOptimalHistory,label='ActionValue: epsilon=0.01')
plt.plot(t,a3.averageOptimalHistory,label='ActionValue: epsilon=0.0')
plt.plot(t,a4.averageOptimalHistory,label='SA: tau=1.0')
plt.plot(t,a5.averageOptimalHistory,label='SA: tau=10.0')
# plt.plot(t,a4.averageOptimalHistory,label='SEGA: epsilon=0.01 tau=1.0')
# plt.plot(t,a5.averageOptimalHistory,label='SEGA: epsilon=0.0 tau=10.0')
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
plt.show()
```


# Introduction to Linear Algebra

## Matrices

### Conventions of Representation

In the last chapter, we introduced a convention of representing vectors as an ordered, vertical arrangement of numbers within square brackets.

$$\vec{a}=\begin{bmatrix} 3\\4\end{bmatrix}; \quad
\vec{b}=\begin{bmatrix} 2\\-1\end{bmatrix}; \quad
\vec{c}=\begin{bmatrix} -3\\-3\end{bmatrix}; \quad
\vec{d}=\begin{bmatrix} -2\\3\end{bmatrix}; \quad
\vec{e}=\begin{bmatrix} 2\\3\\5\end{bmatrix}
$$

This is part of a broader convention of orderly arranging numbers in a rectangular, table-like structure called a __matrix__. A variable containing a vector is represented with a small letter and an arrow above it. A variable containing a matrix is conventionally represented with a capital letter. For example, M is a matrix containing 12 numbers arranged in a particular way.

$$M=\begin{bmatrix} 1 & 3 & -2 & 4\\2 & 3 & -1 & -1\\ -1 & 1 & 3 & 4\end{bmatrix}$$

As the table-like arrangement of numbers suggests, a matrix can be organised into rows and columns. This allows us to assess its size. The matrix $M$ has 3 rows and 4 columns, thus it is described as a $3\times4$ matrix. The convention is to always start with the number of rows.

$$M_{3\times4}=\underset{\;\\\mathbf{3 \, rows \, \times \, 4 \, \text{columns}}}{\begin{bmatrix} 1 & 3 & -2 & 4\\2 & 3 & -1 & -1\\ -1 & 1 & 3 & 4\end{bmatrix}}$$

With this in mind, we can think of vectors as single column matrices.

### Why a Matrix?

There is not a single precise meaning associated with matrices. They are first and foremost a  mathematical structure uniquely arranging numbers. We can use numbers to count but also to measure.  We can use vectors to describe a direction and magnitude of a force but also features of some object we are trying to encode computationally. Consequently, we can use this rectangular arrangement of numbers for many things. We can, for example, concisely represent a system of linear equations, or conveniently represent any linear transformation of space. These specific uses, however, do not exhaust what this structure may ultimately facilitate. It is rather that we have found a way of using an ordered table of numbers to accomplish something more efficiently or to describe something in a way which ends up further illuminating it from a new perspective. When this happens, as it does in linear algebra, we tend to associate a specific meaning to mathematical objects such are matrices and forget that meaning is a matter of the perspective taken.

### Matrices in Linar Algebra

#### Linear Transformation

In linear algebra, matrices extend the idea of manipulating vectors in space, towards manipulating the space itself (including all the possible vectors in it). To explain this, we will use the idea of a __linear transformation__. We can think of a linear transformation as a function (with some properties), which can transform an input defined in terms of vectors into an output defined in terms of vectors.

 In linear algebra, space itself is defined in terms of vectors. We can, for example, make a linear combination of any two linearly independent 2-D vectors to reach any point in 2-D space. Hence a linear transformation can transform a whole space into a different one.

What are the properties of a linear transformation? The most obvious one is the linearity. It is an aspect that mathematics defines formally, in terms of operations, without much regard to its spatial aspect. This leads to a multitude of possible geometric interpretations. Let's describe one:

Imagine a two-dimensional plane. The plane itself has no shape (you can think of it as an empty canvas), so to illustrate its principal two directions, we need some kind of a helping device. One such device is the coordinate axes. Another such device is a grid, subdividing the space with two sets of lines, parallel to the coordinate axes. The axes lay in the centre of this grid. A linear transformation of space implies that the coordinate centre remains fixed at $(0,0)$, and the the grid lines of the transformed space remain parallel to each other and equally spaced.

#### Using Basis Vectors to Describe a Linear Transformation

If the linearity is guaranteed, an easier and much more convenient way to describe what a linear transformation accomplishes is to track how it transforms its basis vectors. In the case of the 2-dimensional space, we start with the basis vectors $\boldsymbol{\hat{\imath}}$ and $\boldsymbol{\hat{\jmath}}$. A linear transformation transforms the space such that  $\boldsymbol{\hat{\imath}}$ and $\boldsymbol{\hat{\jmath}}$ (usually) end up at a new location in space in terms of the starting condition. We can call these transformed vectors $\boldsymbol{\hat{\imath}}^{\;new}$ and $\boldsymbol{\hat{\jmath}}^{\; new}$

Numerically expressing this linear transformation is easier than one might think. We simply need to capture the coordinates of the transformed basis vectors $\boldsymbol{\hat{\imath}}^{\;new}$ and $\boldsymbol{\hat{\jmath}}^{\; new}$ in terms of the original space and put them in a matrix! Since we have two vectors, our matrix will have 2 columns. The first vector corresponds to the first column (we count columns from left to right). Because our vectors are two-dimensional, our matrix will have 2 rows. The first spatial dimension starts at the top (we encode dimensions from the top towards the bottom).

To capture the transformation described in the image above we define a matrix $M$ as follows:

$$M=\begin{bmatrix} -1&-2\\3&-1\end{bmatrix}$$

### Some Common Linear Transformations

Some linear transformations are used very often, especially when applied in computer graphics. A good example is using matrices to rotate a space in a certain direction.

#### Rotate 90 Degrees Clockwise

$$M_{rotate 90+}=\begin{bmatrix} 0&-1\\-1&0\end{bmatrix}$$

#### Rotate 90 Degrees Counter-clockwise

$$M_{rotate 90-}=\begin{bmatrix} 0&-1\\1&0\end{bmatrix}$$

#### Shear Transformation

With this transformation, all the vector components in the direction of $\boldsymbol{\hat{\imath}}$ remain unchanged, but the ones in the $\boldsymbol{\hat{\jmath}}$ direction get pushed towards the $\boldsymbol{\hat{\imath}}$.

$$M_{shear}=\begin{bmatrix} 1&1\\0&1\end{bmatrix}$$

#### Identity Matrix

A transformation matrix that does not transform the space at all is a matrix whose columns contain the basis vectors. This idea might sound not that useful, yet such a matrix is extremely important in mathematics. Its name is an __identity matrix__, and it is usually symbolised with the capital $I$. An identity matrix serves a similar purpose that the number one does in arithmetics. Multiplying any number by one also does not change the number.

$$I=\begin{bmatrix} 1&0\\0&1\end{bmatrix}$$

In the case of three dimensions, an identity matrix would be contain 3 vectors of 3 dimensions:

$$I_{3}=\begin{bmatrix} 1&0&0 \\ 0&1&0 \\ 0&0&1 \end{bmatrix}$$

Notice an important feature that in both cases, the diagonal elements are ones, and all the rest are zeros.

### Elementary Matrix Operations

#### How a Linear Transformation Affects an Arbitary Vector

Let us return to the transformation matrix $M$, we have created, described as:
$$M=\begin{bmatrix} 0&-3\\4&-1\end{bmatrix}$$

The matrix $M$ is sufficient to compute where any vector of the original space will land in the transformed space. The process is illustrated in the image below. (Fig. 1) We start in the space defined by the basis vectors $\boldsymbol{\hat{\imath}}$ and $\boldsymbol{\hat{\jmath}}$. The vector $\vec{p}$ is an arbitrary vector in this space whose coordinates are $-0.5$ and $1.5$. We can describe $\vec{p}$ as a linear combination of the basis vectors: $\vec{p}=-0.5\boldsymbol{\hat{\imath}}-1.5\boldsymbol{\hat{\jmath}}$.

Now we transform the space. To define the transformation we change the direction and orientation of the basis vectors. We end up with new vectors $\boldsymbol{\hat{\imath}}^{\;new}$ and $\boldsymbol{\hat{\jmath}}^{\; new}$. To encode this transformation in a matrix $M$, we capture vector's new coordinates: $\boldsymbol{\hat{\imath}}^{\;new}$ has coordinates $-1$ and $3$; $\boldsymbol{\hat{\jmath}}^{\;new}$ has coordinates $-2$ and $-1$. (Fig. 2)

To compute where the vector $\vec{p}$ lands in the transformed space, we multiply $\vec{p}$'s first coordinate $-0.5$ with the first column of the transformation matrix $M$, and the $\vec{p}$'s second coordinate $-1.5$ with the second column. This gives us the vector $\vec{p}^{\;new}$ whose coordinates in terms of the old space are $3.5$ and $0$ (Fig. 3) Once we remove the old space and its basis vectors from the picture, we are left with new basis vectors $\boldsymbol{\hat{\imath}}^{\;new}$, $\boldsymbol{\hat{\jmath}}^{\;new}$ and the vector $\vec{p}^{\;new}$. (Fig. 4) If we represent the vector $\vec{p}^{\;new}$ as a linear combination of $\boldsymbol{\hat{\imath}}^{\;new}$ and $\boldsymbol{\hat{\jmath}}^{\;new}$ we notice something very interesting. The linear combination is parametrised by the same scalars $-0.5$ and $-1.5$ just like in the space we started! (Fig. 1) Vectors $\vec{p}$'s coordinates are still $-0.5$ and $-1.5$, only in terms of the new basis vectors (In terms of the old basis vectors, its coordinates are $3.5$ and $0$). The consequence of linearity of the transformation is that as we transform the space, the basis vectors of the space change, but the scalars parametrising every vector in this space remain constant.

#### Multiplying a Vector by a Matrix

The calculation we performed to find the landing coordinates of $\vec{p}^{\;new}$ was, in fact, the same as __multiplying__ a vectory $\vec{p}$ by a matrix $M$. To correctly perform the calculation, the transformation matrix needs to be on the left of the vector that it multiplies. What we did can be described algebraically as following:

\begin{align}
\vec{p}^{\;new}&=M \times \vec{p} \\\\
\vec{p}^{\;new}&=\begin{bmatrix} -1&-2\\3&-1 \end{bmatrix}\times \begin{bmatrix} -0.5\\-1.5\end{bmatrix} \\\\
\vec{p}^{\;new}&=-0.5 \begin{bmatrix} -1\\3 \end{bmatrix} -1.5\begin{bmatrix} -2\\-1 \end{bmatrix} \\\\
\vec{p}^{\;new}&=\begin{bmatrix} 0.5\\-1.5 \end{bmatrix}+\begin{bmatrix} 3\\1.5 \end{bmatrix} \\\\
\vec{p}^{\;new}&=\begin{bmatrix} 3.5\\0 \end{bmatrix}
\end{align}

#### Matrix Multiplication

##### Composing Linear Transformations

A single matrix can be used to describe a transformation of space. A __product__ of two matrices can be used to compose two linear transformations into one! Let's say that we wanted to first rotate a vector by 90 degrees counter-clockwise and then perform a shear operation. We could start with a vector $\vec{q}$ and multiply it by the matrix $M_{rotate90+}$. This operation would yield a vector $\vec{q_{rotated}}$.

$$\vec{q_{rotated}}=M_{rotate90-} \times \vec{q}$$

Now we can multiply the vector $q_{rotated}$ by the matrix $M_{shear}$ and get the final vector $q_{final}$.

$$\vec{q_{final}}=M_{shear} \times \vec{q_{rotated}}$$

Vector multiplication allows us to compute a new matrix $M_{rotate-shear}$ whose effect on the vector $\vec{q}$ would be of applying both the rotation and shear transformation at the same time! The order of operations matter, as we will get a different result if we first applied shear and then rotated than if we first rotated and then applied shear. The operation that comes last should be on the left of the operation that precedes it.

\begin{align}
M_{rotate-shear}&=M_{shear}\times M_{rotate90-} \\\\
M_{rotate-shear}&=\begin{bmatrix} 1&1\\0&1\end{bmatrix} \times \begin{bmatrix} 0&-1\\1&0\end{bmatrix} \\\\
M_{rotate-shear}&=\begin{bmatrix} 1&-1\\1&0\end{bmatrix}
\end{align}

##### Matrix Multiplication Rules

In the previous step, we skipped the process of actually computing the matrix product and directly showed the result. This is because matrix multiplication can be quite numerically cumbersome, and its computation can obscure the idea of what it stands for in linear algebra—composing linear transformations. Here we introduce the multiplication rules. Let's start with an example of a single matrix, $A$:

$$
A=\begin{bmatrix}
a_{11}&a_{12}&a_{13}&a_{14} \\
a_{21}&a_{22}&a_{23}&a_{14} \\
a_{31}&a_{32}&a_{33}&a_{14} \\
\end{bmatrix} \\
$$

$A$ is a $3\times4$ matrix containing 12 elements distributed in 3 rows and 4 columns. Each element $a_{ij}$ is indexed by two numbers. The first one $i$ defines the element's row and the second one $j$ the element's column. Thus the name $a_{23}$ indicates an element in the second row and the third column.

Let's introduce another matrix B:

$$
B=\begin{bmatrix}
b_{11}&b_{12} \\
b_{21}&b_{22} \\
b_{31}&a_{32} \\
b_{41}&a_{42} \\
\end{bmatrix} \\
$$

$B$ is a $4\times2$ matrix, containing 8 elements distributed in 4 rows and 2 columns. The element indexing rules are the same as in the case of $A$.

To compute a matrix product, matrices need to be compatible in size. Moreover, the compatibility then defines the size of the product:

__To compute the matrix product $A\times B$, the number of columns of matrix A needs to match the number of rows of matrix B. If this criterion is met, the product matrix will have the same number of rows as the matrix A and the same number of columns as the matrix B.__

Since matrix $A$ is a $\color{red}3\times \boldsymbol{4}$ matrix and B is a $\boldsymbol{4} \times \color{blue}2$ matrix, two matrices are compatible to compute the product $A \times B$, and the product matrix will be a $\color{red}3 \times \color{blue}2$ matrix.

\begin{align}
C_{\color{red}3\times \color{blue}2} &= A_{\color{red}3\times\boldsymbol{4}} \times B_{\boldsymbol{4}\times\color{blue}2} \\\\
C &= \begin{bmatrix}
c_{11} & c_{12} \\
c_{21} & c_{22} \\
c_{31} & c_{32}
\end{bmatrix}
\end{align}

What is left is to compute are actual elements $c_{ij}$, and for that, we will need the dot product operation. Index $_{\color{red}2 \color{blue}1}$ of tells us that to compute the element $c_{\color{red}2 \color{blue}1}$ we need to dot multiply the __second__ row of the matrix $A$ by the __first__ column of the matrix $B$. To compute the element $c_{\color{red}3 \color{blue}2}$ we need to dot multiply the __third__ row of the matrix $A$ by the __second__ column of the matrix $B$. In other words, to compute $c_{\color{red}2 \color{blue}1}$ we need to multiply:

Notice that any column or a row of a matrix is one-dimensional, so we can think of it as a vector and represent it consequently:

\begin{align}
c_{21} &= \begin{bmatrix} a_{21} \\ a_{22} \\ a_{23} \\ a_{24}\end{bmatrix} \cdot
\begin{bmatrix} b_{11} \\ b_{21} \\ b_{31} \\ b_{41}\end{bmatrix} \\[2ex]
c_{21} &=a_{21}b_{11}+a_{22}b_{21}+a_{23}b_{31}+a_{24}b_{41}
\end{align}

to compute $c_{\color{red}3 \color{blue}2}$ we need to multiply:

And in terms of a dot product of vectors:

\begin{align}
c_{32} &= \begin{bmatrix} a_{31} \\ a_{32} \\ a_{33} \\ a_{34}\end{bmatrix} \cdot
\begin{bmatrix} b_{12} \\ b_{22} \\ b_{32} \\ b_{42}\end{bmatrix} \\[2ex]
c_{32} &=a_{31}b_{12}+a_{32}b_{22}+a_{33}b_{32}+a_{34}b_{42}
\end{align}

The animation below shows how to compute the rest of the elements:

Here it should be clear why the matrix multiplication requires that $A$ and $B$ are compatible in size. If matrix $A$ had 3 rows instead of 4, and the matrix $B$ remained the same, we would need to dot multiply two vectors of a different length. That is simply not possible, as dot product requires us to have to vectors with the same number of elements.

To compute all the elements of matrix C, we need to compute 8 dot products of two 4-dimensional vectors. This is why the matrix multiplication is better left to computers.

$$
C=
\begin{bmatrix} a_{11}b_{11}+a_{12}b_{21}+a_{13}b_{31}+a_{14}b_{41} &  a_{11}b_{12}+a_{12}b_{22}+a_{13}b_{32}+a_{14}b_{42} \\
a_{21}b_{11}+a_{22}b_{21}+a_{23}b_{31}+a_{24}b_{41} &  a_{21}b_{12}+a_{22}b_{22}+a_{23}b_{32}+a_{24}b_{42} \\
a_{31}b_{11}+a_{32}b_{21}+a_{33}b_{31}+a_{34}b_{41} &  a_{31}b_{12}+a_{32}b_{22}+a_{33}b_{32}+a_{34}b_{42} \\
a_{41}b_{11}+a_{42}b_{21}+a_{43}b_{31}+a_{44}b_{41} &  a_{41}b_{12}+a_{4}b_{22}+a_{43}b_{32}+a_{44}b_{42} \\
\end{bmatrix}
$$

#### Matrix Addition and Subtraction

Matrix addition and subtraction do not have a straightforward intuitive meaning. They can be performed, but we need to think of them purely in algebraic terms. Similarly to vectors, to be able to add or subtract two matrices, they need to be of the same size. If the matrix $A$ is a $3\times4$ matrix, then the matrix $B$ needs to be a $3\times4$ matrix as well if we wish to compute $A+B$ and $A-B$. If the matrices are of the same size, both addition and subtraction  are trivial. We simply need to add or subtract individual elements of both matrices that have matching indices.

$$A_{2\times3}=\begin{bmatrix}
a_{11} & a_{12} & a_{13}\\
a_{21} & a_{22} & a_{23}\\
\end{bmatrix}; \quad
B_{2\times3} = \begin{bmatrix}
b_{11} & b_{12} & b_{13}\\
b_{21} & b_{22} & b_{23}\\
\end{bmatrix} \\[2em]
A+B = \begin{bmatrix}
a_{11}+b_{11} & a_{12}+b_{12} & a_{13}+b_{13}\\
a_{21}+b_{21} & a_{22}+b_{22} & a_{23}+a_{23}\\
\end{bmatrix} \\[2em]
A-B = \begin{bmatrix}
a_{11}-b_{11} & a_{12}-b_{12} & a_{13}-b_{13}\\
a_{21}-b_{21} & a_{22}-b_{22} & a_{23}-a_{23}\\
\end{bmatrix}
$$

#### Multiplying a Matrix by a Scalar

Another common operation is to multiply a matrix by a scalar. This is also very straightforward. Every element of the new matrix will be a multiple of the old one and the given scalar.

$$A_{2\times3}=\begin{bmatrix}
a_{11} & a_{12} & a_{13}\\
a_{21} & a_{22} & a_{23}\\
\end{bmatrix}; \\[2em]
pA = \begin{bmatrix}
p\,a_{11} & p\,a_{12} & p\,a_{13}\\
p\,a_{21} & p\,a_{22} & p\,a_{23}\\
\end{bmatrix}
$$

### Elementary Matrix Operations in Python

To represent matrices in Python programming language, we will use the linear algebra library called NumPy. To import it we write the following line of code:

```python
import numpy as np
```

Let us define matrices $A$, $B$ and $C$ as follows:

$$
A=\begin{bmatrix}
4 & 2 & -1 & 5 \\
2 & 1 & 3 & -3 \\
-2 & -3 & 1 & 4 \\
\end{bmatrix}; \quad
B=\begin{bmatrix}
-1 & 1 \\
3 & -3 \\
4 & -2 \\
5 & 5 \\
\end{bmatrix}; \quad
C=\begin{bmatrix}
1 & 3  \\
3 & -1 \\
\end{bmatrix}; \quad
D=\begin{bmatrix}
2 & 1  \\
-1 & 2 \\
\end{bmatrix}
$$

Now we can use NumPy's data structure called array to represent matrices:

```python
A = np.array([[4,   2, -1,  5],
              [2,   1,  3, -3],
              [-2, -3,  1,  4]])
```

```python
B = np.array([[-1,  1],
              [ 3, -3],
              [ 4, -2],
              [ 5,  5]])
```

```python
C = np.array([[-1,  3],
              [ 3, -1]])
```

```python
D = np.array([[ 2, 1],
              [-1, 2]])
```

To make sure we encoded them properly, we can use the command print:

```python
print (A)
```

    [[ 4  2 -1  5]
     [ 2  1  3 -3]
     [-2 -3  1  4]]

```python
print (B)
```

    [[-1  1]
     [ 3 -3]
     [ 4 -2]
     [ 5  5]]

```python
print (C)
```

    [[-1  3]
     [ 3 -1]]

```python
print (D)
```

    [[ 2  1]
     [-1  2]]

#### Matrix Addition and Subtraction in Python

We can only add two matrices if they are of the same size. Thus attempting to compute $A+B$ will result in an error:

```python
A+B
```

What we can add are the matrices $C$ and $D$:

```python
print (C+D)
```

    [[1 4]
     [2 1]]

or we can add any matrix to itself

```python
print (A+A)
```

    [[ 8  4 -2 10]
     [ 4  2  6 -6]
     [-4 -6  2  8]]

#### Multiplying a Matrix by a Scalar in Python

Multiplying a matrix by a scalar in python is quite straightforward. To multiply the matrix $A$ by 5, we write:

```python
print (5*A)
```

    [[ 20  10  -5  25]
     [ 10   5  15 -15]
     [-10 -15   5  20]]

```python
print (-5*A)
```

    [[-20 -10   5 -25]
     [-10  -5 -15  15]
     [ 10  15  -5 -20]]

#### Matrix Multiplication in Python

We can only multiply two matrices if their sizes are matching. We can for example compute $A\times B$. To apply matrix multiplication we will use the NumPy command `np.dot`:

```python
print (np.dot(A,B))
```

    [[ 23  25]
     [ -2 -22]
     [ 17  25]]

Since matrix product is not commutative, trying to compute the product $B\times A$ will not work:

```python
print (np.dot(B,A))
```

However, we can find the product $B\times C$ or $B\times D$:

```python
print (np.dot(B,C))
```

    [[  4  -4]
     [-12  12]
     [-10  14]
     [ 10  10]]

```python
print (np.dot(B,D))
```

    [[-3  1]
     [ 9 -3]
     [10  0]
     [ 5 15]]

Since the matrices $C$ and $D$ have the same size, computing both $C\times D$ and $D\times C$ is possible:

```python
print (np.dot(C,D))
```

    [[-5  5]
     [ 7  1]]

```python
print (np.dot(D,C))
```

    [[ 1  5]
     [ 7 -5]]


### Lets talk about the following topics
1. Derivative
2. Derivative and partial derivative
3. Derivative and directional derivative
4. Derivative and gradient
5. Gradient descent algorithm

### Derivative

Definition of derivative:

$$
f'(x_0) = \lim_{\Delta x \to 0}\frac{\Delta y}{\Delta x} = \
\lim_{\Delta x \to 0}\frac{f(x_0 + \Delta x) - f(x_0)}{\Delta x}
$$

$f'(x_0)$ indicates the changing rate/trend of $f(x)$ along the positive x-axis direction, more intuitively to say, to some point of x:
  1. If $f'(x) > 0$, then the value of $f(x)$ will increase along the positive direction of x-axis;
  2. If $f'(x) < 0$, then the value of $f(x)$ will decrease along the positive direction of x-axis;

### Derivative and partial derivative
Definition of partial derivative:

$$
\frac{\partial}{\partial x_i}f(x_0, x_1, \dots, x_n) \
= \lim_{\Delta x \to 0}\frac{\Delta y}{\Delta x} \
= \lim_{\Delta x \to 0}\frac{f(x_0, x_1, \dots, x_i + \Delta x, \dots, x_n) - f(x_0, x_1, \dots, x_i, \dots, x_n)}{\Delta x}
$$

So as you can see, derivative and partial derivative are essentially same, just:
  1. Derivative indicates the changing rate/trend of unary function $f(x)$ along the positive x-axis direction;
  2. Partial derivative $\frac{\partial}{\partial x_i} (i = 0, 1, \dots, n)$ indicates the changing rate/trend of multivariable function $f(x_0, x_1, \dots, x_n)$ along the positive $x_i (i = 0, 1, \dots, n)$ axis direction;

### Derivative and directional derivative
Definition of directional derivative:

$$
\begin{align}
  \frac{\partial}{\partial v}f(x_0, x_1, \dots, x_n) \
  &= \lim_{v \to 0}\frac{\Delta y}{\Delta x} \
  = \lim_{v \to 0}\frac{f(x_0 + \Delta x_0, x_1 + \Delta x_1, \dots, x_i + \Delta x_i, \dots, x_n + \Delta x_n)}{v} \\
  \newline
  v &= \sqrt{(\Delta x_0)^2 + (\Delta x_1)^2 + \dots + (\Delta x_i)^2 + \dots + (\Delta x_n)^2}
\end{align}
$$

In previous derivative and partial derivative definitions, we were all talking about the changing rate/trend of $f(x)$ along the axes, if we want to calculate the changing rate/trend of $f(x)$ along any arbitrary direction(360-degree), then the directional derivative comes into picture.

That is: **directional derivative indicates the changing rate/trend of $f(x)$ along one arbitrary direction.**

### Derivative and gradient
Definition of gradient:

$$
grad\,f(x_0, x_1, \dots, x_n) = \
\Big(
  \frac{\partial f}{\partial x_0}, \
  \dots, \
  \frac{\partial f}{\partial x_i}, \
  \dots, \
  \frac{\partial f}{\partial x_n}
\Big)
$$

The notion of gradient is created only for answering one question: **in which direction of the function's independent variables space it has the maximum changing rate?**

That is: the function's gradient at one point is such a vector:
  1. It has the same direction with the directional derivative which has the maximum value at this point(at one point the directional derivative is 360-degree);
  2. And its norm is the value of above point 1 described directional derivative;

Here pay attentions to the below three points:
  1. Gradient is a vector, so it has both direction and magnitude;
  2. It has the same direction with the directional derivative which has the maximum value at that point;
  3. The value of the gradient is the value of above point 2 described directional derivative;

### Gradient descent algorithm
Since one function has the maximum changing rate along the gradient's direction at one point of the independent variables space, then if we move along the **reversed** direction of the gradient direction, we can reduce the value of the cost function most efficiently.

But how to move along the **reversed** direction of the gradient direction?

Since gradient and partial derivative are all vectors, then according to the rules of vector operating, we can just decrease correspondingly in each axis, that is:

$
\text{repeat until convergence }\Bigg\{
$

$$
x_0 = x_0 - \alpha\frac{\partial f}{\partial x_0} \\
x_1 = x_1 - \alpha\frac{\partial f}{\partial x_1} \\
\vdots \\
x_n = x_n - \alpha\frac{\partial f}{\partial x_n}
$$

$
\Bigg\}
$

### Credit to [[机器学习] ML重要概念：梯度（Gradient）与梯度下降法（Gradient Descent）](https://blog.csdn.net/walilk/article/details/50978864)


# Simple Linear Regression with NumPy

In school, students are taught to draw lines like the following.

$$ y = 2 x + 1$$

They're taught to pick two values for $x$ and calculate the corresponding values for $y$ using the equation.
Then they draw a set of axes, plot the points, and then draw a line extending through the two dots on their axes.

```python
# Import matplotlib.
import matplotlib.pyplot as plt

# Draw some axes.
plt.plot([-1, 10], [0, 0], 'k-')
plt.plot([0, 0], [-1, 10], 'k-')

# Plot the red, blue and green lines.
plt.plot([1, 1], [-1, 3], 'b:')
plt.plot([-1, 1], [3, 3], 'r:')

# Plot the two points (1,3) and (2,5).
plt.plot([1, 2], [3, 5], 'ko')
# Join them with an (extending) green lines.
plt.plot([-1, 10], [-1, 21], 'g-')

# Set some reasonable plot limits.
plt.xlim([-1, 10])
plt.ylim([-1, 10])

# Show the plot.
plt.show()
```

    <Figure size 640x480 with 1 Axes>

Simple linear regression is about the opposite problem - what if you have some points and are looking for the equation?
It's easy when the points are perfectly on a line already, but usually real-world data has some noise.
The data might still look roughly linear, but aren't exactly so.

***

## Example (contrived and simulated)

#### Scenario
Suppose you are trying to weigh your suitcase to avoid an airline's extra charges.
You don't have a weighing scales, but you do have a spring and some gym-style weights of masses 7KG, 14KG and 21KG.
You attach the spring to the wall hook, and mark where the bottom of it hangs.
You then hang the 7KG weight on the end and mark where the bottom of the spring is.
You repeat this with the 14KG weight and the 21KG weight.
Finally, you place your case hanging on the spring, and the spring hangs down halfway between the 7KG mark and the 14KG mark.
Is your case over the 10KG limit set by the airline?

#### Hypothesis
When you look at the marks on the wall, it seems that the 0KG, 7KG, 14KG and 21KG marks are evenly spaced.
You wonder if that means your case weighs 10.5KG.
That is, you wonder if there is a *linear* relationship between the distance the spring's hook is from its resting position, and the mass on the end of it.

#### Experiment
You decide to experiment.
You buy some new weights - a 1KG, a 2KG, a 3Kg, all the way up to 20KG.
You place them each in turn on the spring and measure the distance the spring moves from the resting position.
You tabulate the data and plot them.

#### Analysis
Here we'll import the Python libraries we need for or investigations below.

```python
# Make matplotlib show interactive plots in the notebook.
%matplotlib inline
```

```python
# numpy efficiently deals with numerical multi-dimensional arrays.
import numpy as np

# matplotlib is a plotting library, and pyplot is its easy-to-use module.
import matplotlib.pyplot as plt

# This just sets the default plot size to be bigger.
plt.rcParams['figure.figsize'] = (8, 6)
```

Ignore the next couple of lines where I fake up some data. I'll use the fact that I faked the data to explain some results later. Just pretend that w is an array containing the weight values and d are the corresponding distance measurements.

```python
w = np.arange(0.0, 21.0, 1.0)
d = 5.0 * w + 10.0 + np.random.normal(0.0, 5.0, w.size)
```

```python
# Let's have a look at w.
w
```

    array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12.,
           13., 14., 15., 16., 17., 18., 19., 20.])

```python
# Let's have a look at d.
d
```

    array([  7.82186059,  14.0940333 ,  18.91831809,  29.19790716,
            28.35552966,  35.13268004,  37.92904728,  49.27309391,
            51.95939251,  59.00125941,  54.78074124,  74.59207512,
            82.88496355,  76.9700379 ,  77.64418561,  91.332951  ,
            82.39451551,  88.36237078, 100.88501927, 103.13242505,
           109.47821768])

Let's have a look at the data from our experiment.

```python
# Create the plot.

plt.plot(w, d, 'k.')

# Set some properties for the plot.
plt.xlabel('Weight (KG)')
plt.ylabel('Distance (CM)')

# Show the plot.
plt.show()
```

#### Model
It looks like the data might indeed be linear.
The points don't exactly fit on a straight line, but they are not far off it.
We might put that down to some other factors, such as the air density, or errors, such as in our tape measure.
Then we can go ahead and see what would be the best line to fit the data.

#### Straight lines
All straight lines can be expressed in the form $y = mx + c$.
The number $m$ is the slope of the line.
The slope is how much $y$ increases by when $x$ is increased by 1.0.
The number $c$ is the y-intercept of the line.
It's the value of $y$ when $x$ is 0.

#### Fitting the model
To fit a straight line to the data, we just must pick values for $m$ and $c$.
These are called the parameters of the model, and we want to pick the best values possible for the parameters.
That is, the best parameter values *given* the data observed.
Below we show various lines plotted over the data, with different values for $m$ and $c$.

```python
# Plot w versus d with black dots.
plt.plot(w, d, 'k.', label="Data")

# Overlay some lines on the plot.
x = np.arange(0.0, 21.0, 1.0)
plt.plot(x, 5.0 * x + 10.0, 'r-', label=r"$5x + 10$")
plt.plot(x, 6.0 * x +  5.0, 'g-', label=r"$6x +  5$")
plt.plot(x, 5.0 * x + 15.0, 'b-', label=r"$5x + 15$")

# Add a legend.
plt.legend()

# Add axis labels.
plt.xlabel('Weight (KG)')
plt.ylabel('Distance (CM)')

# Show the plot.
plt.show()
```

#### Calculating the cost
You can see that each of these lines roughly fits the data.
Which one is best, and is there another line that is better than all three?
Is there a "best" line?

It depends how you define the word best.
Luckily, everyone seems to have settled on what the best means.
The best line is the one that minimises the following calculated value.

$$ \sum_i (y_i - mx_i - c)^2 $$

Here $(x_i, y_i)$ is the $i^{th}$ point in the data set and $\sum_i$ means to sum over all points.
The values of $m$ and $c$ are to be determined.
We usually denote the above as $Cost(m, c)$.

Where does the above calculation come from?
It's easy to explain the part in the brackets $(y_i - mx_i - c)$.
The corresponding value to $x_i$ in the dataset is $y_i$.
These are the measured values.
The value $m x_i + c$ is what the model says $y_i$ should have been.
The difference between  the value that was observed ($y_i$) and the value that the model gives ($m x_i + c$), is $y_i - mx_i - c$.

Why square that value?
Well note that the value could be positive or negative, and you sum over all of these values.
If we allow the values to be positive or negative, then the positive could cancel the negatives.
So, the natural thing to do is to take the absolute value $\mid y_i - m x_i - c \mid$.
Well it turns out that absolute values are a pain to deal with, and instead it was decided to just square the quantity instead, as the square of a number is always positive.
There are pros and cons to using the square instead of the absolute value, but the square is used.
This is usually called *least squares* fitting.

```python
# Calculate the cost of the lines above for the data above.
cost = lambda m,c: np.sum([(d[i] - m * w[i] - c)**2 for i in range(w.size)])

print("Cost with m = %5.2f and c = %5.2f: %8.2f" % (5.0, 10.0, cost(5.0, 10.0)))
print("Cost with m = %5.2f and c = %5.2f: %8.2f" % (6.0,  5.0, cost(6.0,  5.0)))
print("Cost with m = %5.2f and c = %5.2f: %8.2f" % (5.0, 15.0, cost(5.0, 15.0)))
```

    Cost with m =  5.00 and c = 10.00:   510.73
    Cost with m =  6.00 and c =  5.00:  1739.48
    Cost with m =  5.00 and c = 15.00:   894.32

#### Minimising the cost
We want to calculate values of $m$ and $c$ that give the lowest value for the cost value above.
For our given data set we can plot the cost value/function.
Recall that the cost is:

$$ Cost(m, c) = \sum_i (y_i - mx_i - c)^2 $$

This is a function of two variables, $m$ and $c$, so a plot of it is three dimensional.
See the **Advanced** section below for the plot.

In the case of fitting a two-dimensional line to a few data points, we can easily calculate exactly the best values of $m$ and $c$.
Some of the details are discussed in the **Advanced** section, as they involve calculus, but the resulting code is straight-forward.
We first calculate the mean (average) values of our $x$ values and that of our $y$ values.
Then we subtract the mean of $x$ from each of the $x$ values, and the mean of $y$ from each of the $y$ values.
Then we take the *dot product* of the new $x$ values and the new $y$ values and divide it by the dot product of the new $x$ values with themselves.
That gives us $m$, and we use $m$ to calculate $c$.

Remember that in our dataset $x$ is called $w$ (for weight) and $y$ is called $d$ (for distance).
We calculate $m$ and $c$ below.

```python
# Calculate the best values for m and c.

# First calculate the means (a.k.a. averages) of w and d.
w_avg = np.mean(w)
d_avg = np.mean(d)

# Subtract means from w and d.
w_zero = w - w_avg
d_zero = d - d_avg

# The best m is found by the following calculation.
m = np.sum(w_zero * d_zero) / np.sum(w_zero * w_zero)
# Use m from above to calculate the best c.
c = d_avg - m * w_avg

print("m is %8.6f and c is %6.6f." % (m, c))
```

    m is 4.951198 and c is 11.161382.

Note that numpy has a function that will perform this calculation for us, called polyfit.
It can be used to fit lines in many dimensions.

```python
np.polyfit(w, d, 1)
```

    array([ 4.95119814, 11.16138172])

#### Best fit line
So, the best values for $m$ and $c$ given our data and using least squares fitting are about $4.95$ for $m$ and about $11.13$ for $c$.
We plot this line on top of the data below.

```python
# Plot the best fit line.
plt.plot(w, d, 'k.', label='Original data')
plt.plot(w, m * w + c, 'b-', label='Best fit line')

# Add axis labels and a legend.
plt.xlabel('Weight (KG)')
plt.ylabel('Distance (CM)')
plt.legend()

# Show the plot.
plt.show()
```

Note that the $Cost$ of the best $m$ and best $c$ is not zero in this case.

```python
print("Cost with m = %5.2f and c = %5.2f: %8.2f" % (m, c, cost(m, c)))
```

    Cost with m =  4.95 and c = 11.16:   499.37

### Summary
In this notebook we:
1. Investigated the data.
2. Picked a model.
3. Picked a cost function.
4. Estimated the model parameter values that minimised our cost function.

### Advanced
In the following sections we cover some of the more advanced concepts involved in fitting the line.

#### Simulating data
Earlier in the notebook we glossed over something important: we didn't actually do the weighing and measuring - we faked the data.
A better term for this is *simulation*, which is an important tool in research, especially when testing methods such as simple linear regression.

We ran the following two commands to do this:

```python
w = np.arange(0.0, 21.0, 1.0)
d = 5.0 * w + 10.0 + np.random.normal(0.0, 5.0, w.size)
```

The first command creates a numpy array containing all values between 1.0 and 21.0 (including 1.0 but not including 21.0) in steps of 1.0.

```python
 np.arange(0.0, 21.0, 1.0)
```

    array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12.,
           13., 14., 15., 16., 17., 18., 19., 20.])

The second command is more complex.
First it takes the values in the `w` array, multiplies each by 5.0 and then adds 10.0.

```python
5.0 * w + 10.0
```

    array([ 10.,  15.,  20.,  25.,  30.,  35.,  40.,  45.,  50.,  55.,  60.,
            65.,  70.,  75.,  80.,  85.,  90.,  95., 100., 105., 110.])

It then adds an array of the same length containing random values.
The values are taken from what is called the normal distribution with mean 0.0 and standard deviation 5.0.

```python
np.random.normal(0.0, 5.0, w.size)
```

    array([  5.21128808,   2.13198653,  -0.74232395,   7.45955589,
             0.98834441,  11.1086927 ,   9.80473266,  -1.24492606,
             0.90826174,   1.09293531,  -3.85817768,   6.31678901,
            -3.89289611,  -0.8390512 ,   5.41083856,  14.17220137,
             1.12206625,   0.84775242,   1.1699498 ,  -5.79200309,
           -11.09390936])

The normal distribution follows a bell shaped curve.
The curve is centred on the mean (0.0 in this case) and its general width is determined by the standard deviation (5.0 in this case).

```python
# Plot the normal distrution.
normpdf = lambda mu, s, x: (1.0 / (2.0 * np.pi * s**2)) * np.exp(-((x - mu)**2)/(2 * s**2))

x = np.linspace(-20.0, 20.0, 100)
y = normpdf(0.0, 5.0, x)
plt.plot(x, y)

plt.show()
```

The idea here is to add a little bit of randomness to the measurements of the distance.
The random values are entered around 0.0, with a greater than 99% chance they're within the range -15.0 to 15.0.
The normal distribution is used because of the [Central Limit Theorem](https://en.wikipedia.org/wiki/Central_limit_theorem) which basically states that when a bunch of random effects happen together the outcome looks roughly like the normal distribution. (Don't quote me on that!)

#### Plotting the cost function
We can plot the cost function for a given set of data points.
Recall that the cost function involves two variables: $m$ and $c$, and that it looks like this:

$$ Cost(m,c) = \sum_i (y_i - mx_i - c)^2 $$

To plot a function of two variables we need a 3D plot.
It can be difficult to get the viewing angle right in 3D plots, but below you can just about make out that there is a low point on the graph around the $(m, c) = (\approx 5.0, \approx 10.0)$ point.

```python
# This code is a little bit involved - don't worry about it.
# Just look at the plot below.

from mpl_toolkits.mplot3d import Axes3D

# Ask pyplot a 3D set of axes.
ax = plt.figure().gca(projection='3d')

# Make data.
mvals = np.linspace(4.5, 5.5, 100)
cvals = np.linspace(0.0, 20.0, 100)

# Fill the grid.
mvals, cvals = np.meshgrid(mvals, cvals)

# Flatten the meshes for convenience.
mflat = np.ravel(mvals)
cflat = np.ravel(cvals)

# Calculate the cost of each point on the grid.
C = [np.sum([(d[i] - m * w[i] - c)**2 for i in range(w.size)]) for m, c in zip(mflat, cflat)]
C = np.array(C).reshape(mvals.shape)

# Plot the surface.
surf = ax.plot_surface(mvals, cvals, C)

# Set the axis labels.
ax.set_xlabel('$m$', fontsize=16)
ax.set_ylabel('$c$', fontsize=16)
ax.set_zlabel('$Cost$', fontsize=16)

# Show the plot.
plt.show()
```

#### Coefficient of determination
Earlier we used a cost function to determine the best line to fit the data.
Usually the data do not perfectly fit on the best fit line, and so the cost is greater than 0.
A quantity closely related to the cost is the *coefficient of determination*, also known as the *R-squared* value.
The purpose of the R-squared value is to measure how much of the variance in $y$ is determined by $x$.

For instance, in our example the main thing that affects the distance the spring is hanging down is the weight on the end.
It's not the only thing that affects it though.
The room temperature and density of the air at the time of measurment probably affect it a little.
The age of the spring, and how many times it has been stretched previously probably also have a small affect.
There are probably lots of unknown factors affecting the measurment.

The R-squared value estimates how much of the changes in the $y$ value is due to the changes in the $x$ value compared to all of the other factors affecting the $y$ value.
It is calculated as follows:

$$ R^2 = 1 - \frac{\sum_i (y_i - m x_i - c)^2}{\sum_i (y_i - \bar{y})^2} $$

Note that sometimes the [*Pearson correlation coefficient*](https://en.wikipedia.org/wiki/Pearson_correlation_coefficient) is used instead of the R-squared value.
You can just square the Pearson coefficient to get the R-squred value.

```python
# Calculate the R-squared value for our data set.
rsq = 1.0 - (np.sum((d - m * w - c)**2)/np.sum((d - d_avg)**2))

print("The R-squared value is %6.4f" % rsq)
```

    The R-squared value is 0.9742

```python
# The same value using numpy.
np.corrcoef(w, d)[0][1]**2
```

    0.9742264265116457

#### The minimisation calculations
Earlier we used the following calculation to calculate $m$ and $c$ for the line of best fit.
The code was:

```python
w_zero = w - np.mean(w)
d_zero = d - np.mean(d)

m = np.sum(w_zero * d_zero) / np.sum(w_zero * w_zero)
c = np.mean(d) - m * np.mean(w)
```

In mathematical notation we write this as:

$$ m = \frac{\sum_i (x_i - \bar{x}) (y_i - \bar{y})}{\sum_i (x_i - \bar{x})^2} \qquad \textrm{and} \qquad c = \bar{y} - m \bar{x} $$

where $\bar{x}$ is the mean of $x$ and $\bar{y}$ that of $y$.

Where did these equations come from?
They were derived using calculus.
We'll give a brief overview of it here, but feel free to gloss over this section if it's not for you.
If you can understand the first part, where we calculate the partial derivatives, then great!

The calculations look complex, but if you know basic differentiation, including the chain rule, you can easily derive them.
First, we differentiate the cost function with respect to $m$ while treating $c$ as a constant, called a partial derivative.
We write this as $\frac{\partial m}{ \partial Cost}$, using $\delta$ as opposed to $d$ to signify that we are treating the other variable as a constant.
We then do the same with respect to $c$ while treating $m$ as a constant.
We set both equal to zero, and then solve them as two simultaneous equations in two variables.

###### Calculate the partial derivatives
$$
\begin{align}
Cost(m, c)                       &= \sum_i (y_i - mx_i - c)^2      \\[1cm]
\frac{\partial Cost}{\partial m} &= \sum 2(y_i - m x_i -c)(-x_i) \\
                                 &= -2 \sum x_i (y_i - m x_i -c) \\[0.5cm]
\frac{\partial Cost}{\partial c} & = \sum 2(y_i - m x_i -c)(-1) \\
                                 & = -2 \sum (y_i - m x_i -c)   \\
\end{align}
$$

###### Set to zero
$$
\begin{align}
& \frac{\partial Cost}{\partial m}                         = 0 \\[0.2cm]
& \Rightarrow -2 \sum x_i (y_i - m x_i -c)                 = 0 \\
& \Rightarrow \sum (x_i y_i - m x_i x_i - x_i c)           = 0 \\
& \Rightarrow \sum x_i y_i - \sum_i m x_i x_i - \sum x_i c = 0 \\
& \Rightarrow m \sum x_i x_i                               = \sum x_i y_i - c \sum x_i \\[0.2cm]
& \Rightarrow m                                            = \frac{\sum x_i y_i - c \sum x_i}{\sum x_i x_i} \\[0.5cm]
& \frac{\partial Cost}{\partial c}               = 0 \\[0.2cm]
& \Rightarrow -2 \sum (y_i - m x_i - c)          = 0 \\
& \Rightarrow \sum y_i - \sum_i m x_i - \sum c   = 0 \\
& \Rightarrow \sum y_i - m \sum_i x_i            = c \sum 1 \\
& \Rightarrow c = \frac{\sum y_i - m \sum x_i}{\sum 1} \\
& \Rightarrow c = \frac{\sum y_i}{\sum 1} - m \frac{\sum x_i}{\sum 1} \\[0.2cm]
& \Rightarrow c = \bar{y} - m \bar{x} \\
\end{align}
$$

###### Solve the simultaneous equations
Here we let $n$ be the length of $x$, which is also the length of $y$.

$$
\begin{align}
& m = \frac{\sum_i x_i y_i - c \sum_i x_i}{\sum_i x_i x_i} \\[0.2cm]
& \Rightarrow m = \frac{\sum x_i y_i - (\bar{y} - m \bar{x}) \sum x_i}{\sum x_i x_i} \\
& \Rightarrow m \sum x_i x_i = \sum x_i y_i - \bar{y} \sum x_i + m \bar{x} \sum x_i \\
& \Rightarrow m \sum x_i x_i - m \bar{x} \sum x_i = \sum x_i y_i - \bar{y} \sum x_i \\[0.3cm]
& \Rightarrow m = \frac{\sum x_i y_i - \bar{y} \sum x_i}{\sum x_i x_i - \bar{x} \sum x_i} \\[0.2cm]
& \Rightarrow m = \frac{\sum (x_i y_i) - n \bar{y} \bar{x}}{\sum (x_i x_i) - n \bar{x} \bar{x}} \\
& \Rightarrow m = \frac{\sum (x_i y_i) - n \bar{y} \bar{x} - n \bar{y} \bar{x} + n \bar{y} \bar{x}}{\sum (x_i x_i) - n \bar{x} \bar{x} - n \bar{x} \bar{x} + n \bar{x} \bar{x}} \\
& \Rightarrow m = \frac{\sum (x_i y_i) - \sum y_i \bar{x} - \sum \bar{y} x_i + n \bar{y} \bar{x}}{\sum (x_i x_i) - \sum x_i \bar{x} - \sum \bar{x} x_i + n \bar{x} \bar{x}} \\
& \Rightarrow m = \frac{\sum_i (x_i - \bar{x}) (y_i - \bar{y})}{\sum_i (x_i - \bar{x})^2} \\
\end{align}
$$

#### End


# Advanced Expression Manipulation

```
import sys
import os
sys.path.insert(1, os.path.join(os.path.pardir, "ipython_doctester"))
from sympy import *
from ipython_doctester import test
x, y, z = symbols('x y z')
```

For each exercise, fill in the function according to its docstring. Execute the cell to see if you did it right.

## Creating expressions from classes

Create the following objects without using any mathematical operators like `+`, `-`, `*`, `/`, or `**` by explicitly using the classes `Add`, `Mul`, and `Pow`.  You may use `x` instead of `Symbol('x')` and `4` instead of `Integer(4)`.

$$x^2 + 4xyz$$
$$x^{(x^y)}$$
$$x - \frac{y}{z}$$

```
@test
def explicit_classes1():
    """
    Returns the expression x**2 + 4*x*y*z, built using SymPy classes explicitly.

    >>> explicit_classes1()
    x**2 + 4*x*y*z
    """

```

```
@test
def explicit_classes2():
    """
    Returns the expression x**(x**y), built using SymPy classes explicitly.

    >>> explicit_classes2()
    x**(x**y)
    """

```

```
@test
def explicit_classes3():
    """
    Returns the expression x - y/z, built using SymPy classes explicitly.

    >>> explicit_classes3()
    x - y/z
    """

```

## Nested args

```
expr = x**2 - y*(2**(x + 3) + z)
```

Use nested `.args` calls to get the 3 in expr.

```
@test
def nested_args():
    """
    Get the 3 in the above expression.

    >>> nested_args()
    3
    """

```

## Traversal

Write a post-order traversal function that prints each node.

```
@test
def post(expr):
    """
    Post-order traversal

    >>> expr = x**2 - y*(2**(x + 3) + z)
    >>> post(expr)
    -1
    y
    2
    3
    x
    x + 3
    2**(x + 3)
    z
    2**(x + 3) + z
    -y*(2**(x + 3) + z)
    x
    2
    x**2
    x**2 - y*(2**(x + 3) + z)
    """

```

```
for i in postorder_traversal(expr):
    print i
```


## ROW REDUCTION USING PYTHON

```
## GETTING OUR TOOLS READY

#importing python's package called sympy as sym
import sympy as sym
```

```
## Creating a matrix using sympy and matrix is named A
## sym.Matrix() is the syntax

#1.First you write syntax as A=sym.Matrix()
#2.And inside brackets you write 2-D matrix as A=sym.Matrix([[],[],[],[]])
#3.And in every large brackets put values of each row as I have written below.

## Creating the matrix from given question
A=sym.Matrix([[0,-3,-6,4,9],
              [-1,-2,-1,3,1],
              [-2,-3,0,3,-1],
              [1,4,5,-9,-7]])
```

```
## finding the row reduced matrix

## Here the syntax A.rref(pivots=False) contains
#first element A as our matrix and rref is attribute for finding
#row reduced form and yeah don't worry about pivots=False
#I did it to make the answer seem more clear.You could try using pivots=True

A.rref(pivots=False)
```

    Matrix([
    [1, 0, -3, 0,  5],
    [0, 1,  2, 0, -3],
    [0, 0,  0, 1,  0],
    [0, 0,  0, 0,  0]])

```

```


# Funciones y expresiones booleanas

El paquete `sympy` tiene un módulo de [lógica](http://docs.sympy.org/latest/modules/logic.html). Con él podemos hacer algunas simplificaciones

```python
from sympy import *
```

Definimos los símbolos que vamos a utilizar. Supremo e ínfimo se introducen usando el o e y lógicos. Aunque también podemos utilizar `And` y `Or` como funciones. Para la negación usamos `Not` o `~`. Tenemos además `Xor`, `Nand`, `Implies` (que se puede usar de forma prefija con `>>`) y `Equivalent`.

```python
x, y, z = symbols("x,y,z")
```

```python
p = (x | y) & ~ z
```

```python
pprint(p)
```

    ¬z ∧ (x ∨ y)

La formas normales conjuntiva y disyuntivas las podemos calcular como sigue

```python
to_cnf(p)
```

    And(Not(z), Or(x, y))

```python
to_dnf(p)
```

    Or(And(Not(z), x), And(Not(z), y))

También podemos simplificar expresiones

```python
simplify(x | ~x)
```

    True

O dar valores de verdad a las variables

```python
p.xreplace({x:True})
```

    Not(z)

Esto nos permite crear nuestras propias tablas de verdad

```python
p.free_symbols
```

    {x, y, z}

```python
p = Or(x,And(x,y))
```

```python
from IPython.display import HTML,display

```

```python
colores=['LightCoral','Aquamarine']
tabla="<table style='width:25%'><tr><td bgcolor='lightblue'>$"+latex(x)
tabla=tabla+"$ </td><td bgcolor='lightblue'>$"+latex( y)+"$</td><td bgcolor='lightblue'>$"+latex(p)+"$</td></tr>"
for t in cartes({True,False}, repeat=2):
    v =dict(zip((x,y),t))
    tabla=tabla+"<tr> <td bgcolor="+colores[v[x]]+">"+str(v[x])
    tabla=tabla+"</td><td bgcolor="+colores[v[y]]+">"+str(v[y])
    tabla=tabla+"</td><td bgcolor="+colores[v[x]]+">"+str(p.xreplace(v))+"</td></tr>"
tabla=tabla+"</table>"
display(HTML(tabla))
```

$x$ 	$y$	$x \vee \left(x \wedge y\right)$

 False	False	False

 False	True	False

 True	False	True

 True	True	True

Una forma de comprobar que dos expresiones son equivalentes es la siguiente

```python
Equivalent(simplify(p), simplify(x))
```

    True

Veamos ahora cómo podemos encontrar la versión simplificada de una función booleana que venga dada por minterms. Aparentemente `SOPform` hace algunas simplificaciones usando el algoritmo de Quine-McCluskey

```python
p=SOPform([x,y,z],[[0,0,1],[0,1,0],[0,1,1],[1,1,0],[1,0,0],[1,0,1]])
```

```python
p
```

    Or(And(Not(x), y), And(Not(x), z), And(Not(y), z), And(Not(z), x), And(Not(z), y))

Al utilizar `sympy` podemos escribir una forma más amigable de una expresión booleana

```python
pprint(p)
```

    (x ∧ ¬z) ∨ (y ∧ ¬x) ∨ (y ∧ ¬z) ∨ (z ∧ ¬x) ∨ (z ∧ ¬y)

Los comandos `simplify` or `simplify_logic` pueden simplificar aún más

```python
pprint(simplify(p))
```

    (x ∧ ¬y) ∨ (x ∧ ¬z) ∨ (y ∧ ¬x) ∨ (z ∧ ¬y)

```python
pprint(simplify_logic(p))
```

    (x ∧ ¬y) ∨ (x ∧ ¬z) ∨ (y ∧ ¬x) ∨ (z ∧ ¬y)

De hecho, `p` se puede escribir de forma más compacta. Para ello vamos a utilizar el algoritmo espresso, que viene implementado en el paquete `pyeda`

```python
from pyeda.inter import *
```

Este paquete no admite las variables definidas con `symbols`, así que las vamos a declarar con `expvar` para definir variables booleanas

```python
x,y,z = map(exprvar,"xyz")
```

```python
p=SOPform([x,y,z],[[0,0,1],[0,1,0],[0,1,1],[1,1,0],[1,0,0],[1,0,1]])
```

Otro problema es que la salida de `SOPform` no es una expresión de `pyeda`. Lo podemos arreglar pasándola a cadena de caracteres y releyéndola en `pyeda`

```python
p=expr(str(p))
```

Ahora sí que podemos utilizar el simplificador *espresso* implementado en `pyeda`

```python
pm, =espresso_exprs(p)
```

```python
pm
```

    Or(And(x, ~z), And(~x, y), And(~y, z))

Y podemos comprobar que es más corta que la salida que daba `sympy`. Para escribirla de forma más "legible" volvemos a utilizar `pprint` de `sympy`, pero para ello necesitamos pasar nuestra expresión en `pyeda` a `sympy`

```python
pprint(sympify(pm))
```

    (x ∧ ¬z) ∨ (y ∧ ¬x) ∨ (z ∧ ¬y)

Podríamos haber definido directamente `p` utilizando tablas de verdad

```python
p=truthtable([x,y,z], "01111110")
```

```python
pm, = espresso_tts(p)
```

```python
pprint(sympify(pm))
```

    (x ∧ ¬z) ∨ (y ∧ ¬x) ∨ (z ∧ ¬y)

La tabla de verdad de una expresión se obtiene como sigue

```python
expr2truthtable(pm)
```

    z y x
    0 0 0 : 0
    0 0 1 : 1
    0 1 0 : 1
    0 1 1 : 1
    1 0 0 : 1
    1 0 1 : 1
    1 1 0 : 1
    1 1 1 : 0

Veamos un ejemplo análogo pero con más variables, y de paso mostramos como definir vectores de variables

```python
X = ttvars('x', 4)
f = truthtable(X, "0111111111111110")
```

```python
fm, = espresso_tts(f)
```

```python
fm
```

    Or(And(~x[0], x[1]), And(~x[1], x[2]), And(x[0], ~x[3]), And(~x[2], x[3]))

```python
expr2truthtable(fm)
```

    x[3] x[2] x[1] x[0]
       0    0    0    0 : 0
       0    0    0    1 : 1
       0    0    1    0 : 1
       0    0    1    1 : 1
       0    1    0    0 : 1
       0    1    0    1 : 1
       0    1    1    0 : 1
       0    1    1    1 : 1
       1    0    0    0 : 1
       1    0    0    1 : 1
       1    0    1    0 : 1
       1    0    1    1 : 1
       1    1    0    0 : 1
       1    1    0    1 : 1
       1    1    1    0 : 1
       1    1    1    1 : 0

### Un ejemplo de simplificación

Veamos que el o exclusivo con la definición $x\oplus y=(x\wedge \neg y)\vee (\neg x\wedge y)$ es asociativo

```python
x, y, z = map(exprvar,"xyz")
```

```python
f = lambda x,y : Or(And(x,~ y),And(~x,y))
```

```python
f(x,y)
```

    Or(And(x, ~y), And(~x, y))

```python
expr2truthtable(f(x,y))
```

    y x
    0 0 : 0
    0 1 : 1
    1 0 : 1
    1 1 : 0

```python
f(x,y).equivalent(Xor(x,y))
```

    True

Veamos que efectivamente $x\oplus(y\oplus z)=(x\oplus y)\oplus z$

```python
pprint(simplify_logic(f(x,f(y,z))))
```

    (x ∧ y ∧ z) ∨ (x ∧ ¬y ∧ ¬z) ∨ (y ∧ ¬x ∧ ¬z) ∨ (z ∧ ¬x ∧ ¬y)

```python
pprint(simplify_logic(f(f(x,y),z)))
```

    (x ∧ y ∧ z) ∨ (x ∧ ¬y ∧ ¬z) ∨ (y ∧ ¬x ∧ ¬z) ∨ (z ∧ ¬x ∧ ¬y)

```python
a= f(f(x,y),z)
b= f(x,f(y,z))
```

```python
a.equivalent(b)
```

    True

Podemos hacer una función que pase de minterm a expresions en pyeda

```python
def minterm2expr(l,v):
    n = len(l)
    vv=v.copy()
    for i in range(n):
        if not(l[i]):
            vv[i]=Not(vv[i])
    return And(*vv)
```

```python
x,y,z,t = map(exprvar,"xyzt")
```

```python
minterm2expr([0,1,0,1],[x,y,z,t])
```

    And(~x, y, ~z, t)

```python
def minterms2expr(l,v):
    return Or(*[minterm2expr(a,v) for a in l])
```

```python
hh2=minterms2expr([[0,0,0,0],[0,0,1,0],[0,1,0,0],[0,1,1,0],[0,1,1,1],[1,0,0,0],[1,0,1,0],[1,1,0,0]],[x,y,z,t])
```

```python
hh2
```

    Or(And(x, y, ~z, ~t), And(~x, ~y, z, ~t), And(~x, y, ~z, ~t), And(~x, y, z, ~t), And(~x, y, z, t), And(x, ~y, ~z, ~t), And(x, ~y, z, ~t), And(~x, ~y, ~z, ~t))

```python
pprint(sympify(hh2))
```

    (t ∧ y ∧ z ∧ ¬x) ∨ (x ∧ y ∧ ¬t ∧ ¬z) ∨ (x ∧ z ∧ ¬t ∧ ¬y) ∨ (x ∧ ¬t ∧ ¬y ∧ ¬z)
    ∨ (y ∧ z ∧ ¬t ∧ ¬x) ∨ (y ∧ ¬t ∧ ¬x ∧ ¬z) ∨ (z ∧ ¬t ∧ ¬x ∧ ¬y) ∨ (¬t ∧ ¬x ∧ ¬y
    ∧ ¬z)

Y ahora la podemos simplificar

```python
sh2, = espresso_exprs(hh2)
```

```python
sh2
```

    Or(And(~z, ~t), And(~y, ~t), And(~x, y, z))


# Modeling and Simulation in Python

SymPy code for Chapter 16

Copyright 2017 Allen Downey

License: [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0)

### Mixing liquids

We can figure out the final temperature of a mixture by setting the total heat flow to zero and then solving for $T$.

```python
from sympy import *

init_printing()
```

```python
C1, C2, T1, T2, T = symbols('C1 C2 T1 T2 T')

eq = Eq(C1 * (T - T1) + C2 * (T - T2), 0)
eq
```

```python
solve(eq, T)
```

### Analysis

We can use SymPy to solve the cooling differential equation.

```python
T_init, T_env, r, t = symbols('T_init T_env r t')
T = Function('T')

eqn = Eq(diff(T(t), t), -r * (T(t) - T_env))
eqn
```

Here's the general solution:

```python
solution_eq = dsolve(eqn)
solution_eq
```

```python
general = solution_eq.rhs
general
```

We can use the initial condition to solve for $C_1$.  First we evaluate the general solution at $t=0$

```python
at0 = general.subs(t, 0)
at0
```

Now we set $T(0) = T_{init}$ and solve for $C_1$

```python
solutions = solve(Eq(at0, T_init), C1)
value_of_C1 = solutions[0]
value_of_C1
```

Then we plug the result into the general solution to get the particular solution:

```python
particular = general.subs(C1, value_of_C1)
particular
```

We use a similar process to estimate $r$ based on the observation $T(t_{end}) = T_{end}$

```python
t_end, T_end = symbols('t_end T_end')
```

Here's the particular solution evaluated at $t_{end}$

```python
at_end = particular.subs(t, t_end)
at_end
```

Now we set $T(t_{end}) = T_{end}$ and solve for $r$

```python
solutions = solve(Eq(at_end, T_end), r)
value_of_r = solutions[0]
value_of_r
```

We can use `evalf` to plug in numbers for the symbols.  The result is a SymPy float, which we have to convert to a Python float.

```python
subs = dict(t_end=30, T_end=70, T_init=90, T_env=22)
r_coffee2 = value_of_r.evalf(subs=subs)
type(r_coffee2)
```

    sympy.core.numbers.Float

```python
r_coffee2 = float(r_coffee2)
r_coffee2
```

```python

```


### 1. measure linear separability

https://www.python-course.eu/linear_discriminant_analysis.php

Given mxn dimensional input $X_{mn}$, and expecting 1xn dimensional output $y_{1n}$, the Fishers Linear Discriminant Analysis (LDA) searches for the **linear** projection parameterized by $w_{m1}$, noted as $y_{1n} = w^T_{m1} * X_{mn}$, where the **separability** of the classes is maximized. The **separability** of the classes means that, the samples of a class should have the predictions closer to ground truth class, than to other classes.

********
Consider the optimization as **least square regresssion problem** from $X_{mn}$ to output $y_{1n}$, the regression loss is:

$\begin{align}
(1) Loss_w &= \sum_{c\in C} SE_c\\
    & =  \sum_{c\in C} \sum_{j\in N_c} [y({x_j}) - y({u_c})]^2 \\
    & =  \sum_{c\in C} \sum_{j\in N_c} (w^T * x_j - w^T * u_c)(w^T * x_j - w^T * u_c)^T\\
    & =  \sum_{c\in C} \sum_{j\in N_c}  w^T*(x_j - u_c)(x_j - u_c)^T * w\\
    & =  \sum_{c\in C}w^T * [\sum_{j\in N_c} (x_j - u_c)(x_j - u_c)^T] * w\\
    & =  w^T * S_W * w \\
\end{align}$

where $S_W$ is the within class scatter matrix, denoted as:

$\begin{align}
S_W =  \sum_{c \in C}\sum_{j \in N_{c}} (x_j - u_c)(x_j - u_c)^T\\
\end{align}$

Given that we have calculated the the scatter matrix , the computation of covariance matrix is straight forward. We just a have to scale by n-1 the scatter matrix values to compute the covariance matrix, which means:

$\begin{align}
Cov(X) &= \frac{\sum_{i\in N}\sum_{j\in N}(X_i - u)(X_j - u)^T}{N-1}\\
       &= \frac{\sum_{i\in N}\sum_{S_X}}{N-1}\\
S_X &= (N - 1) * Cov(X)\\
\end{align}$

$Loss_w$ represents how much the predictions deviate from the ground truth across all samples, noted as **Within Group Loss**. This is important information, but not enough, as **separatability** should be a notion reflecting the **contrast** between the confidence of an instance belonging to a class, and the confidence of belonging to other classes. $Loss_w$ measures how close are the predictions and ground truth labels, but it does not tell how far the predictions are away from the wrong labels (away from the faults). There should be a loss term measuring the scatter between different classes in the transformed space. Again, using the square error, the scatter between two classes a, b can be expressed as:

$\begin{align}
SE_{a,b \in C} & =  N_{a} * N_{b} * [(y(u_{a}) - y(u_{b})]^2 \\
& =    N_a * N_b * (w^T * u_a - w^T * u_b)(w^T * u_ia - w^T * u_b)^T\\
& =    N_a * N_b * w^T*[(u_a - u_b)(u_a - u_b)^T] * w\\
\end{align}$

When summing up all pairs, the overal becomes:

$\begin{align}
(2) Loss_b &= \sum^{a \neq b} SE_{a,b}\\
       &= w^T*\sum_{}^{a \neq b} N_a * N_b * [(u_a - u_b)(u_a - u_b)^T] * w\\
       &= w^T*\sum_{a,b}^{a \neq b}  N_a *N_b * [(u_a - u_b)(u_a - u + u - u_b)^T] * w \\
       &= w^T*\sum_{a,b}^{a \neq b}  N_a *N_b * [(u_a - u_b)(u_a - u)^T * w  + w^T*\sum_{a,b}^{a \neq b}  N_a *N_b * [(u_a - u_b)(u - u_b)^T] * w \\
       &= w^T*\sum_{a,b}^{a \neq b}  N_a *N_b * [(u_a - u_b)(u_a - u)^T * w  + w^T*\sum_{b,a}^{b \neq a}  N_b *N_a * [(u_b - u_a)(u_b - u)^T] * w \\
       &= 2 * w^T*\sum_{a,b}^{a \neq b}  N_a *N_b * [(u_a - u_b)(u_a - u)^T * w \\
       &= 2 * w^T*\sum_{a}N_a*\sum_{b \neq a} N_b * [(u_a - u_b)(u_a - u)^T * w \\
       &= 2 * w^T*\sum_{a}N_a*[\sum_{b \neq a} N_b * u_a - \sum_{b \neq a} N_b * u_b)]*(u_a - u)^T * w\\
       &= 2 * w^T*\sum_{a}N_a*[(N - N_a) * u_a - (N*u - N_a*u_a)]*(u_a - u)^T * w\\
       &= 2 * w^T*\sum_{a}N_a*[(N * u_a - N*u]*(u_a - u)^T * w\\
       &= 2 * w^T*\sum_{a}N * N_a*[(u_a - u]*(u_a - u)^T * w \\
       &= 2 * N * w^T*\sum_{c}N_c*[(u_c - u]*(u_c - u)^T * w \\
       &= 2 * N * w^T* S_B * w \\
\end{align}$

where $S_B$ is the between class scatter matrix, denoted as:

$\begin{align}
S_B =  \sum_{c \in C} N_c (u_c - u)(u_c - u)^T\\
\end{align}$

Interestingly, $SB$ was initially defined as weighted sum of pairwised outerproduct of the class mean vectors in the transformed space, in the end, it's equilevant to calculate the weighted sum of the outer product of each class mean and the global mean in the transformed space.

Moreover, when summing up $S_W, S_B$, we get $S_T$ which captures the overal scatter of the samples:

$\begin{align}
(3) S_T &=  \sum_{x \in X} (x - u)(x - u)^T  \\
&=  \sum_{c \in C}\sum_{ j \in N_c} [(x_j - u_c) + (u_c - u)][(x_j - u_c) + (u_c - u)]^T \\
&=  \sum_{c \in C}\sum_{ j \in N_c} (x_j - u_c)(x_j - u_c)^T + \sum_{c \in C}\sum_{ j \in N_c} (u_c - u)(u_c - u)^T + \sum_{c \in C}\sum_{ j \in N_c} (x_j - u_c)(u_c - u)^T + \sum_{c \in C}\sum_{ j \in N_c} (u_c - u)(x_j - u_c)^T \\
&=  \sum_{c \in C}\sum_{ j \in N_c} (x_j - u_c)(x_j - u_c)^T +  \sum_{c \in C} N_c(u_c - u)(u_c - u)^T +\sum_{c \in C}(\sum_{ j \in N_c} x_j - N_c * u_c)(u_c - u)^T + \sum_{c \in C}(u_c - u) (\sum_{ j \in N_c}x_j - N_c * u_c)^T \\
&=  \sum_{c \in C}\sum_{ j \in N_c} (x_j - u_c)(x_j - u_c)^T +  \sum_{c \in C} N_c(u_c - u)(u_c - u)^T + \sum_{c}(0)(u_x - u)^T + \sum_{c}(u_x - u)(0) \\
&=  \sum_{c \in C}\sum_{ j \in N_c} (x_j - u_c)(x_j - u_c)^T +  \sum_{c \in C} N_c(u_c - u)(u_c - u)^T + 0 + 0 \\
&=  S_W + S_B +0 +0 \\
&=  S_W + S_B
\end{align}$

As scatter matrix captures the variance/covariance, it represents a notion of energy. We can think that $S_T$ captures the overal energy in the distribution, which can be split into two parts: the $S_W$ which captures the 'harmful' energy which enlarges the distances between samples in same class, and $S_B$ captures the 'useful' energy between classes which enlarges the distances between samples of different classes.

### 2. optimize linear separability

To increase the linear separability, we are looking for small $Loss_w$ and large $Loss_b$. So we can form the loss function as:

$\begin{align}
(4) J_w & = \frac{Loss_b}{Loss_w}\\
& = \frac{w^T * S_B * w}{w^T * S_W * w}\\
\end{align}$

Do the derivative and make it zero:

$\begin{align}
(5) J^{'}_w & = \frac{D(J_w)}{D_w} = 0\\
        & => (w^T * S_W * w)* 2 * S_B * w - (w^T * S_B * w) * 2 * S_W * w = 0\\
        & => \frac{(w^T * S_W * w)*  S_B * w}{(w^T * S_W * w)} - \frac{(w^T * S_B * w) *  S_W * w}{(w^T * S_W * w)}= 0\\
        & => S_B * w - \frac{(w^T * S_B * w)}{(w^T * S_W * w)} * S_W * w= 0\\
        & => S_B * w - J_w * S_W * w= 0\\
        & => (S_B  - J_w * S_W) * w= 0\\
        & => S^{-1}_W*(S_B  - J_w * S_W) * w= 0\\
        & => S^{-1}_W*S_B *w  - J_w * w  = 0\\
        & => S^{-1}_W*S_B *w  = \lambda * w\\
\end{align}$

Now we see that the optimal w is an eigen-vector of $S^{-1}_W*S_B$, corresponding to the largest eigen-value. Note that here w represents a normalized vector where $||w||_2 = 1$. When perform multi-class LDA, we would extract the first $N_c-1$ eigen-vectors to form the overal transformation. As these eigen-vectors are orthogonal to each other, they form the axis bases of the transformed space. This combination makes $\sum_{i \in N_c-1}J_{wi}$ largest in the transformation solution space composed by all ${w:||w||_2 = 1}$.

There is another way using Lagrangian form of the problem:

The problem of maximizing $J_w$ is equilevant to maximizing $Loss_b = w^T*S_B*w$ when keeping $Loss_w =  w^T*S_W*w = K$, where K is constant.

Then the lagrangian form is:

$\begin{align}
(6) L & =  w^T * S_B * w - \lambda * (w^T * S_W * w - K)\\
\end{align}$

Then make the derivative to $w$ equals to $0_{m1}$ (vector):

$\begin{align}
(7) \frac {\delta L}{\delta w} & =  2 * S_B * w - \lambda * 2 * S_W * w  = 0_{m1}\\
& =>  S_B * w - \lambda *  S_W * w  = 0_{m1}\\
& =  S_B * w = \lambda *  S_W * w\\
& =  S_W^{-1}*S_B * w = \lambda *w\\
\end{align}$

Again this is eigen-values and eigen-vectors problem.

### 3. Now implement

#### 3.1 generate dataset

```python
#### Load dataset
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import style
style.use('fivethirtyeight')
np.random.seed(seed=42)
```

```python
# Create data
num_samples = 100
gap = 4
A = np.random.randn(num_samples) + gap, np.random.randn(num_samples)
B = np.random.randn(num_samples), np.random.randn(num_samples) + gap
C = np.random.randn(num_samples) + gap, np.random.randn(num_samples) + gap
A = np.array(A)
B = np.array(B)
C = np.array(C)
ABC = np.hstack([A, B, C])
y = np.array([0] * num_samples + [1] * num_samples+ [2] * num_samples)

## calculate the mean
mean_A = A.mean(axis = 1, keepdims = True)
mean_B = B.mean(axis = 1, keepdims = True)
mean_C = C.mean(axis = 1, keepdims = True)
mean_ABC = ABC.mean(axis = 1, keepdims = True)

```

```python
## visualize
fig = plt.figure(figsize=(10,5))
ax0 = fig.add_subplot(111)
ax0.scatter(A[0],A[1],marker='s',c='r',edgecolor='black')
ax0.scatter(B[0],B[1],marker='^',c='g',edgecolor='black')
ax0.scatter(C[0],C[1],marker='o',c='b',edgecolor='black')
ax0.scatter(mean_A[0],mean_A[1],marker='o', s = 100, c='y',edgecolor='black')
ax0.scatter(mean_B[0],mean_B[1],marker='o', s = 100, c='y',edgecolor='black')
ax0.scatter(mean_C[0],mean_C[1],marker='o', s = 100, c='y',edgecolor='black')
ax0.scatter(mean_ABC[0],mean_ABC[1],marker='o', s = 200,c='y',edgecolor='red')
plt.show()

```

```python

## calculate the scatters
scatter_A = np.dot(A-mean_A, np.transpose(A-mean_A))
scatter_B = np.dot(B-mean_B, np.transpose(B-mean_B))
scatter_C = np.dot(C-mean_C, np.transpose(C-mean_C))
scatter_ABC = np.dot(ABC-mean_ABC, np.transpose(ABC-mean_ABC))

## see the equilevant of scatter matrix and covariance matrix
print('@scatter matrix:\n',scatter_A)
print('\n@covariance matrix to scatter matrix:\n', np.cov(A) *99)

```

    @scatter matrix:
     [[ 81.65221947 -11.69726509]
     [-11.69726509  90.03896521]]

    @covariance matrix to scatter matrix:
     [[ 81.65221947 -11.69726509]
     [-11.69726509  90.03896521]]

```python
## compute Sw, Sb
Sw = scatter_A + scatter_B + scatter_C
Sb = scatter_ABC - Sw

## computer eigen-values and eigen-vectors
eigval, eigvec = np.linalg.eig(np.dot(np.linalg.inv(Sw),Sb))

## get first 2 projections
eigen_pairs = zip(eigval, eigvec)
eigen_pairs = sorted(eigen_pairs,key=lambda k: k[0],reverse=True)
w = eigvec[:2]
```

```python
## transform
Projected = ABC.T.dot(w).T

## plot transformed feature and means
fig = plt.figure(figsize=(12, 8))
ax0 = fig.add_subplot(111)

means = []
for l,c,m in zip(np.unique(y),['r','g','b'],['s','x','o']):
    means.append(np.mean(Projected[:,y==l],axis=1))
    ax0.scatter(Projected[0][y==l],
                Projected[1][y==l],
                c=c, marker=m, label=l, edgecolors='black')

## make grid
mesh_x, mesh_y = np.meshgrid(np.linspace(min(Projected[0]),max(Projected[0])),
                             np.linspace(min(Projected[1]),max(Projected[1])))
mesh = []
for i in range(len(mesh_x)):
    for j in range(len(mesh_x[0])):
        mesh.append((mesh_x[i][j],mesh_y[i][j]))

## make decision on grid points
from sklearn.neighbors import KNeighborsClassifier
NN = KNeighborsClassifier(n_neighbors=1)
NN.fit(means,['r','g','b'])
predictions = NN.predict(np.array(mesh))

## plot grid
ax0.scatter(np.array(mesh)[:,0],np.array(mesh)[:,1],color=predictions,alpha=0.4)

## plot means
means = np.array(means)
ax0.scatter(means[:,0],means[:,1],marker='o',c='yellow', edgecolors='red', s=200)

ax0.legend(loc='upper right')
plt.show()
```

```python

```


```python
import matplotlib.pyplot as plt
import math
import numpy as np
%matplotlib inline
from ipywidgets import interact
from sympy import nsolve
```

TOTIENT FUNCTION

```python
def gcd(a, b):
  if b==0:
    return a
  return gcd(b, a % b)
```

```python
plt.gca().spines['top'].set_visible(False)
plt.gca().spines['right'].set_visible(False)
plt.xlabel("N")
plt.ylabel("phi(N)")
plt.scatter([*range(1,2500)], [sum(gcd(n, i) == 1 for i in range(1,n)) for n in range(1, 2500)], s = 1, c='green');
plt.show()
```

GENERAL TO WEISTRASS FORM

```python
@interact(a = (-10,10,0.1), b=(-10,10,0.1), c=(-10,10,0.1), d=(-10,10,0.1), e=(-10,10,0.1))
def ell_curve(a, b, c, d, e):
  mx2, mx1 = np.ogrid[-10:10:0.1,-15:15:0.1]
  def evaluate_general(x,y):
    return np.power(y,2) + a*x*y + b*y - np.power(x, 3) - c * np.power(x,2) - d*x - e
  def transform_coord(x,y):
    return x - ((a**2 + 4*c) / 12), y - (a / 2)*x + ((a**3 + 4*a*c - 12 * b) / 24)
  def evaluate_normal(x,y):
    x, y = transform_coord(x,y)
    return np.power(y,2) - np.power(x,3) - d*x - e

  plt.contour(mx1.ravel(), mx2.ravel(), evaluate_general(mx1, mx2), [0], colors="blue")
  plt.contour(mx1.ravel(), mx2.ravel(), evaluate_normal(mx1, mx2), [0], colors="red")
  plt.show()
```

    interactive(children=(FloatSlider(value=0.0, description='a', max=10.0, min=-10.0), FloatSlider(value=0.0, des…

FINITE CURVE

```python
def display_finite_curve(a, b, N):
  def is_point(x, y, a, b, N):
    return (y**2) % N == (x**3+ a*x + b) % N
  points = [(x,y) for x in range(N) for y in range(N) if is_point(x,y,a,b,N)]
  plt.text(-5,-5,s = "p = {}\n a = {}\n b= {}".format(N,a,b),c = "black",bbox={'facecolor': 'green', 'alpha': 0.5})
  plt.scatter(list(zip(*points))[0], list(zip(*points))[1], s=10)
```

```python
display_finite_curve(1, -1, 39)
```


## Question 1

```python
import matplotlib.pyplot as plt
import math
import statistics as st
import numpy as np
import scipy.linalg as la
from datetime import datetime
import sympy as sym
```

```python
def myawgn(PSD,B,Fs,l):
    sigma = math.sqrt(2*PSD*B)
    #sigma is the standard deviation
    print("Power = ",2*PSD*B)

    N = 1000
    x = sigma*(np.random.randn(N,l))
    awgn_sequence = x[0]

    y = abs(np.fft.fft(x))
    y_conjugate = np.conjugate(y)
    y_squared = np.multiply(y,y_conjugate)
    y_mean = np.mean(y_squared,axis = 1)
    y_mean = y_mean/(Fs*l);
    calculated_psd = np.mean(y_mean);

    print("Calculated PSD = ",calculated_psd)

    #plotting
    plt.figure()
    plt.plot(y_mean, color = 'green', label='Mean using sample functions')
    plt.axhline(y=calculated_psd, color='r', linestyle='-', label = 'Calculated PSD')
    plt.axhline(y=PSD, color='blue', linestyle='dotted', label = 'Given PSD')
    plt.legend()
    plt.show

    return awgn_sequence
```

```python
PSD = float(input('PSD: '))
B = float(input('Bandwidth: '))
Fs = float(input('Sampling frequency: '))
l = int(input('Length of the sequence: '))
awgn = myawgn(PSD,B,Fs,l)
print("AWGN sequence: ",awgn)
```

## Question 2

```python
# library function to check PSD
def posSymDef(mat):
    return np.all(np.linalg.eigvals(mat) > 0)

def mygauss(mu,cov,s):

    eigval,m1 = la.eig(cov);
    m1 = np.array(m1).astype(float);
    m1_inv = np.linalg.inv(m1);
    m2 = np.matmul(m1_inv,cov);
    m2 = np.matmul(m2,m1);

    for i in range(len(m2)):
        m2[i][i] = np.sqrt(m2[i][i]);

    M = np.matmul(m1,m2);
    M = np.matmul(M,m1_inv);
    ans = np.matmul(np.random.randn(s, len(m2)),M);

    for i in range(s):
        ans[i] = ans[i] + mu;
    print('\nSamples:');
    print(ans);

    print('\nCalculated Mean Vector:');
    mu = [];
    ans = np.transpose(ans);
    for i in range(len(m2)):
        mu.append(np.mean(ans[i]));
    print(mu);

    print('\nCalculated Covariance Matrix:');
    print(np.cov(ans));
    print('\nCalculated matrix SPD?: '+str(posSymDef(cov)));
```

```python
cov = np.array([[2, -1, 0],
                [-1, 2, -1],
                [0, -1, 2]]);
mu = np.array([1,3,-5]);

print('Entered Mean Vector:');
print(mu);
print('\nEntered Covariance Matrix:');
print(cov);

mygauss(mu,cov,10000);
```

    Entered Mean Vector:
    [ 1  3 -5]

    Entered Covariance Matrix:
    [[ 2 -1  0]
     [-1  2 -1]
     [ 0 -1  2]]

    Samples:
    [[ 1.07126392  1.57325674 -1.99812429]
     [ 1.8310989   2.95882931 -5.24555917]
     [ 0.24695964  1.16748971 -4.66019658]
     ...
     [ 1.80900553  6.01039805 -6.02386182]
     [ 3.8905174   0.82114425 -6.22760604]
     [ 1.70671262  5.53382482 -9.03190338]]

    Calculated Mean Vector:
    [1.0353198096946992, 2.953741212848046, -4.984730914314471]

    Calculated Covariance Matrix:
    [[ 2.0194349  -1.02033005  0.01733812]
     [-1.02033005  2.0414831  -1.02638963]
     [ 0.01733812 -1.02638963  2.01643282]]

    Calculated matrix SPD?: True

```python

```