# Programming Exercise 4:  Neural Networks Learning

## Introduction

In this exercise, you will implement the backpropagation algorithm for neural networks and apply it to the task of hand-written digit recognition. Before starting on the programming exercise, we strongly recommend watching the video lectures and completing the review questions for the associated topics.

All the information you need for solving this assignment is in this notebook, and all the code you will be implementing will take place within this notebook. The assignment can be promptly submitted to the coursera grader directly from this notebook (code and instructions are included below).

Before we begin with the exercises, we need to import all libraries required for this programming exercise. Throughout the course, we will be using [`numpy`](http://www.numpy.org/) for all arrays and matrix operations, [`matplotlib`](https://matplotlib.org/) for plotting, and [`scipy`](https://docs.scipy.org/doc/scipy/reference/) for scientific and numerical computation functions and tools. You can find instructions on how to install required libraries in the README file in the [github repository](https://github.com/dibgerge/ml-coursera-python-assignments).

```python
# used for manipulating directory paths
import os

# Scientific and vector computation for python
import numpy as np

# Plotting library
from matplotlib import pyplot

# Optimization module in scipy
from scipy import optimize

# will be used to load MATLAB mat datafile format
from scipy.io import loadmat

# library written for this exercise providing additional functions for assignment submission, and others
import utils

# define the submission/grader object for this exercise
grader = utils.Grader()

# tells matplotlib to embed plots within the notebook
%matplotlib inline
```

## Submission and Grading

After completing each part of the assignment, be sure to submit your solutions to the grader. The following is a breakdown of how each part of this exercise is scored.

| Section | Part                                             | Submission function | Points
| :-      |:-                                                | :-                  | :-:
| 1       | [Feedforward and Cost Function](#section1)                    | [`nnCostFunction`](#nnCostFunction)   | 30
| 2       | [Regularized Cost Function](#section2)                        | [`nnCostFunction`](#nnCostFunction)   | 15
| 3       | [Sigmoid Gradient](#section3)                                 | [`sigmoidGradient`](#sigmoidGradient) | 5
| 4       | [Neural Net Gradient Function (Backpropagation)](#section4)   | [`nnCostFunction`](#nnCostFunction)   | 40
| 5       | [Regularized Gradient](#section5)                             | [`nnCostFunction`](#nnCostFunction)   |10
|         | Total Points                                     |    | 100

You are allowed to submit your solutions multiple times, and we will take only the highest score into consideration.

At the end of each section in this notebook, we have a cell which contains code for submitting the solutions thus far to the grader. Execute the cell to see your score up to the current section. For all your work to be submitted properly, you must execute those cells at least once.

## Neural Networks

In the previous exercise, you implemented feedforward propagation for neural networks and used it to predict handwritten digits with the weights we provided. In this exercise, you will implement the backpropagation algorithm to learn the parameters for the neural network.

We start the exercise by first loading the dataset.

```python
#  training data stored in arrays X, y
data = loadmat(os.path.join('Data', 'ex4data1.mat'))
X, y = data['X'], data['y'].ravel()

# set the zero digit to 0, rather than its mapped 10 in this dataset
# This is an artifact due to the fact that this dataset was used in
# MATLAB where there is no index 0
y[y == 10] = 0

# Number of training examples
m = y.size
```

```python
X.shape
```

    (5000, 400)

### 1.1 Visualizing the data

You will begin by visualizing a subset of the training set, using the function `displayData`, which is the same function we used in Exercise 3. It is provided in the `utils.py` file for this assignment as well. The dataset is also the same one you used in the previous exercise.

There are 5000 training examples in `ex4data1.mat`, where each training example is a 20 pixel by 20 pixel grayscale image of the digit. Each pixel is represented by a floating point number indicating the grayscale intensity at that location. The 20 by 20 grid of pixels is “unrolled” into a 400-dimensional vector. Each
of these training examples becomes a single row in our data matrix $X$. This gives us a 5000 by 400 matrix $X$ where every row is a training example for a handwritten digit image.

$$ X = \begin{bmatrix} - \left(x^{(1)} \right)^T - \\
- \left(x^{(2)} \right)^T - \\
\vdots \\
- \left(x^{(m)} \right)^T - \\
\end{bmatrix}
$$

The second part of the training set is a 5000-dimensional vector `y` that contains labels for the training set.
The following cell randomly selects 100 images from the dataset and plots them.

```python
# Randomly select 100 data points to display
rand_indices = np.random.choice(m, 100, replace=False)
sel = X[rand_indices, :]

utils.displayData(sel)
```

### 1.2 Model representation

Our neural network is shown in the following figure.

It has 3 layers - an input layer, a hidden layer and an output layer. Recall that our inputs are pixel values
of digit images. Since the images are of size $20 \times 20$, this gives us 400 input layer units (not counting the extra bias unit which always outputs +1). The training data was loaded into the variables `X` and `y` above.

You have been provided with a set of network parameters ($\Theta^{(1)}, \Theta^{(2)}$) already trained by us. These are stored in `ex4weights.mat` and will be loaded in the next cell of this notebook into `Theta1` and `Theta2`. The parameters have dimensions that are sized for a neural network with 25 units in the second layer and 10 output units (corresponding to the 10 digit classes).

```python
# Setup the parameters you will use for this exercise
input_layer_size  = 400  # 20x20 Input Images of Digits
hidden_layer_size = 25   # 25 hidden units
num_labels = 10          # 10 labels, from 0 to 9

# Load the weights into variables Theta1 and Theta2
weights = loadmat(os.path.join('Data', 'ex4weights.mat'))

# Theta1 has size 25 x 401
# Theta2 has size 10 x 26
Theta1, Theta2 = weights['Theta1'], weights['Theta2']

# swap first and last columns of Theta2, due to legacy from MATLAB indexing,
# since the weight file ex3weights.mat was saved based on MATLAB indexing
Theta2 = np.roll(Theta2, 1, axis=0)

# Unroll parameters
nn_params = np.concatenate([Theta1.ravel(), Theta2.ravel()])
```

```python
Theta1[:][1:]
```

    array([[ -9.83811294e-02,   7.66168682e-09,  -9.75873689e-09, ...,
             -5.60134007e-05,   2.00940969e-07,   3.54422854e-09],
           [  1.16156052e-01,  -8.77654466e-09,   8.16037764e-09, ...,
             -1.20951657e-04,  -2.33669661e-06,  -7.50668099e-09],
           [ -2.39693299e-01,  -6.31835805e-09,   2.64128630e-09, ...,
             -5.10602800e-05,   8.68409174e-07,   9.88743624e-09],
           ...,
           [ -1.83220638e-01,  -8.89272060e-09,  -9.81968100e-09, ...,
              2.35311186e-05,  -3.25484493e-06,   9.02499060e-09],
           [ -7.02096331e-01,   3.05178374e-10,   2.56061008e-09, ...,
             -8.61759744e-04,   9.43449909e-05,   3.83761998e-09],
           [ -3.50933229e-01,   8.85876862e-09,  -6.57515140e-10, ...,
             -1.80365926e-06,  -8.14464807e-06,   8.79454531e-09]])

```python
nn_params
```

    array([ -2.25623899e-02,  -1.05624163e-08,   2.19414684e-09, ...,
            -2.23093151e-01,  -1.40442136e+00,  -2.13191530e+00])

### 1.3 Feedforward and cost function

Now you will implement the cost function and gradient for the neural network. First, complete the code for the function `nnCostFunction` in the next cell to return the cost.

Recall that the cost function for the neural network (without regularization) is:

$$ J(\theta) = \frac{1}{m} \sum_{i=1}^{m}\sum_{k=1}^{K} \left[ - y_k^{(i)} \log \left( \left( h_\theta \left( x^{(i)} \right) \right)_k \right) - \left( 1 - y_k^{(i)} \right) \log \left( 1 - \left( h_\theta \left( x^{(i)} \right) \right)_k \right) \right]$$

where $h_\theta \left( x^{(i)} \right)$ is computed as shown in the neural network figure above, and K = 10 is the total number of possible labels. Note that $h_\theta(x^{(i)})_k = a_k^{(3)}$ is the activation (output
value) of the $k^{th}$ output unit. Also, recall that whereas the original labels (in the variable y) were 0, 1, ..., 9, for the purpose of training a neural network, we need to encode the labels as vectors containing only values 0 or 1, so that

$$ y =
\begin{bmatrix} 1 \\ 0 \\ 0 \\\vdots \\ 0 \end{bmatrix}, \quad
\begin{bmatrix} 0 \\ 1 \\ 0 \\ \vdots \\ 0 \end{bmatrix}, \quad \cdots  \quad \text{or} \qquad
\begin{bmatrix} 0 \\ 0 \\ 0 \\ \vdots \\ 1 \end{bmatrix}.
$$

For example, if $x^{(i)}$ is an image of the digit 5, then the corresponding $y^{(i)}$ (that you should use with the cost function) should be a 10-dimensional vector with $y_5 = 1$, and the other elements equal to 0.

You should implement the feedforward computation that computes $h_\theta(x^{(i)})$ for every example $i$ and sum the cost over all examples. **Your code should also work for a dataset of any size, with any number of labels** (you can assume that there are always at least $K \ge 3$ labels).

**Implementation Note:** The matrix $X$ contains the examples in rows (i.e., X[i,:] is the i-th training example $x^{(i)}$, expressed as a $n \times 1$ vector.) When you complete the code in `nnCostFunction`, you will need to add the column of 1’s to the X matrix. The parameters for each unit in the neural network is represented in Theta1 and Theta2 as one row. Specifically, the first row of Theta1 corresponds to the first hidden unit in the second layer. You can use a for-loop over the examples to compute the cost.

```python
def nnCostFunction(nn_params,
                   input_layer_size,
                   hidden_layer_size,
                   num_labels,
                   X, y, lambda_=0.0):
    """
    Implements the neural network cost function and gradient for a two layer neural
    network which performs classification.

    Parameters
    ----------
    nn_params : array_like
        The parameters for the neural network which are "unrolled" into
        a vector. This needs to be converted back into the weight matrices Theta1
        and Theta2.

    input_layer_size : int
        Number of features for the input layer.

    hidden_layer_size : int
        Number of hidden units in the second layer.

    num_labels : int
        Total number of labels, or equivalently number of units in output layer.

    X : array_like
        Input dataset. A matrix of shape (m x input_layer_size).

    y : array_like
        Dataset labels. A vector of shape (m,).

    lambda_ : float, optional
        Regularization parameter.

    Returns
    -------
    J : float
        The computed value for the cost function at the current weight values.

    grad : array_like
        An "unrolled" vector of the partial derivatives of the concatenatation of
        neural network weights Theta1 and Theta2.

    Instructions
    ------------
    You should complete the code by working through the following parts.

    - Part 1: Feedforward the neural network and return the cost in the
              variable J. After implementing Part 1, you can verify that your
              cost function computation is correct by verifying the cost
              computed in the following cell.

    - Part 2: Implement the backpropagation algorithm to compute the gradients
              Theta1_grad and Theta2_grad. You should return the partial derivatives of
              the cost function with respect to Theta1 and Theta2 in Theta1_grad and
              Theta2_grad, respectively. After implementing Part 2, you can check
              that your implementation is correct by running checkNNGradients provided
              in the utils.py module.

              Note: The vector y passed into the function is a vector of labels
                    containing values from 0..K-1. You need to map this vector into a
                    binary vector of 1's and 0's to be used with the neural network
                    cost function.

              Hint: We recommend implementing backpropagation using a for-loop
                    over the training examples if you are implementing it for the
                    first time.

    - Part 3: Implement regularization with the cost function and gradients.

              Hint: You can implement this around the code for
                    backpropagation. That is, you can compute the gradients for
                    the regularization separately and then add them to Theta1_grad
                    and Theta2_grad from Part 2.

    Note
    ----
    We have provided an implementation for the sigmoid function in the file
    `utils.py` accompanying this assignment.
    """
    # Reshape nn_params back into the parameters Theta1 and Theta2, the weight matrices
    # for our 2 layer neural network
    Theta1 = np.reshape(nn_params[:hidden_layer_size * (input_layer_size + 1)],
                        (hidden_layer_size, (input_layer_size + 1)))

    Theta2 = np.reshape(nn_params[(hidden_layer_size * (input_layer_size + 1)):],
                        (num_labels, (hidden_layer_size + 1)))

    # Setup some useful variables
    m = y.size

    # You need to return the following variables correctly
    J = 0
    Theta1_grad = np.zeros(Theta1.shape)
    Theta2_grad = np.zeros(Theta2.shape)

    # ====================== YOUR CODE HERE ======================
    Theta1 = nn_params[:(input_layer_size+1)*hidden_layer_size].reshape((hidden_layer_size,input_layer_size+1))
    Theta2 = nn_params[(input_layer_size+1)*hidden_layer_size:].reshape((num_labels,hidden_layer_size+1))
    X = np.concatenate([np.ones((m,1)),X], axis=1)

    z_hidden = np.dot(X,np.transpose(Theta1))
    a_hidden = utils.sigmoid(z_hidden)

    a_hidden = np.concatenate([np.ones((m,1)),a_hidden], axis=1)

    z = np.dot(a_hidden,np.transpose(Theta2))
    a = utils.sigmoid(z)

    actual_a = np.zeros((m,num_labels))
    for i in range(m):
        actual_a[i][y[i]] = 1

    J = np.sum(actual_a*np.log(a) + (1-actual_a)*np.log(1-a))*(-1/m)
    J += (np.sum(Theta1[:,1:]**2)+np.sum(Theta2[:,1:]**2))*(lambda_/(2*m))

    for i in range(m):
        delta_L = a[i] - actual_a[i]
        delta_2 = np.dot(np.transpose(Theta2),delta_L)[1:]
        delta_2 *= sigmoidGradient(z_hidden[i])

        Theta1_grad += np.dot(delta_2.reshape((-1, 1)),np.transpose(X[i].reshape((-1, 1))))
        Theta2_grad += np.dot(delta_L.reshape((-1, 1)),np.transpose(a_hidden[i].reshape((-1, 1))))

    Theta1_grad /= m
    Theta2_grad /= m

    Theta1_grad[:,1:] += (lambda_/m)*Theta1[:,1:]
    Theta2_grad[:,1:] += (lambda_/m)*Theta2[:,1:]

    # ================================================================
    # Unroll gradients
    # grad = np.concatenate([Theta1_grad.ravel(order=order), Theta2_grad.ravel(order=order)])
    grad = np.concatenate([Theta1_grad.ravel(), Theta2_grad.ravel()])

    return J, grad
```

Use the following links to go back to the different parts of this exercise that require to modify the function `nnCostFunction`.

Back to:
- [Feedforward and cost function](#section1)
- [Regularized cost](#section2)
- [Neural Network Gradient (Backpropagation)](#section4)
- [Regularized Gradient](#section5)

Once you are done, call your `nnCostFunction` using the loaded set of parameters for `Theta1` and `Theta2`. You should see that the cost is about 0.287629.

```python
lambda_ = 0
J, _ = nnCostFunction(nn_params, input_layer_size, hidden_layer_size,
                   num_labels, X, y, lambda_)
print('Cost at parameters (loaded from ex4weights): %.6f ' % J)
print('The cost should be about                   : 0.287629.')
```

    (25, 401)
    Cost at parameters (loaded from ex4weights): 0.287629
    The cost should be about                   : 0.287629.

*You should now submit your solutions.*

```python
grader = utils.Grader()
grader[1] = nnCostFunction
grader.grade()
```

    Submitting Solutions | Programming Exercise neural-network-learning

    Use token from last successful submission (ionejunhong@gmail.com)? (Y/n): Y
                                      Part Name |     Score | Feedback
                                      --------- |     ----- | --------
                  Feedforward and Cost Function |  30 /  30 | Nice work!
                      Regularized Cost Function |   0 /  15 |
                               Sigmoid Gradient |   0 /   5 |
      Neural Network Gradient (Backpropagation) |   0 /  40 |
                           Regularized Gradient |   0 /  10 |
                                      --------------------------------
                                                |  30 / 100 |

### 1.4 Regularized cost function

The cost function for neural networks with regularization is given by:

$$ J(\theta) = \frac{1}{m} \sum_{i=1}^{m}\sum_{k=1}^{K} \left[ - y_k^{(i)} \log \left( \left( h_\theta \left( x^{(i)} \right) \right)_k \right) - \left( 1 - y_k^{(i)} \right) \log \left( 1 - \left( h_\theta \left( x^{(i)} \right) \right)_k \right) \right] + \frac{\lambda}{2 m} \left[ \sum_{j=1}^{25} \sum_{k=1}^{400} \left( \Theta_{j,k}^{(1)} \right)^2 + \sum_{j=1}^{10} \sum_{k=1}^{25} \left( \Theta_{j,k}^{(2)} \right)^2 \right] $$

You can assume that the neural network will only have 3 layers - an input layer, a hidden layer and an output layer. However, your code should work for any number of input units, hidden units and outputs units. While we
have explicitly listed the indices above for $\Theta^{(1)}$ and $\Theta^{(2)}$ for clarity, do note that your code should in general work with $\Theta^{(1)}$ and $\Theta^{(2)}$ of any size. Note that you should not be regularizing the terms that correspond to the bias. For the matrices `Theta1` and `Theta2`, this corresponds to the first column of each matrix. You should now add regularization to your cost function. Notice that you can first compute the unregularized cost function $J$ using your existing `nnCostFunction` and then later add the cost for the regularization terms.

[Click here to go back to `nnCostFunction` for editing.](#nnCostFunction)

Once you are done, the next cell will call your `nnCostFunction` using the loaded set of parameters for `Theta1` and `Theta2`, and $\lambda = 1$. You should see that the cost is about 0.383770.

```python
# Weight regularization parameter (we set this to 1 here).
lambda_ = 1
J, _ = nnCostFunction(nn_params, input_layer_size, hidden_layer_size,
                      num_labels, X, y, lambda_)

print('Cost at parameters (loaded from ex4weights): %.6f' % J)
print('This value should be about                 : 0.383770.')
```

    (25, 401)
    Cost at parameters (loaded from ex4weights): 0.383770
    This value should be about                 : 0.383770.

*You should now submit your solutions.*

```python
grader[2] = nnCostFunction
grader.grade()
```

    Submitting Solutions | Programming Exercise neural-network-learning

    Use token from last successful submission (ionejunhong@gmail.com)? (Y/n): Y
    (4, 3)
                                      Part Name |     Score | Feedback
                                      --------- |     ----- | --------
                  Feedforward and Cost Function |  30 /  30 | Nice work!
                      Regularized Cost Function |  15 /  15 | Nice work!
                               Sigmoid Gradient |   0 /   5 |
      Neural Network Gradient (Backpropagation) |   0 /  40 |
                           Regularized Gradient |   0 /  10 |
                                      --------------------------------
                                                |  45 / 100 |

## 2 Backpropagation

In this part of the exercise, you will implement the backpropagation algorithm to compute the gradient for the neural network cost function. You will need to update the function `nnCostFunction` so that it returns an appropriate value for `grad`. Once you have computed the gradient, you will be able to train the neural network by minimizing the cost function $J(\theta)$ using an advanced optimizer such as `scipy`'s `optimize.minimize`.
You will first implement the backpropagation algorithm to compute the gradients for the parameters for the (unregularized) neural network. After you have verified that your gradient computation for the unregularized case is correct, you will implement the gradient for the regularized neural network.

### 2.1 Sigmoid Gradient

To help you get started with this part of the exercise, you will first implement
the sigmoid gradient function. The gradient for the sigmoid function can be
computed as

$$ g'(z) = \frac{d}{dz} g(z) = g(z)\left(1-g(z)\right) $$

where

$$ \text{sigmoid}(z) = g(z) = \frac{1}{1 + e^{-z}} $$

Now complete the implementation of `sigmoidGradient` in the next cell.

```python
def sigmoidGradient(z):
    """
    Computes the gradient of the sigmoid function evaluated at z.
    This should work regardless if z is a matrix or a vector.
    In particular, if z is a vector or matrix, you should return
    the gradient for each element.

    Parameters
    ----------
    z : array_like
        A vector or matrix as input to the sigmoid function.

    Returns
    --------
    g : array_like
        Gradient of the sigmoid function. Has the same shape as z.

    Instructions
    ------------
    Compute the gradient of the sigmoid function evaluated at
    each value of z (z can be a matrix, vector or scalar).

    Note
    ----
    We have provided an implementation of the sigmoid function
    in `utils.py` file accompanying this assignment.
    """

    g = np.zeros(z.shape)

    # ====================== YOUR CODE HERE ======================

    a = 1.0 / (1.0 + np.exp(-z))
    g = a*(1-a)

    # =============================================================
    return g
```

When you are done, the following cell call `sigmoidGradient` on a given vector `z`. Try testing a few values by calling `sigmoidGradient(z)`. For large values (both positive and negative) of z, the gradient should be close to 0. When $z = 0$, the gradient should be exactly 0.25. Your code should also work with vectors and matrices. For a matrix, your function should perform the sigmoid gradient function on every element.

```python
z = np.array([-1, -0.5, 0, 0.5, 1])
g = sigmoidGradient(z)
print('Sigmoid gradient evaluated at [-1 -0.5 0 0.5 1]:\n  ')
print(g)
```

    Sigmoid gradient evaluated at [-1 -0.5 0 0.5 1]:

    [ 0.19661193  0.23500371  0.25        0.23500371  0.19661193]

*You should now submit your solutions.*

```python
grader[3] = sigmoidGradient
grader.grade()
```

    Submitting Solutions | Programming Exercise neural-network-learning

    Use token from last successful submission (ionejunhong@gmail.com)? (Y/n): Y
    (4, 3)
                                      Part Name |     Score | Feedback
                                      --------- |     ----- | --------
                  Feedforward and Cost Function |  30 /  30 | Nice work!
                      Regularized Cost Function |  15 /  15 | Nice work!
                               Sigmoid Gradient |   5 /   5 | Nice work!
      Neural Network Gradient (Backpropagation) |   0 /  40 |
                           Regularized Gradient |   0 /  10 |
                                      --------------------------------
                                                |  50 / 100 |

## 2.2 Random Initialization

When training neural networks, it is important to randomly initialize the parameters for symmetry breaking. One effective strategy for random initialization is to randomly select values for $\Theta^{(l)}$ uniformly in the range $[-\epsilon_{init}, \epsilon_{init}]$. You should use $\epsilon_{init} = 0.12$. This range of values ensures that the parameters are kept small and makes the learning more efficient.

One effective strategy for choosing $\epsilon_{init}$ is to base it on the number of units in the network. A good choice of $\epsilon_{init}$ is $\epsilon_{init} = \frac{\sqrt{6}}{\sqrt{L_{in} + L_{out}}}$ where $L_{in} = s_l$ and $L_{out} = s_{l+1}$ are the number of units in the layers adjacent to $\Theta^{l}$.

Your job is to complete the function `randInitializeWeights` to initialize the weights for $\Theta$. Modify the function by filling in the following code:

```python
# Randomly initialize the weights to small values
W = np.random.rand(L_out, 1 + L_in) * 2 * epsilon_init - epsilon_init
```
Note that we give the function an argument for $\epsilon$ with default value `epsilon_init = 0.12`.

```python
def randInitializeWeights(L_in, L_out, epsilon_init=0.12):
    """
    Randomly initialize the weights of a layer in a neural network.

    Parameters
    ----------
    L_in : int
        Number of incomming connections.

    L_out : int
        Number of outgoing connections.

    epsilon_init : float, optional
        Range of values which the weight can take from a uniform
        distribution.

    Returns
    -------
    W : array_like
        The weight initialiatized to random values.  Note that W should
        be set to a matrix of size(L_out, 1 + L_in) as
        the first column of W handles the "bias" terms.

    Instructions
    ------------
    Initialize W randomly so that we break the symmetry while training
    the neural network. Note that the first column of W corresponds
    to the parameters for the bias unit.
    """

    # You need to return the following variables correctly
    W = np.zeros((L_out, 1 + L_in))

    # ====================== YOUR CODE HERE ======================
    W = np.random.rand(L_out, 1 + L_in) * 2 * epsilon_init - epsilon_init

    # ============================================================
    return W
```

*You do not need to submit any code for this part of the exercise.*

Execute the following cell to initialize the weights for the 2 layers in the neural network using the `randInitializeWeights` function.

```python
print('Initializing Neural Network Parameters ...')

initial_Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size)
initial_Theta2 = randInitializeWeights(hidden_layer_size, num_labels)

# Unroll parameters
initial_nn_params = np.concatenate([initial_Theta1.ravel(), initial_Theta2.ravel()], axis=0)
```

    Initializing Neural Network Parameters ...

### 2.4 Backpropagation

Now, you will implement the backpropagation algorithm. Recall that the intuition behind the backpropagation algorithm is as follows. Given a training example $(x^{(t)}, y^{(t)})$, we will first run a “forward pass” to compute all the activations throughout the network, including the output value of the hypothesis $h_\theta(x)$. Then, for each node $j$ in layer $l$, we would like to compute an “error term” $\delta_j^{(l)}$ that measures how much that node was “responsible” for any errors in our output.

For an output node, we can directly measure the difference between the network’s activation and the true target value, and use that to define $\delta_j^{(3)}$ (since layer 3 is the output layer). For the hidden units, you will compute $\delta_j^{(l)}$ based on a weighted average of the error terms of the nodes in layer $(l+1)$. In detail, here is the backpropagation algorithm (also depicted in the figure above). You should implement steps 1 to 4 in a loop that processes one example at a time. Concretely, you should implement a for-loop `for t in range(m)` and place steps 1-4 below inside the for-loop, with the $t^{th}$ iteration performing the calculation on the $t^{th}$ training example $(x^{(t)}, y^{(t)})$. Step 5 will divide the accumulated gradients by $m$ to obtain the gradients for the neural network cost function.

1. Set the input layer’s values $(a^{(1)})$ to the $t^{th }$training example $x^{(t)}$. Perform a feedforward pass, computing the activations $(z^{(2)}, a^{(2)}, z^{(3)}, a^{(3)})$ for layers 2 and 3. Note that you need to add a `+1` term to ensure that the vectors of activations for layers $a^{(1)}$ and $a^{(2)}$ also include the bias unit. In `numpy`, if a 1 is a column matrix, adding one corresponds to `a_1 = np.concatenate([np.ones((m, 1)), a_1], axis=1)`.

1. For each output unit $k$ in layer 3 (the output layer), set
$$\delta_k^{(3)} = \left(a_k^{(3)} - y_k \right)$$
where $y_k \in \{0, 1\}$ indicates whether the current training example belongs to class $k$ $(y_k = 1)$, or if it belongs to a different class $(y_k = 0)$. You may find logical arrays helpful for this task (explained in the previous programming exercise).

1. For the hidden layer $l = 2$, set
$$ \delta^{(2)} = \left( \Theta^{(2)} \right)^T \delta^{(3)} * g'\left(z^{(2)} \right)$$
Note that the symbol $*$ performs element wise multiplication in `numpy`.

1. Accumulate the gradient from this example using the following formula. Note that you should skip or remove $\delta_0^{(2)}$. In `numpy`, removing $\delta_0^{(2)}$ corresponds to `delta_2 = delta_2[1:]`.

1. Obtain the (unregularized) gradient for the neural network cost function by dividing the accumulated gradients by $\frac{1}{m}$:
$$ \frac{\partial}{\partial \Theta_{ij}^{(l)}} J(\Theta) = D_{ij}^{(l)} = \frac{1}{m} \Delta_{ij}^{(l)}$$

**Python/Numpy tip**: You should implement the backpropagation algorithm only after you have successfully completed the feedforward and cost functions. While implementing the backpropagation alogrithm, it is often useful to use the `shape` function to print out the shapes of the variables you are working with if you run into dimension mismatch errors.

[Click here to go back and update the function `nnCostFunction` with the backpropagation algorithm](#nnCostFunction).

After you have implemented the backpropagation algorithm, we will proceed to run gradient checking on your implementation. The gradient check will allow you to increase your confidence that your code is
computing the gradients correctly.

### 2.4  Gradient checking

In your neural network, you are minimizing the cost function $J(\Theta)$. To perform gradient checking on your parameters, you can imagine “unrolling” the parameters $\Theta^{(1)}$, $\Theta^{(2)}$ into a long vector $\theta$. By doing so, you can think of the cost function being $J(\Theta)$ instead and use the following gradient checking procedure.

Suppose you have a function $f_i(\theta)$ that purportedly computes $\frac{\partial}{\partial \theta_i} J(\theta)$; you’d like to check if $f_i$ is outputting correct derivative values.

$$
\text{Let } \theta^{(i+)} = \theta + \begin{bmatrix} 0 \\ 0 \\ \vdots \\ \epsilon \\ \vdots \\ 0 \end{bmatrix}
\quad \text{and} \quad \theta^{(i-)} = \theta - \begin{bmatrix} 0 \\ 0 \\ \vdots \\ \epsilon \\ \vdots \\ 0 \end{bmatrix}
$$

So, $\theta^{(i+)}$ is the same as $\theta$, except its $i^{th}$ element has been incremented by $\epsilon$. Similarly, $\theta^{(i−)}$ is the corresponding vector with the $i^{th}$ element decreased by $\epsilon$. You can now numerically verify $f_i(\theta)$’s correctness by checking, for each $i$, that:

$$ f_i\left( \theta \right) \approx \frac{J\left( \theta^{(i+)}\right) - J\left( \theta^{(i-)} \right)}{2\epsilon} $$

The degree to which these two values should approximate each other will depend on the details of $J$. But assuming $\epsilon = 10^{-4}$, you’ll usually find that the left- and right-hand sides of the above will agree to at least 4 significant digits (and often many more).

We have implemented the function to compute the numerical gradient for you in `computeNumericalGradient` (within the file `utils.py`). While you are not required to modify the file, we highly encourage you to take a look at the code to understand how it works.

In the next cell we will run the provided function `checkNNGradients` which will create a small neural network and dataset that will be used for checking your gradients. If your backpropagation implementation is correct,
you should see a relative difference that is less than 1e-9.

**Practical Tip**: When performing gradient checking, it is much more efficient to use a small neural network with a relatively small number of input units and hidden units, thus having a relatively small number
of parameters. Each dimension of $\theta$ requires two evaluations of the cost function and this can be expensive. In the function `checkNNGradients`, our code creates a small random model and dataset which is used with `computeNumericalGradient` for gradient checking. Furthermore, after you are confident that your gradient computations are correct, you should turn off gradient checking before running your learning algorithm.

**Practical Tip:** Gradient checking works for any function where you are computing the cost and the gradient. Concretely, you can use the same `computeNumericalGradient` function to check if your gradient implementations for the other exercises are correct too (e.g., logistic regression’s cost function).

```python
utils.checkNNGradients(nnCostFunction)
```

    [[ -9.27825235e-03  -9.27825236e-03]
     [ -3.04978931e-06  -3.04978914e-06]
     [ -1.75060084e-04  -1.75060082e-04]
     [ -9.62660640e-05  -9.62660620e-05]
     [  8.89911959e-03   8.89911960e-03]
     [  1.42869450e-05   1.42869443e-05]
     [  2.33146358e-04   2.33146357e-04]
     [  1.17982666e-04   1.17982666e-04]
     [ -8.36010761e-03  -8.36010762e-03]
     [ -2.59383093e-05  -2.59383100e-05]
     [ -2.87468729e-04  -2.87468729e-04]
     [ -1.37149709e-04  -1.37149706e-04]
     [  7.62813550e-03   7.62813551e-03]
     [  3.69883257e-05   3.69883234e-05]
     [  3.35320351e-04   3.35320347e-04]
     [  1.53247082e-04   1.53247082e-04]
     [ -6.74798369e-03  -6.74798370e-03]
     [ -4.68759764e-05  -4.68759769e-05]
     [ -3.76215585e-04  -3.76215587e-04]
     [ -1.66560294e-04  -1.66560294e-04]
     [  3.14544970e-01   3.14544970e-01]
     [  1.64090819e-01   1.64090819e-01]
     [  1.64567932e-01   1.64567932e-01]
     [  1.58339334e-01   1.58339334e-01]
     [  1.51127527e-01   1.51127527e-01]
     [  1.49568335e-01   1.49568335e-01]
     [  1.11056588e-01   1.11056588e-01]
     [  5.75736494e-02   5.75736493e-02]
     [  5.77867378e-02   5.77867378e-02]
     [  5.59235296e-02   5.59235296e-02]
     [  5.36967009e-02   5.36967009e-02]
     [  5.31542052e-02   5.31542052e-02]
     [  9.74006970e-02   9.74006970e-02]
     [  5.04575855e-02   5.04575855e-02]
     [  5.07530173e-02   5.07530173e-02]
     [  4.91620841e-02   4.91620841e-02]
     [  4.71456249e-02   4.71456249e-02]
     [  4.65597186e-02   4.65597186e-02]]
    The above two columns you get should be very similar.
    (Left-Your Numerical Gradient, Right-Analytical Gradient)

    If your backpropagation implementation is correct, then
    the relative difference will be small (less than 1e-9).
    Relative Difference: 2.38572e-11

*Once your cost function passes the gradient check for the (unregularized) neural network cost function, you should submit the neural network gradient function (backpropagation).*

```python
grader[4] = nnCostFunction
grader.grade()
```

    Submitting Solutions | Programming Exercise neural-network-learning

    Use token from last successful submission (ionejunhong@gmail.com)? (Y/n): Y
    (4, 3)
                                      Part Name |     Score | Feedback
                                      --------- |     ----- | --------
                  Feedforward and Cost Function |  30 /  30 | Nice work!
                      Regularized Cost Function |  15 /  15 | Nice work!
                               Sigmoid Gradient |   5 /   5 | Nice work!
      Neural Network Gradient (Backpropagation) |  40 /  40 | Nice work!
                           Regularized Gradient |   0 /  10 |
                                      --------------------------------
                                                |  90 / 100 |

### 2.5 Regularized Neural Network

After you have successfully implemented the backpropagation algorithm, you will add regularization to the gradient. To account for regularization, it turns out that you can add this as an additional term *after* computing the gradients using backpropagation.

Specifically, after you have computed $\Delta_{ij}^{(l)}$ using backpropagation, you should add regularization using

$$ \begin{align}
& \frac{\partial}{\partial \Theta_{ij}^{(l)}} J(\Theta) = D_{ij}^{(l)} = \frac{1}{m} \Delta_{ij}^{(l)} & \qquad \text{for } j = 0 \\
& \frac{\partial}{\partial \Theta_{ij}^{(l)}} J(\Theta) = D_{ij}^{(l)} = \frac{1}{m} \Delta_{ij}^{(l)} + \frac{\lambda}{m} \Theta_{ij}^{(l)} & \qquad \text{for } j \ge 1
\end{align}
$$

Note that you should *not* be regularizing the first column of $\Theta^{(l)}$ which is used for the bias term. Furthermore, in the parameters $\Theta_{ij}^{(l)}$, $i$ is indexed starting from 1, and $j$ is indexed starting from 0. Thus,

$$
\Theta^{(l)} = \begin{bmatrix}
\Theta_{1,0}^{(i)} & \Theta_{1,1}^{(l)} & \cdots \\
\Theta_{2,0}^{(i)} & \Theta_{2,1}^{(l)} & \cdots \\
\vdots &  ~ & \ddots
\end{bmatrix}
$$

[Now modify your code that computes grad in `nnCostFunction` to account for regularization.](#nnCostFunction)

After you are done, the following cell runs gradient checking on your implementation. If your code is correct, you should expect to see a relative difference that is less than 1e-9.

```python
#  Check gradients by running checkNNGradients
lambda_ = 3
utils.checkNNGradients(nnCostFunction, lambda_)

# Also output the costFunction debugging values
debug_J, _  = nnCostFunction(nn_params, input_layer_size,
                          hidden_layer_size, num_labels, X, y, lambda_)

print('\n\nCost at (fixed) debugging parameters (w/ lambda = %f): %f ' % (lambda_, debug_J))
print('(for lambda = 3, this value should be about 0.576051)')
```

    [[ -9.27825235e-03  -9.27825236e-03]
     [ -1.67679797e-02  -1.67679797e-02]
     [ -6.01744725e-02  -6.01744725e-02]
     [ -1.73704651e-02  -1.73704651e-02]
     [  8.89911959e-03   8.89911960e-03]
     [  3.94334829e-02   3.94334829e-02]
     [ -3.19612287e-02  -3.19612287e-02]
     [ -5.75658668e-02  -5.75658668e-02]
     [ -8.36010761e-03  -8.36010762e-03]
     [  5.93355565e-02   5.93355565e-02]
     [  2.49225535e-02   2.49225535e-02]
     [ -4.51963845e-02  -4.51963845e-02]
     [  7.62813550e-03   7.62813551e-03]
     [  2.47640974e-02   2.47640974e-02]
     [  5.97717617e-02   5.97717617e-02]
     [  9.14587966e-03   9.14587966e-03]
     [ -6.74798369e-03  -6.74798370e-03]
     [ -3.26881426e-02  -3.26881426e-02]
     [  3.86410548e-02   3.86410548e-02]
     [  5.46101547e-02   5.46101547e-02]
     [  3.14544970e-01   3.14544970e-01]
     [  1.18682669e-01   1.18682669e-01]
     [  2.03987128e-01   2.03987128e-01]
     [  1.25698067e-01   1.25698067e-01]
     [  1.76337550e-01   1.76337550e-01]
     [  1.32294136e-01   1.32294136e-01]
     [  1.11056588e-01   1.11056588e-01]
     [  3.81928689e-05   3.81928696e-05]
     [  1.17148233e-01   1.17148233e-01]
     [ -4.07588279e-03  -4.07588279e-03]
     [  1.13133142e-01   1.13133142e-01]
     [ -4.52964427e-03  -4.52964427e-03]
     [  9.74006970e-02   9.74006970e-02]
     [  3.36926556e-02   3.36926556e-02]
     [  7.54801264e-02   7.54801264e-02]
     [  1.69677090e-02   1.69677090e-02]
     [  8.61628953e-02   8.61628953e-02]
     [  1.50048382e-03   1.50048382e-03]]
    The above two columns you get should be very similar.
    (Left-Your Numerical Gradient, Right-Analytical Gradient)

    If your backpropagation implementation is correct, then
    the relative difference will be small (less than 1e-9).
    Relative Difference: 2.28634e-11

    Cost at (fixed) debugging parameters (w/ lambda = 3.000000): 0.576051
    (for lambda = 3, this value should be about 0.576051)

```python
grader[5] = nnCostFunction
grader.grade()
```

    Submitting Solutions | Programming Exercise neural-network-learning

    Use token from last successful submission (ionejunhong@gmail.com)? (Y/n): Y
    (4, 3)
                                      Part Name |     Score | Feedback
                                      --------- |     ----- | --------
                  Feedforward and Cost Function |  30 /  30 | Nice work!
                      Regularized Cost Function |  15 /  15 | Nice work!
                               Sigmoid Gradient |   5 /   5 | Nice work!
      Neural Network Gradient (Backpropagation) |  40 /  40 | Nice work!
                           Regularized Gradient |  10 /  10 | Nice work!
                                      --------------------------------
                                                | 100 / 100 |

### 2.6 Learning parameters using `scipy.optimize.minimize`

After you have successfully implemented the neural network cost function
and gradient computation, the next step we will use `scipy`'s minimization to learn a good set parameters.

```python
#  After you have completed the assignment, change the maxiter to a larger
#  value to see how more training helps.
options= {'maxiter': 100}

#  You should also try different values of lambda
lambda_ = 1

# Create "short hand" for the cost function to be minimized
costFunction = lambda p: nnCostFunction(p, input_layer_size,
                                        hidden_layer_size,
                                        num_labels, X, y, lambda_)

# Now, costFunction is a function that takes in only one argument
# (the neural network parameters)
res = optimize.minimize(costFunction,
                        initial_nn_params,
                        jac=True,
                        method='TNC',
                        options=options)

# get the solution of the optimization
nn_params = res.x

# Obtain Theta1 and Theta2 back from nn_params
Theta1 = np.reshape(nn_params[:hidden_layer_size * (input_layer_size + 1)],
                    (hidden_layer_size, (input_layer_size + 1)))

Theta2 = np.reshape(nn_params[(hidden_layer_size * (input_layer_size + 1)):],
                    (num_labels, (hidden_layer_size + 1)))
```

After the training completes, we will proceed to report the training accuracy of your classifier by computing the percentage of examples it got correct. If your implementation is correct, you should see a reported
training accuracy of about 95.3% (this may vary by about 1% due to the random initialization). It is possible to get higher training accuracies by training the neural network for more iterations. We encourage you to try
training the neural network for more iterations (e.g., set `maxiter` to 400) and also vary the regularization parameter $\lambda$. With the right learning settings, it is possible to get the neural network to perfectly fit the training set.

```python
pred = utils.predict(Theta1, Theta2, X)
print('Training Set Accuracy: %f' % (np.mean(pred == y) * 100))
```

    Training Set Accuracy: 93.580000

## 3 Visualizing the Hidden Layer

One way to understand what your neural network is learning is to visualize what the representations captured by the hidden units. Informally, given a particular hidden unit, one way to visualize what it computes is to find an input $x$ that will cause it to activate (that is, to have an activation value
($a_i^{(l)}$) close to 1). For the neural network you trained, notice that the $i^{th}$ row of $\Theta^{(1)}$ is a 401-dimensional vector that represents the parameter for the $i^{th}$ hidden unit. If we discard the bias term, we get a 400 dimensional vector that represents the weights from each input pixel to the hidden unit.

Thus, one way to visualize the “representation” captured by the hidden unit is to reshape this 400 dimensional vector into a 20 × 20 image and display it (It turns out that this is equivalent to finding the input that gives the highest activation for the hidden unit, given a “norm” constraint on the input (i.e., $||x||_2 \le 1$)).

The next cell does this by using the `displayData` function and it will show you an image with 25 units,
each corresponding to one hidden unit in the network. In your trained network, you should find that the hidden units corresponds roughly to detectors that look for strokes and other patterns in the input.

```python
utils.displayData(Theta1[:, 1:])
```

### 3.1 Optional (ungraded) exercise

In this part of the exercise, you will get to try out different learning settings for the neural network to see how the performance of the neural network varies with the regularization parameter $\lambda$ and number of training steps (the `maxiter` option when using `scipy.optimize.minimize`). Neural networks are very powerful models that can form highly complex decision boundaries. Without regularization, it is possible for a neural network to “overfit” a training set so that it obtains close to 100% accuracy on the training set but does not as well on new examples that it has not seen before. You can set the regularization $\lambda$ to a smaller value and the `maxiter` parameter to a higher number of iterations to see this for youself.


# Using Laplace Transforms to solve a linear differential equation in SymPy

Consider solving

$$3\ddot{x}+30\dot{x}+63x=4\dot{g}(t)+6g(t)$$
where
$$g(t)=u_s(t)$$
is the unit step function (Heaviside Function)
and $x(0) = 4$ and $\dot{x}(0)=7$.

```python
%matplotlib inline
from sympy.integrals.transforms import laplace_transform
from sympy.integrals.transforms import inverse_laplace_transform
from sympy import *
import sympy as sympy
from sympy.abc import a, t, x, s, X, g, G
init_printing(use_unicode=True)
```

```python
x, g, X = symbols('x g X', cls = Function)
```

```python
x0 = 4
v0 = 7
```

```python
g = Heaviside(t) #This is just the step function
```

```python
laplace_transform(Heaviside(t), t, s) # Checking the performance of the Laplace Transform Module
```

    (1/s, 0, True)

```python
laplace_transform(DiracDelta(t), t, s) # Checking the performance... this is wrong. Should get 1.
```

    (-Heaviside(0) + 1, -oo, True)

```python
numer(laplace_transform(DiracDelta(t), t, s) )
```

```python
laplace_transform(DiracDelta(t - 2), t, s) # right result
```

    (exp(-2*s), -oo, True)

```python
laplace_transform(DiracDelta(t - a), t, s)[0] # right result (theta(t) is a Heaviside function representation in SymPy)
```

```python
laplace_transform(DiracDelta(t - a), t, s)[0].subs(a,2) # Looks good
```

```python
laplace_transform(DiracDelta(t - a), t, s)[0].subs(a,1) # Looks good
```

```python
laplace_transform(DiracDelta(t - a), t, s)[0].subs(a,0.0000001) # Looks to converge
```

```python
laplace_transform(DiracDelta(t - a), t, s)[0].subs(a,-0.0000001) #So... here is the illustration.
```

What's happening here is that SymPy currently takes the position that half the diract happens before zero, half after, so the result should only be half as big. I think I'm in a rather large crowd in saying that this isn't proper. It makes some theoretical sense, and is a wonderful math debate. However, given convention says that $\delta(t)$ is fully captured by a Laplace transform with a result of $1$ (Mathematica, Maple, Matlab, every System Dynamics, Controls, and Signal Processing book I've ever read), SymPy is practically wrong. I'm hoping that they will change their minds. I am now a bit skeptical about using SymPy for my math work as the results of a simple conventional application don't match expected behavior consistent with other codes.

So we note that SymPy isn't taking the Laplace Transform properly here, so we need to avoid using this result. (we should have gotten 1) Valid as of 0.7.6.1

```python
print(sympy.__version__)
```

    1.0

The first line below would work if SymPy performed the Laplace Transform of the Dirac Delta correctly. Short of that,
we manually insert the Laplace Transform of $g(t)$ and $\dot{g}(t)$ where $g(t)=u(t)$.

```python
#eom  = Eq(3*(s**2 * X(s)-s*x0-v0)+30 * (s*X(s)-x0)+ 63 * X(s), laplace_transform( 4 * diff(g,t) + 6 * g, t, s, noconds = True))

eom  = Eq(3*(s**2 * X(s)-s*x0-v0)+30 * (s*X(s)-x0)+ 63 * X(s), 4 * 1 + 6 * 1/s)
eom
```

```python
Xofs = solve(eom,X(s))
Xofs[0]
```

```python
soln = inverse_laplace_transform(Xofs[0],s,t)
soln
```

```python
soln_simp = expand(soln)
soln_simp
```

```python
N(soln_simp,5)
```

Note that $\theta(t)$ is SymPy's notation for a step function. This simply means the answer can't be used before $t=0$.

```python

```


Table of Contents

1   Discrete Uniform Distribution

1.1   Discrete Probability Mass Function

2   Binomial Distribution

2.1    Example 1

2.2    Example 2

2.3   Binomial R.V. Generator

2.4   Moments of Binomial Distribution

3   Poisson Distribution

3.1  Example

3.2   Poisson R.V. Generator

3.3   Moments of Poisson Distribution

4   Geometric Distribution

4.1  Example

4.2   G-D Moments and Generator

5   Hypergeometric Distribution

5.1   Example

6   Continous Uniform Distribution

6.1   CDF and PDF of Continous Uniform Distribution

7   Normal Distribution

7.1   Inverse Normal CDF

7.2   Normal R.V. Generator

7.3   Bivariate Normal Distribution

7.3.1   1st Method of Formulation

7.3.2   2st Method of Formulation

8   Beta Distribution

9   <span id="MathJax-Element-503-Frame" class="mjx-chtml MathJax_CHTML" tabindex="0" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>&#x03C7;</mi><mn>2</mn></msup></math>" role="presentation" style="font-size: 109%; position: relative;">χ2<math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>χ</mi><mn>2</mn></msup></math>$\chi^2$ Distribution

9.1   <span id="MathJax-Element-542-Frame" class="mjx-chtml MathJax_CHTML" tabindex="0" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>&#x03C7;</mi><mn>2</mn></msup></math>" role="presentation" style="font-size: 109%; position: relative;">χ2<math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>χ</mi><mn>2</mn></msup></math>$\chi^2$ PDF and CDF

10   F Distribution

```python
from texttable import Texttable
import numpy as np
import matplotlib.pyplot as plt
import scipy as sp
import pandas as pd
import scipy.stats
```

```python
import warnings
warnings.filterwarnings("ignore")
```

In this chapter, we will only be dealing with special distributions which are frequently encountered in practice.

#  Discrete Uniform Distribution

We have seen PMF and PDF in last chapter, here we review an example of discrete uniform distribution.

```python
unif_d = sp.stats.randint.rvs(0, 10, size = 1000)
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(unif_d, density = True, bins = 30)
ax.set_title('Discrete Uniform Frequency Distribution', size = 16)
plt.show()
```

##  Discrete Probability Mass Function

```python
x = np.arange(1, 11)
unif_pmf = sp.stats.randint.pmf(x, 2, 10)
fig, ax = plt.subplots(figsize = (12, 7))
ax.scatter(x, unif_pmf, s = 100, color = 'green', label =  'Low = 2, High = 9')
ax.plot(x,unif_pmf, lw = 3, color = 'k')
ax.legend(fontsize = 18, loc = 'center')
```

#  Binomial Distribution

The binomial experiment has 4 properties:

* A sequence of $n$ identical trials
* Only two outcomes are possible: $success$ or $failure$
* The probability of success $p$ does not change from trial to trial
* Trials are independent events

The PMF of binomial ditribution is

\begin{equation}
_nC_k p^kq^{n-k}
\end{equation}

##   Example 1

We use a simple example to explain the PMF.

 Every month, a personal banker might meet 50 people enquiring loans, emperically 30% of them have bad credit history. So calculate probability from 1:50 people that have bad credit history.

First we can asnwer what the probability is that the banker meet exactly $14$ people who have bad credit history?

```python
n = 50
k = 14 # what is the prob that exact 14 ppl she met had bad credit history?
b = scipy.special.comb(50, 14)
p = .3
f_bino = b*p**k*(1-p)**(n-k)
print('The prob of meeting {0} persons who have bad credit history is {1:.2f}%.'.format(k, f_bino * 100))
```

    The prob of meeting 14 persons who have bad credit history is 11.89%.

Or we can use ```scipy.stats.binom.pmf```. To show the probability from $1$ to $50$ persons.

```python
n = 50
p = .3
bad_credit = np.arange(1, 51)
y = sp.stats.binom.pmf(bad_credit, n, p)
fig, ax = plt.subplots(figsize = (10, 6))
ax.plot(bad_credit, y, lw = 3, color = 'r', alpha = .5)
ax.set_ylim([0, .13])
ax.set_title('The probability that from 1 to 50 persons who have bad credit history', size = 16, x = .5, y = 1.02)
ax.set_xlabel('Number of Person Who Has Bad Credit History', size = 12)
ax.set_ylabel('Probability', size = 12)
plt.show()
```

Because there are the $30\%$ of people who have bad credit history, thus we can see the mean of the distribution is $15$ out of $50$.

##   Example 2

Next we can formulate another question using ```scipy.stats.binom.cdf```.

If a trade trades $n$ times a month, he has a $p%$ chance of winning the trade, find out the probability that he can win at least $k$ trades a month.

We can also ask what are the probabilites he wins more than $k$ trades, or between $(k_1, \ k_2)$ trades.

```python
n = 20 # number of trades
p = .55 # winning odds
k = 12 # at least win k trades
k1 = 14
k2 = 4

win_less = sp.stats.binom.cdf(k, n, p)
win_more = 1- sp.stats.binom.cdf(k, n, p)
win_betw = sp.stats.binom.cdf(k1, n, p) - sp.stats.binom.cdf(k2, n, p)
```

```python
table = Texttable()
table.set_cols_align([ 'c', 'c', 'c'])
table.set_cols_valign([ 'm', 'm', 'm'])
table.add_rows([['Win Less', ' Win More ', ' Win btw 4~14 '],
                [ win_less, win_more, win_betw]])
print(table.draw())
```

    +----------+------------+----------------+
    | Win Less |  Win More  |  Win btw 4~14  |
    +==========+============+================+
    |  0.748   |   0.252    |     0.943      |
    +----------+------------+----------------+

What if the probability of wining changing from 0.1 to 0.8, what is the probability that he wins less than 6 trades, assuming every month he trades 20 times.

```python
chance = np.arange(.1, .81, .05)
win_less = sp.stats.binom.cdf(6, 20, chance)

data_dict = {'win_rate':chance, 'win_less':win_less} # I am using annotation, that's why i am using pandas to locate the index
df = pd.DataFrame(data_dict)
df.plot(figsize = (10, 6))
plt.show()
```

To show the idea in a scatter plot.

```python
fig, ax = plt.subplots(figsize = (12, 7))
ax.scatter(chance, win_less)
txt = 'This point means if the winning rate is 40%,\n then the probability of winning less\n than 6 trades is 25%.'
ax.annotate(txt, xy = (df.iloc[6][0], df.iloc[6][1]),
            xytext = (.35, .5), weight = 'bold', color  =  'Tomato', size = 14,
             arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3', color = 'b'))
plt.show()
```

##  Binomial R.V. Generator

```sp.stats.binom.rvs()``` is the randome generator for binomial distribution.

```python
n = 1000 # number of trials
p = 0.3 # probability of success
bino = sp.stats.binom.rvs(n, p, size = 1000)
```

```python
txt = 'This line is the $y =p \cdot n$ \n and where highest draw should be.'
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(bino,bins= 80)
ax.axvline(p*n, color = 'Tomato', ls = '--', lw = 3)
ax.annotate(txt, xy = (p*n, h.max()*0.7),
            xytext = (p*n ,h.max()-5), weight = 'bold',
            color  =  'Tomato', size = 14,
             arrowprops = dict(arrowstyle = '->',
                               connectionstyle = 'arc3', color = 'b'))
ax.set_title('Generated Binomial R.V.s', size = 18)
plt.show()
```

According to the example, if success rate $p=.3$, total trials of $1000$.Then the counts of successes have the frequency distribution in the histogram.

##  Moments of Binomial Distribution

We can also calculate all important moments of distributions by using ```sp.stats.binom.stats(n, p, moments = 'mvsk')```. The text table library is used to format the output.

```python
n = 1000 # number of trials
p = 0.3 # probability of success
bino_stats = sp.stats.binom.stats(n, p, moments = 'mvsk') # mean, variance, skewness, kurtosis
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c'])
table.set_cols_valign([ "m", "m", 'm','m'])
table.add_rows([["mean", " variance ", ' skewness ', 'kurtosis '],
                [ bino_stats[0], bino_stats[1], bino_stats[2], bino_stats[3]]])
print(table.draw())
```

    +------+------------+------------+-----------+
    | mean |  variance  |  skewness  | kurtosis  |
    +======+============+============+===========+
    | 300  |    210     |   0.028    |  -0.001   |
    +------+------------+------------+-----------+

#  Poisson Distribution

When $n\rightarrow\infty$ and $p\rightarrow0$，binomial distribution converges to Poisson distribution, i.e. when $n$ is large and $p$ is small, we can use Poisson to approximate Binomial.

## Example

For an ordinary traders, every trade has $1/1000$ probabilty to encounter a 'Black Swan' shock, a trader sets $20$ trades per month, what is the probability that she will encounter $2$ 'Black Swan' within 5 years?

This problem can be solved by Binomial, the formulation as below

\begin{equation}
\text{Number of Trades} = 20\times 12\times 5=1200\\
P(x=2) = \binom{1200}{2}\Big(\frac{1}{1000}\Big)^2\Big(\frac{999}{1000}\Big)^{1198}
\end{equation}

Of course, we can employ Poisson PMF to approximate it, the parameter for Poisson distribution is

\begin{equation}
\lambda = np = 1200 \times \frac{1}{1000} = 1.2
\end{equation}

that means every 5 years, there is in average 1.2 times of Black Swan shock.

\begin{equation}
P(x=2)=\frac{\lambda^ke^{-\lambda}}{k!}=\frac{1.2^2e^{-1.2}}{2!}
\end{equation}

To use the PMF directly

```python
sp.special.comb(1200, 2)*(1/1000)**2*(999/1000)**1198
```

    0.21698280952603388

Or use the built-in function for Poisson ```sp.stats.poisson.pmf()```.

```python
k = 2
n = 20 * 12 * 5 # 20 times per month, and 5 years span
p = 1/1000
lambdaP = p * n # lambda in Poisson
p = sp.stats.poisson.pmf(k, lambdaP)
print('The probability of having {0} BS shock(s) in a span of 5 years is {1:.2f}%.'.format(k, p*100))
```

    The probability of having 2 BS shock(s) in a span of 5 years is 21.69%.

Suprisingly high probability of having 1 BS shock, and one BS shock could possibly wipe out the whole account. Take care, traders, the survival is pivotal!

So what's the probability of having more than $k$ times BS shock?

```python
k = 2
prob_sf = 1 - sp.stats.poisson.cdf(k, lambdaP)
prob_sf_inv = sp.stats.poisson.cdf(k, lambdaP)
print('The probability of having more than %1.0f BS shock in 5 years is %3.3f%%.' % (k, prob_sf*100)) # double % can escape formating
```

    The probability of having more than 2 BS shock in 5 years is 12.051%.

##  Poisson R.V. Generator

```python
poiss = sp.stats.poisson.rvs(lambdaP, size = 10000)
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(poiss, density = True, bins = 10)
ax.set_title('Poisson Frequency Distribution', size = 16)
plt.show()
```

##  Moments of Poisson Distribution

Again we can compute all most important moments

```python
poiss_stats = sp.stats.poisson.stats(k, moments = 'mvsk') # mean, variance, skewness, kurtosis
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c'])
table.set_cols_valign([ "m", "m", 'm','m'])
table.add_rows([["mean", " variance ", ' skewness ', 'kurtosis '],
                [ poiss_stats[0], poiss_stats[1], poiss_stats[2], poiss_stats[3]]])
print(table.draw())
```

    +------+------------+------------+-----------+
    | mean |  variance  |  skewness  | kurtosis  |
    +======+============+============+===========+
    |  2   |     2      |   0.707    |   0.500   |
    +------+------------+------------+-----------+

#  Geometric Distribution

The PMF of G-D is $f(k)=p(1-p)^k$, where  $k$ is a non-negative integer.  $p$ is the probability of success, and  $k$ is the number of failures before success.

## Example

So G-D is trying to answer 'How many times you have to fail in order to embrace the first success?'

If each trade has 1/1000 chance to encounter a BS shock, what is the prob of encounter a BS shock after trade $k$ times.

```python
k = 10
p = 1/1000

geodist = (1 - 1/1000)**10*1/1000
geodist
print('The probability of observing exact %1.0f times of safe trading before a BS shock is %3.3f.' %(k, geodist))
```

    The probability of observing exact 10 times of safe trading before a BS shock is 0.001.

Or use built-in ```sp.stats.geom.pmf()```

```python
sp.stats.geom.pmf(10, 1/1000)
```

    0.000991035916125874

```python
sp.stats.geom.cdf(k,p)
print('The probability of observing %1.0f or fewer than %1.0f times of safe trading before a BS shock is %3.3f.'%(k,k,geodist))
```

    The probability of observing 10 or fewer than 10 times of safe trading before a BS shock is 0.001.

##  G-D Moments and Generator

The moments and randome generator are as following

```python
mean, var, skew, kurt = sp.stats.geom.stats(p, moments='mvsk')
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c', 'c', 'c'])
table.set_cols_valign([ "m", "m", 'm','m', 'm', 'm'])
table.add_rows([['p','k',"mean", " variance ", ' skewness ', ' kurtosis '],
                [ p, k, mean, var, skew, kurt]])
print(table.draw())
```

    +-------+----+------+------------+------------+------------+
    |   p   | k  | mean |  variance  |  skewness  |  kurtosis  |
    +=======+====+======+============+============+============+
    | 0.001 | 10 | 1000 |   999000   |   2.000    |   6.000    |
    +-------+----+------+------------+------------+------------+

```python
geomet = sp.stats.geom.rvs(p, size = 10000)
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(geomet, density = True, bins = 30)
ax.set_title('Geometric Frequency Distribution', size = 16)
plt.show()
```

#  Hypergeometric Distribution

The main difference between hypergeometric and binomial is that the former sampling is not independent of each other, i.e. the  sampling is without replacement.

The formula is
\begin{equation}
f(x) =\frac{{K\choose x} {M-K \choose N-x}}{{M\choose N}}
\end{equation}

##  Example

There is 100 candies in an urn, 20 are red, 80 are blue, if we take 5 of them out from it. What is the prob of having exact 4 red candies?

Solution:

\begin{equation}
\frac{{20\choose4}{80\choose1}}{{100\choose5}}
\end{equation}

To solve it:

```python
C = sp.special.comb(20, 4)*sp.special.comb(80, 1) /sp.special.comb(100, 5)
print('The prob of have 4 red candies by taking 5 out is %1.6f%%.'% (C*100))
```

    The prob of have 4 red candies by taking 5 out is 0.514826%.

Or with built-in function ```sp.stats.hypergeom.pmf()```

```python
# pmf(x, M, N, n, loc=0)
hgeo = sp.stats.hypergeom.pmf(4, 100, 20, 5,loc = 0) # the arg order the same as MATLAB, not recommended
hgeo
```

    0.005148263616599428

What is the probability that at most $4$ red candies are taken? i.e. The sum-up of  probabilities of drawing $1$, $2$, $3$, and $4$ candies.

```python
hgeo_cdf = sp.stats.hypergeom.cdf(4, 100, 20, 5,loc = 0) # the arg order the same as MATLAB, not recommended
print('The prob of have at most 4 red candies by taking 5 out is %1.6f%%.' %(hgeo_cdf*100))
```

    The prob of have at most 4 red candies by taking 5 out is 99.979407%.

```python
hgeo_rv = sp.stats.hypergeom.rvs(100, 20, 5, size = 10000)
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(hgeo_rv, density = True)
ax.set_title('Geometric Frequency Distribution', size = 16)
s = '''    It can be interpreted as: if 100 candies in the urn,
    20 are red, take 5 out of 100.

    The chance of getting from 1 to 5 red candies,
    is shown in the chart.

    As we can see it is nearly impossible to get 4 or 5 candies.
    But getting 1 red candy is the most possible outcome.'''

ax.text(1.6, .5, s, fontsize=14, color ='red')
plt.show()
```

```python
mean, var, skew, kurt = sp.stats.hypergeom.stats(100, 20, 5, moments='mvsk')
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c', 'c', 'c'])
table.set_cols_valign([ "m", "m", 'm','m', 'm', 'm'])
table.add_rows([['M','k',"mean", " variance ", ' skewness ', ' kurtosis '],
                [ 100, 20, mean, var, skew, kurt]])
print(table.draw())
```

    +-----+----+------+------------+------------+------------+
    |  M  | k  | mean |  variance  |  skewness  |  kurtosis  |
    +=====+====+======+============+============+============+
    | 100 | 20 |  1   |   0.768    |   0.629    |   -0.010   |
    +-----+----+------+------------+------------+------------+

#  Continous Uniform Distribution

The PDF of Uniform Distribution is

\begin{equation}
f(x)=\frac{1}{b-a}
\end{equation}

And  its r.v. generator is one of most frequently used function in NumPy: ```np.random.rand()```

```python
unif = np.random.rand(10000)
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(unif, density = True, bins = 30)
ax.set_title('Continous Uniform Frequency Distribution', size = 16)
plt.show()
```

##  CDF and PDF of Continous Uniform Distribution

```python
# pdf(x, loc=0, scale=1)
# cdf(x, loc=0, scale=1)
x = np.linspace(-.2, 1.2, 100)
unif_pdf = sp.stats.uniform.pdf(x)
unif_cdf = sp.stats.uniform.cdf(x)
fig, ax = plt.subplots(nrows = 1, ncols = 2, figsize = (17, 7))
ax[0].plot(x,unif_pdf, lw = 4, label = 'PDF of Continouse U-D')
ax[0].set_xlim([-.1, 1.1])
ax[0].set_ylim([0, 2])
ax[0].legend(fontsize = 16)

ax[1].plot(x,unif_cdf, lw  = 4, label = 'CDF of Continouse U-D')
ax[1].set_xlim([-.2, 1.2])
ax[1].set_ylim([0, 2])
ax[1].legend(fontsize = 16)
plt.show()
```

```python
mean, var, skew, kurt = sp.stats.uniform.stats(moments='mvsk')
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c'])
table.set_cols_valign([ "m", "m", 'm','m'])
table.add_rows([["mean", " variance ", ' skewness ', ' kurtosis '],
                [ mean, var, skew, kurt]])
print(table.draw())
```

    +-------+------------+------------+------------+
    | mean  |  variance  |  skewness  |  kurtosis  |
    +=======+============+============+============+
    | 0.500 |   0.083    |     0      |   -1.200   |
    +-------+------------+------------+------------+

#  Normal Distribution

The most convenient method of creating normal distribution plot is to use ```sp.stats.norm.pdf()```.

The PDF function single normal distribution is

$$
f(x)= \frac{1}{\sigma \sqrt{2 \pi}} e^{-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^{2}}
$$

```python
mu = 2
sigma = 1
x = np.arange(-2, 6, 0.1)
norm_pdf = sp.stats.norm.pdf(x, mu, sigma)
norm_cdf = sp.stats.norm.cdf(x, mu, sigma)

fig, ax = plt.subplots(nrows = 1, ncols = 2, figsize = (17, 7))
ax[0].plot(x,norm_pdf, lw = 4, label = 'PDF of Normal Distribution', ls = '--')
ax[0].legend(fontsize = 16, loc = 'lower left', framealpha=0.2)

ax[1].plot(x,norm_cdf, lw  = 4, label = 'CDF of Normal Distribution')
ax[1].legend(fontsize = 16,fancybox=True, framealpha=0.5)
plt.show()
```

##  Inverse Normal CDF

The inverse normal CDF is commonly used in calculating $p$-value, the plot below is useful to understand the idea.

```python
norm_95_r = sp.stats.norm.ppf(.975) # ppf mean point percentage function, actually inverse CDF
norm_95_l = sp.stats.norm.ppf(.025)

x = np.linspace(-5, 5, 200)
y = sp.stats.norm.pdf(x)

xl = np.linspace(-5, norm_95_l, 100)
yl = sp.stats.norm.pdf(xl)
xr = np.linspace(norm_95_r, 5, 100)
yr = sp.stats.norm.pdf(xr)

fig, ax = plt.subplots(figsize = (17, 7))
ax.plot(x,y, lw = 4, label = 'PDF of Normal Distribution', ls = '-', color = 'orange')
ax.set_ylim([0, .45])

ax.fill_between(x, y, 0, alpha=0.1, color = 'blue')
ax.fill_between(xl,yl, 0, alpha=0.6, color = 'blue')
ax.fill_between(xr,yr, 0, alpha=0.6, color = 'blue')

ax.text(-.2, 0.15, '95%', fontsize = 20)
ax.text(-2.3, 0.015, '2.5%', fontsize = 12, color = 'white')
ax.text(2.01, 0.015, '2.5%', fontsize = 12, color = 'white')

ax.annotate('±%.4f' %norm_95_r, xy = (norm_95_r, 0), xytext = (-.4, .05), weight = 'bold', color  =  'r',
             arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3', color = 'b'), fontsize = 16)
ax.annotate('±%.4f' %norm_95_r, xy = (norm_95_l, 0), xytext = (-.4, .05), weight = 'bold', color  =  'r',
             arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3', color = 'b'), fontsize = 16)

ax.set_title('Normal Distribution And 2.5% Shaded Area', size = 20)
plt.show()
```

##  Normal R.V. Generator

To generate a normal distribution histogram

```python
# rvs(loc=0, scale=1, size=1, random_state=None)
norm_rv = sp.stats.norm.rvs(mu, sigma, size = 5000)
fig, ax = plt.subplots(figsize = (12, 7))
h, bins, patches = ax.hist(norm_rv, density = True, bins = 50)
ax.set_title('Normal Frequency Distribution', size = 16)
plt.show()
```

```python
mean, var, skew, kurt = sp.stats.norm.stats(mu, sigma, moments='mvsk')
table = Texttable()
table.set_cols_align([ "c", "c", 'c','c'])
table.set_cols_valign([ "m", "m", 'm','m'])
table.add_rows([["mean", " variance ", ' skewness ', ' kurtosis '],
                [ mean, var, skew, kurt]])
print(table.draw())
```

    +------+------------+------------+------------+
    | mean |  variance  |  skewness  |  kurtosis  |
    +======+============+============+============+
    |  2   |     1      |     0      |     0      |
    +------+------------+------------+------------+

##  Bivariate Normal Distribution

The multivariate normal distribution density function is
\begin{equation}
f_\boldsymbol{X}(x_1,...,x_2)=\frac{1}{\sqrt{(2\pi)^k|\Sigma|}}\exp{\Big(-\frac{(x-\mu)^T\Sigma^{-1}(x-\mu)}{2}\Big)}
\end{equation}

###  1st Method of Formulation

```python
%matplotlib inline
mu_x = 0
sigma_x = 2

mu_y = 0
sigma_y = 2

#Create grid and multivariate normal
x = np.linspace(-10,10,500)
y = np.linspace(-10,10,500)
X, Y = np.meshgrid(x,y)
pos = np.empty(X.shape + (2,))
pos[:, :, 0] = X; pos[:, :, 1] = Y # more technical than next one
norm = sp.stats.multivariate_normal([mu_x, mu_y], [[sigma_x, 0], [0, sigma_y]]) # frozen

#Make a 3D plot
fig = plt.figure(figsize = (10, 6))
ax = fig.gca(projection='3d')
ax.plot_surface(X, Y, norm.pdf(pos),cmap='viridis',linewidth=0)
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')

plt.show()
```

###  2st Method of Formulation

```python
#Parameters to set
mu_x = 0
sigma_x = 7

mu_y = 0
sigma_y = 15

x = np.linspace(-10,10,500)
y = np.linspace(-10,10,500)
X,Y = np.meshgrid(x,y)

pos = np.array([X.flatten(),Y.flatten()]).T  # more intuitive than last one

rv = sp.stats.multivariate_normal([mu_x, mu_y], [[sigma_x, 0], [0, sigma_y]])

fig = plt.figure(figsize=(10,10))
ax0 = fig.add_subplot(111)
ax0.contourf(X, Y, rv.pdf(pos).reshape(500,500),cmap='viridis')

plt.show()
```

#  Beta Distribution

The PDF of Beta distribution is

\begin{equation}
f(x, a, b)=\frac{\Gamma(a+b) x^{a-1}(1-x)^{b-1}}{\Gamma(a) \Gamma(b)}
\end{equation}

where $0\leq x \leq 1$ and $a>0$, $b>0$, $\Gamma$ is the Gamma function.

Beta distribution is a natural good choice for priors in Bayesian econometrics since its range is bounded in unit.

```python
params[0][0][1]
```

    0.5

```python
x = np.linspace(0, 1, 100)

fig = plt.figure(figsize=(9, 9))
ax = fig.add_subplot(111)

params = np.array([[[.5,.5]],
                  [[5,1]],
                  [[1,5]],
                  [[2,2]],
                  [[2,3]],
                  [[3,2]],
                  [[1,1]]])
for i in range(params.shape[0]):
    beta_pdf = sp.stats.beta.pdf(x, params[i][0][0], params[i][0][1])
    ax.plot(x, beta_pdf, lw = 3, label = '$a = %.1f, b =  %.1f$' % (params[i][0][0], params[i][0][1]))
    ax.legend()

ax.axis([0, 1, 0, 3])
```

```python
fig = plt.figure(figsize=(9, 9))
ax = fig.add_subplot(111)

x = np.linspace(0,1,100)
params = np.array([[[.5,.5]],
                  [[5,1]],
                  [[1,5]],
                  [[2,2]],
                  [[2,3]],
                  [[3,2]],
                  [[1,1]]])
for i in range(params.shape[0]):
    beta_cdf = sp.stats.beta.cdf(x, params[i][0][0], params[i][0][1])
    ax.plot(x, beta_cdf, lw = 3, label = '$a = %.1f, b =  %.1f$' % (params[i][0][0], params[i][0][1]))
    ax.legend()

ax.axis([0, 1, 0, 1])
```

#  $\chi^2$ Distribution

$\chi^2$ distribution is closely connected with normal distributions, if $z$ has the standard normal distribution, then $z^2$ has the $\chi^2$ distribution with $d.f.=1$. And futher,if

\begin{equation}
z_1, z_2, ..., z_k \sim i.i.d. N(0, 1)
\end{equation}

Then summation has a $\chi^2$ distribution of $d.f. = k$:

\begin{equation}
\sum_{i=0}^k z_i^2 \sim \chi^2(k)
\end{equation}

##  $\chi^2$ PDF and CDF

```python
k = 1
x = np.linspace(0, 5, 100)
chi_pdf = sp.stats.chi2.pdf(x, k)

fig, ax = plt.subplots(figsize = (12, 7))
ax.plot(x, chi_pdf, lw = 3, c = 'r', label = '$\chi^2$ distribution with d.f. = 1')
ax.legend(fontsize = 18)
plt.show()
```

```python
fig, ax = plt.subplots(figsize = (12, 7))
for i in range(1, 6):
    x = np.linspace(0, 5, 100)
    chi_pdf = sp.stats.chi2.pdf(x, i)
    ax.plot(x, chi_pdf, lw = 3, label = '$\chi^2$ distribution with d.f. = %.0d'%i)
    ax.legend(fontsize = 12)
    ax.axis([0, 5, 0, 1])
plt.show()
```

#  F Distribution

If $U_1$ has a $\chi^2$ distribution with $\nu_1$ d.f. and $U_2$ has a $\chi^2$ distribution with $\nu_2$ d.f., then

\begin{equation}
\frac{U_1/\nu_1}{U_2/\nu_2}\sim F(\nu_1, \nu_2)
\end{equation}

We are using $F$ distribution for ratios of variances.

```python
x = np.linspace(0, 5, 100)
fig, ax = plt.subplots(figsize = (12, 7))

df1 = 10
df2 = 5
f_pdf = sp.stats.f.pdf(x, dfn = df1, dfd = df2)
ax.plot(x, f_pdf, lw =3, label = '$df_1 = %.d, df_2 = %.d$' %(df1, df2))

df1 = 5
df2 = 10
f_pdf = sp.stats.f.pdf(x, dfn = df1, dfd = df2)
ax.plot(x, f_pdf, lw =3, label = '$df_1 = %.d, df_2 = %.d$ '%(df1, df2))

df1 = 8
df2 = 15
f_pdf = sp.stats.f.pdf(x, dfn = df1, dfd = df2)
ax.plot(x, f_pdf, lw =3, color = 'red', label = '$df_1 = %.d, df_2 = %.d$' %(df1, df2))

df1 = 15
df2 = 8
f_pdf = sp.stats.f.pdf(x, dfn = df1, dfd = df2)
ax.plot(x, f_pdf, lw =3, label = '$df_1 = %.d, df_2 = %.d$ '%(df1, df2))

ax.legend(fontsize = 15)
plt.show()
```

```python

```

Chapter 4 - Most Important Discrete and Continuous Distributions.ipynb


```python
pip install control
```

    Collecting control
      Downloading control-0.9.1.tar.gz (357 kB)
         |████████████████████████████████| 357 kB 29.1 MB/s
    Requirement already satisfied: numpy in /usr/local/lib/python3.7/dist-packages (from control) (1.19.5)
    Requirement already satisfied: scipy in /usr/local/lib/python3.7/dist-packages (from control) (1.4.1)
    Requirement already satisfied: matplotlib in /usr/local/lib/python3.7/dist-packages (from control) (3.2.2)
    Requirement already satisfied: python-dateutil>=2.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->control) (2.8.2)
    Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->control) (3.0.6)
    Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->control) (1.3.2)
    Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.7/dist-packages (from matplotlib->control) (0.11.0)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.1->matplotlib->control) (1.15.0)
    Building wheels for collected packages: control
      Building wheel for control (setup.py) ... done
      Created wheel for control: filename=control-0.9.1-py2.py3-none-any.whl size=364705 sha256=468330210157ac65af85fa8da39c7faee654cb3271b4fc353191326f77a9843a
      Stored in directory: /root/.cache/pip/wheels/b1/50/22/8005107713828997d1af664fbd629c7f935a1b87fc2f4ba6c1
    Successfully built control
    Installing collected packages: control
    Successfully installed control-0.9.1

```python
#circuito de 3ª ordem, saida no capacitor
#****parte 1: o sistema*****
#importa a biblioteca simbólica
import sympy
#define as variáveis
s,vc1,vout,il,iR1,iR2,vin,R1,R2,C1,C2,L = sympy.symbols(
    's,vc1,vout,il,iR1,iR2,vin,R1,R2,C1,C2,L')
#valor dos componentes
R1=1; R2=1; C1=1; C2=1; L=1 #comente essa linha para solução geral!
#equações
eq1 =  vc1 + R1*iR1 - vin
eq2 =  vout-vc1 + L*s*il + R2*iR2
eq3 = -C1*s*vc1 + iR1 - iR2
eq4 =  iR2 - il
eq5 =  C2*s*vout - il
#resolve o sistema
solucao=sympy.solve([eq1, eq2,eq3,eq4,eq5],(vout,vc1,il,iR1,iR2))
#mostra a solução
print('\n Vout=')
sympy.pprint(solucao[vout])

#****parte 2: O controle****
#calcula a saída sobre a entrada simbólica
tf=sympy.simplify(solucao[vout]/vin)
print('\n Vout/Vin=',tf)
#importa as demais bibliotecas
import control as ctl
import matplotlib.pyplot as plt
import numpy as np

#transforma Vout/Vin simbólica em FT
s = ctl.TransferFunction.s
P_s=eval(str(tf))
print("FT=",P_s)
#calcula o ganho crítico para oscilação
out_st_magn=ctl.stability_margins(P_s)
K_cr=np.round(out_st_magn[0]*1000)/1000
W_cr=np.round(out_st_magn[3]*1000)/1000
T_cr=2*np.pi/W_cr
print('K de oscilação ',K_cr)
print('T de oscilação ',T_cr)

# controlador PID Ziegler-Nichols da frequencia
Ki=1.2*K_cr/T_cr;Kp=K_cr*0.6;Kd=0.075*K_cr*T_cr;
print('Kp=' f'{Kp:6.2f}, Ki=' f'{Ki:6.2f}, Kd=' f'{Kd:6.2f}')
C_s=(Ki+Kp*s+Kd*s**2)/(s)
# calcula FT em malha fechada
H_s=1
G_s=ctl.series(C_s, P_s);
G1_s=ctl.feedback(G_s, H_s, sign=-1);
#calcula a resposta ao degrau do PID sintonizado
Tsim=10
T, yout = ctl.step_response(G1_s, Tsim)

plt.rcParams.update({'font.size': 14})
plt.figure()
plt.plot(T,yout,'k-',label="resposta em malha fechada");plt.grid()

T2=np.linspace(-0.2,Tsim,1000)
degrau=np.ones_like(T2)
degrau[T2<0]=0;
plt.plot(T2,degrau,'r-',label="entrada: degrau unitário")
plt.legend()
```


Massimo Nocentini

October 22, 2016: Horner method, binomial anti-difference

October 9, 2016: computing sums, Stirling numbers, thms coding

October 7, 2016: operators and powers theory

Abstract

In this document we collect notes and exercises for the first chapter of the course.

```python
from sympy import *
from sympy.abc import n, i, N, x, k, y

init_printing()
```

```python
%run src/commons.py
```

---

```python
delta, antidifference, I, E, f = symbols(r'\Delta \Delta^{-1} I E f', cls=Function) # combinators
```

```python
I_eval_rule = define(I(x), x) # identity
E_eval_rule = define(E(x),x+1) # forward shift

I_eval_rule, E_eval_rule
```

```python
delta_eval_rule = define(delta(f(x)), f(x+1)-f(x),) # forward difference def
delta_pow_rule = define(delta(f(x))**k, delta(delta(f(x))**(k-1)),) # repeated applications of differentiation

delta_eval_rule, delta_pow_rule
```

```python
delta_EI_conv = define(delta(x), E(x)-I(x)) # conversion via combinators

delta_EI_conv
```

```python
def rewrite(eq, rule, **kwds):
    return eq.replace(query=rule.lhs, value=rule.rhs, **kwds)
```

```python
s = Wild('s')
rewrite(delta_eval_rule, define(f, Lambda([s], ff(s,n))))
```

```python
f_wild = WildFunction('f')
D=define(delta(f_wild**k), delta(delta(f_wild**(k-1))))
rewrite(delta_pow_rule, D)
```

```python
class ForwardDifference(Function):

    def _latex(self, *_):
        if len(self.args) < 2:
            func, args = self.args[0].func, self.args[0].args
            v = args[0]
        else:
            func, args = self.args[0].func, self.args[0].args
            v = self.args[1]

        D = Function(r'\Delta_{{{}}}'.format(latex(v)))
        expr = D(func(*args))
        return latex(expr)

    def doit(self):

        if len(self.args) < 2:
            func, args = self.args[0].func, self.args[0].args
            v = args[0]
        else:
            func, args = self.args[0].func, self.args[0].args
            v = self.args[1]

        return func(*map(lambda a: a.subs({v:v+1}, simulataneous=True),args)) - func(*args)

    def _e2val_power(self, k):
        if k.is_Number:
            return ForwardDifference(Pow(ForwardDifference(self.args), k-1, evaluate=False))
        else:
            return super()._eval_power(k)
```

# know the difference
Let $\{y_{n}\}_{n\in\mathbb{N}}$ be a sequence, where $y_{n}=f(n)$ for some function $f$. Assume that each coefficient $y_{n}$ *is not known*; on the contrary, assume that there exists a *known* sequence $\{g_{n}\}_{n\in\mathbb{N}}$ which satisfies:
$$
\begin{equation}
\Delta y_{n} = y_{n+1}-y_{n}=g_{n}
\end{equation}
$$
By finite summation on both sides:
$$
\sum_{n=n_{0}}^{N-1}{\Delta y_{n}} = \sum_{n=n_{0}}^{N-1}{g_{n}}
$$
many terms in the lhs disappear, so:
$$
y_{N}-y_{n_{0}} = \sum_{n=n_{0}}^{N-1}{g_{n}}
$$
therefore, if the initial term $y_{n_{0}}$ is given, we can compute any term $y_{N}$ by:
$$
y_{N} = y_{n_{0}} + \sum_{n=n_{0}}^{N-1}{g_{n}}
$$
since each term $g_{n}$ is known by hypothesis and the summation can be done.

## a little generalization

Consider an additional known sequence $\{p_{n}\}_{n\in\mathbb{N}}$ and we're required to find a solution for  equation: $ y_{n+1} = p_{n}y_{n} + g_{n} $. So define an helper sequence $\{P_{n}\}_{n\in\mathbb{N}}$ such that $P_{n_{0}}=1$ and $P_{n}=p_{n-1}P_{n-1}$, therefore $P_{n}=\prod_{k=n_{0}}^{n-1}{p_{k}}$ holds by induction. Now study the following:

$$
    \frac{y_{n+1}}{P_{n+1}} = \frac{p_{n}y_{n}}{P_{n+1}} + \frac{g_{n}}{P_{n+1}}
    = \frac{y_{n}}{P_{n}} + \frac{g_{n}}{P_{n+1}}
$$

calling $z_{n}=\frac{y_{n}}{P_{n}}$ and $q_{n}=\frac{g_{n}}{P_{n+1}}$, it yields: $z_{n+1} = z_{n}+q_{n}$, with initial condition $z_{n_{0}} = \frac{y_{n_{0}}}{P_{n_{0}}} = y_{n_{0}}$. So we've a recurrence in a simpler form, whose solution is a sequence $\{z_{n}\}_{n\in\mathbb{N}}$ such that $z_{n} = z_{n_{0}} + \sum_{i=n_{0}}^{n-1}{q_{i}}$. By backward substitution:

$$
\begin{split}
    \frac{y_{n}}{P_{n}} &= y_{n_{0}} + \sum_{i=n_{0}}^{n-1}{\frac{g_{i}}{P_{i+1}}} \\
    y_{n} &= P_{n}y_{n_{0}} + \sum_{i=n_{0}}^{n-1}{\frac{P_{n}g_{i}}{P_{i+1}}} \\
    y_{n} &= \left(\prod_{k=n_{0}}^{n-1}{p_{k}}\right)y_{n_{0}} +
        \sum_{i=n_{0}}^{n-1}{\left(\prod_{k=i+1}^{n-1}{p_{k}}\right)g_{i}} \\
\end{split}
$$

which is the closed form for coefficients of solution sequence $\{y_{n}\}_{n\in\mathbb{N}}$.

### Horner method

Let $p\in\prod_{n}$ be a polynomial over coefficients $\{b_{n}\in\mathbb{C}\}_{n\in\mathbb{N}}$, defined as $p(x)=\sum_{i=0}^{n}{b_{i}x^{n-i}}$. Define the difference equation $y_{i} = xy_{i-1} + b_{i}$, for $i\in\{1,\ldots,n\}$, with initial condition $y_{0}=b_{0}$; therefore, $y_{n}=p(x)$ holds.

In order to see this, recognize that we have a recurrence of the last form where $p_{i}=x$ and $g_{i}=b_{i+1}$ forall $i$ in the domain, therefore its solution has the generic coefficient $y_{n}$ which satisfies:

$$
    y_{n} = \left(\prod_{k=n_{0}}^{n-1}{x}\right)b_{0} +
        \sum_{i=n_{0}}^{n-1}{\left(\prod_{k=i+1}^{n-1}{x}\right)b_{i+1}}
        = x^{n}b_{0} + \sum_{i=0}^{n-1}{x^{n-1-(i+1)+1}b_{i+1}} =
        = x^{n}b_{0} + \sum_{i=1}^{n}{x^{n-i}b_{i}} = \sum_{i=0}^{n}{x^{n-i}b_{i}} = p(x)
$$

as required.

# $\Delta$ operator relations

On the other hand, assume that no initial condition $y_{n_{0}}$ is given, we obtain $y_{n}$ on the lhs by application of the *anti-difference* operator $\Delta^{-1}$ on the left in both members $\Delta y_{n}=g_{n}$ so
$y_{n} = \Delta^{-1}g_{n}$. Use this identity as rewriting rule and apply it to the former equation, obtaining $\Delta\Delta^{-1}g_{n}=g_{n}$, therefore the relation $\Delta\Delta^{-1}=I$ on operators holds.

Moreover, let $\{w_{n}\}_{n\in\mathbb{N}}$ be a constant sequence, so we can augment:
$$
y_{n} = \Delta^{-1}g_{n} + w_{n} = \Delta^{-1}\Delta y_{n} + w_{n}
$$
because $\Delta w_{n}=0$, therefore the relation $\Delta^{-1}\Delta = I - K$, where $K$ is the constant operator, holds.

# computing sums via $\Delta^{-1}$

Let $g_{n}=\Delta y_{n}$ and assume to *not* have a closed formula in $n$ for coefficients $y_{n}$, but to *know* that $y_{n}=\Delta^{-1}g_{n}$ holds. Apply summation on both members and manipulating on the rhs:
$$
\sum_{n=n_{0}}^{N-1}{g_{n}} = \sum_{n=n_{0}}^{N-1}{\Delta y_{n}}
= y_{N}-y_{n_{0}} =  y_{n} \big|_{n_{0}}^{N} = \Delta^{-1}g_{n} \big|_{n_{0}}^{N} = \Delta^{-1}g_{n} \big|_{n=N} - \Delta^{-1}g_{n} \big|_{n=n_{0}}
$$
therefore, if we have an *unknown* sequence $\{g_{n}\}_{n\in\mathbb{N}}$, which has a closed-form of
$\Delta^{-1}g_{n}$ as a term which support *substitution* of symbol $n$, so the sum $\sum_{n=n_{0}}^{N-1}{g_{n}}$ can be easily computed by a difference, as done in the fundamental theorem of calculus.

```python
g = IndexedBase('g')
n = IndexedBase('n')
f = Function('f')

anti_difference = Function('\Delta^{-1}')

def accept_replacing(thm_ctor):

    def replacing(subs=lambda *args: {}, **kwds):

        weq, variables = thm_ctor(**kwds)
        mapping = subs(*variables) if callable(subs) else subs
        for k,v in mapping.items():
            weq = weq.replace(k, v, simultaneous=True)

        return weq, [mapping.get(v, v) for v in variables]

    return replacing

@accept_replacing
def summation_antidifference_thm():

    (n, sup), inf = symbols('n N'), IndexedBase('n')[0]
    eq = Eq(Sum(g[n], (n, inf, sup-1)),
            Subs(anti_difference(g[n]), n, sup) -
            Subs(anti_difference(g[n]), n, inf))

    return eq, (g, n, inf, sup)

@accept_replacing
def antidifference_of_ff_thm():
    (n, i), w = symbols('n i'), IndexedBase('w')
    eq = Eq(anti_difference(ff(n, i)), ff(n, i+1)/(i+1)+w[n])
    return eq, (x, n, w)

@accept_replacing
def antidifference_of_binomial_thm():
    (n, k), w = symbols('n k'), IndexedBase('w')
    eq = Eq(anti_difference(binomial(n, k)), binomial(n, k+1)+w[n])
    return eq, (x, n, w)

@accept_replacing
def constant_sequence_thm():
    variables = w, i, j = IndexedBase('w'), *symbols('i j')
    eq = Eq(w[i], w[j])
    return eq, (w, i, j)

def doit(thm, lhs=True, rhs=True):
    eq, *variables = thm
    return Eq(eq.lhs.doit() if lhs else eq.lhs, eq.rhs.doit() if rhs else eq.rhs), variables

def rewrite(thm, rule, include_rule_vars=False):

    eq, *rest = thm
    try:
        rw, *others = rule # so, `rule` can be a thm too
    except:
        rw, *others = rule, []

    augmented = []
    augmented.extend(*rest)
    if include_rule_vars: augmented.extend(*others)
    return eq.replace(rw.lhs, rw.rhs, simultaneous=True), augmented

```

```python
thm = eq, (g, n, inf, sup) = summation_antidifference_thm()
thm
```

## $(x)_{i}$ application

```python
local_thm = Eq(g[n], ff(n,i)), (g, n, i)
local_thm
```

```python
inst_thm = rewrite(thm, local_thm)
inst_thm
```

```python
ant_ff_thm = antidifference_of_ff_thm(subs={})
ant_ff_thm
```

```python
ready_thm = eq, *_ = rewrite(inst_thm, ant_ff_thm)
ready_thm
```

```python
done_thm = doit(ready_thm, lhs=False)
done_thm
```

## ${{n}\choose{k}}$ application

```python
local_thm = Eq(g[n], binomial(n,k)), (g, n, k)
local_thm
```

```python
inst_thm = rewrite(thm, local_thm)
inst_thm
```

```python
ant_binomial_thm = antidifference_of_binomial_thm(subs={})
ant_binomial_thm
```

Previous thm holds by the following argument.

$$
\begin{split}
    \Delta{{x}\choose{j}} = {{x+1}\choose{j}} - {{x}\choose{j}} &= \frac{(x+1)_{j}}{(j)_{j}}-\frac{(x)_{j}}{(j)_{j}}\\
    &= \frac{ (x+1)x\cdots(x-j+2) -x\cdots(x-j+2)(x-j+1) }{(j)_{j}}\\
    &= \frac{ x\cdots(x-j+2)(x+1 -(x-j+1)) }{(j)_{j}}\\
    &= \frac{ x\cdots(x-j+2) }{(j-1)!} =  \frac{ (x)_{(j-1)} }{(j-1)_{(j-1)}} =  {{x}\choose{j-1}}\\
\end{split}
$$

therefore, to find $\Delta^{-1}{{x}\choose{j}}$ we are required to provide a term $t_{x}$ such that application of $\Delta$ to it yields ${{x}\choose{j}}$. So choose $t_{x}={{x}\choose{j+1}}$, according to above identity.

```python
ready_thm = eq, *_ = rewrite(inst_thm, ant_ff_thm)
ready_thm
```

```python
done_thm = doit(ready_thm, lhs=False)
done_thm
```

```python
ff(x+1,i)-ff(x,i)
```

# powers

## in $\mathbb{R}$

In $\mathbb{R}$ the $n$-th power of the symbol $x$ satisfies:
$$
\begin{split}
x^{0}&=1 \\
x \neq0 &\rightarrow x^{-n}=\frac{1}{x^{n}}\\
\frac{\partial x^{n}}{\partial{x}} &= n x^{n-1} \\
\frac{\partial^{-1} x^{n}}{\partial{x}} = \int x^{n}\partial x &= \frac{x^{n+1}}{n+1}+c
\end{split}
$$
for some $c\in\mathbb{R}$. In $\mathbb{N}$ its counterpart is the *falling factorial* function in the variable $x$ defined as:

$$(x)_{n} = \underbrace{x(x-1)(x-2)\cdots(x-n+1)}_{n\text{ terms}}$$

## in $\mathbb{N}$

We apply operator $\Delta$ to derive an identity about forward differences of $(x)_{n}$:
$$
\begin{split}
\Delta (x)_{n} &= (x+1)_{n} - (x)_{n} \\
&= (x+1)x(x-1)\cdots(x-n+2) - x(x-1)\cdots(x-n+2)(x-n+1) \\
&= (x)_{(n-1)}(x+1 -(x-n+1)) \\
&=n(x)_{(n-1)}
\end{split}
$$
Previous identity allows us to recover the *anti-difference* of $(x)_{n}$: it requires to find a sequence $\{g_{n}\}_{n\in\mathbb{N}}$ such that $\Delta g_{n} = (x)_{n}$, namely $$g_{n}=\frac{(x)_{(n+1)}}{n+1}+w_{n}=\Delta^{-1}(x)_{n}$$ where $\{w_{n}\}_{n\in\mathbb{N}}$ is a constant sequence.

Moreover, in order to provide corresponding identities for the left ones, we reason according to:
$$
(x)_{m+n} = \underbrace{x(x-1)\cdots(x-m+1)}_{(x)_{m}}\underbrace{(x-m)(x-m-1)\cdots(x-m-n+1)}_{(x-m)_{n}}
$$
substitution $m=0$ yields $(x)_{n}=(x)_{0}(x)_{n}$ therefore $(x)_{0}=1$. On the other hand, substitution $m=-n$ yields $(x)_{0}=(x)_{-n}(x+n)_{n}$. So:
$$
(x+n)_{n} \neq 0 \rightarrow (x)_{-n} = \frac{1}{(x+n)_{n}} = \frac{1}{(x+n)(x+n-1)\cdots(x+1)}
$$
requiring $x\not\in\{-1, -2, \ldots, -n\}$.

## properties

- $(x)_{n}$ is monic polynomial of degree $n$ with roots $\{0, 1, \ldots, n-1\}$
- $\Delta(x)_{n}\in\Pi_{n-1}$
- $\Delta^{-1}(x)_{n}\in\Pi_{n+1}$
- $k < j \rightarrow (k)_{j} = k(k-1)\cdots(k-k)\cdots(k-j+1)=0$
- $(k)_{k} = k(k-1)\cdots(k-(k-1)+1)(k-k+1)=k!$
- $(k)_{k}=(k)_{(k-1)}$

and, finally:

$$\frac{(k)_{j}}{(j)_{j}} = \frac{k(k-1)\cdots(k-j+1)}{j!}=\frac{k!}{j!(k-j)!}={{k}\choose{j}}$$

## $\mathbb{R} \leftarrow \mathbb{N}$, via Stirling numbers of the *second* type

```python
from sympy.functions.combinatorial.numbers import stirling
```

The following identity links the two kinds of powers:
$$
x^{n} = \sum_{i=1}^{n}{\mathcal{S}_{n,i} (x)_{i}}
$$
where coefficients $\mathcal{S}_{n,i}$ are Stirling's numbers of the second kind, defined according to the following recurrence relation $\mathcal{S}_{n+1, i} = \mathcal{S}_{n, i-1} + i\mathcal{S}_{n, i}$, for $i\in\{2,\ldots,n\}$, with initial conditions $\mathcal{S}_{n, 1} = \mathcal{S}_{n, n} = 1$.

In the following matrix we report the upper chunk of the infinite matrix generated by the recurrence relation; for the sake of clarity, according to Python indexing which is *zero*-based, we include the very first row and column, which yields $\mathcal{S}_{0, 0}=1$ and $0$ everywhere else.

```python
stirling_matrix_second_kink = Matrix(11,11,lambda i, j: stirling(i,j, kind=2, signed=False))
stirling_matrix_second_kink
```

```python
m = Mul(stirling_matrix_second_kink, Matrix(11,1,lambda i, _: ff(x, i, evaluate=False)),evaluate=False)
#.applyfunc(lambda i: i.as_poly(x).as_expr())
Eq(m, Matrix(11,1,lambda i, _: x**i), evaluate=False)
```

```python
_.lhs.doit()
```

__Proof.__ By induction on $n$.

Base case $n=1$, so $x = \mathcal{S}_{1,1}(x)_{1}=x$, which holds.

Assume the theorem true for $n$ and show for $n+1$, so:
$$
\begin{split}
    x^{n+1} = x\cdot x^{n} &= x\sum_{i=1}^{n}{\mathcal{S}_{n,i} (x)_{i}}
    = \sum_{i=1}^{n}{\mathcal{S}_{n,i} (x-i+i) (x)_{i}}
    = \sum_{i=1}^{n}{\mathcal{S}_{n,i}\left(\underbrace{(x-i) (x)_{i}}_{(x)_{(i+1)}} + i (x)_{i} \right)}\\
    &= \sum_{i=2}^{n+1}{\mathcal{S}_{n,i-1} (x)_{i}} + \sum_{i=1}^{n}{i \mathcal{S}_{n,i} (x)_{i}}
    = \underbrace{\mathcal{S}_{n,1}}_{\mathcal{S}_{n+1,1}} (x)_{1} +
        \sum_{i=2}^{n}{\underbrace{\left(\mathcal{S}_{n,i-1}+i\mathcal{S}_{n,i}\right)}_{\mathcal{S}_{n+1,i}} (x)_{i}} +
        \underbrace{\mathcal{S}_{n,n}}_{\mathcal{S}_{n+1,n+1}} (x)_{n+1}
    = \sum_{i=1}^{n+1}{\mathcal{S}_{n+1,i} (x)_{i}}
\end{split}
$$
as required. $\blacksquare$

The just proved identity allows us to easily compute summations of the form $\sum_{x=n_{0}}^{N-1}{x^{k}}$, for some $k\in\mathbb{N}$. One way to do this is to use the result seen some cells above, where it is required to know $\Delta^{-1}x^{k}$:
$$
    \sum_{x=n_{0}}^{N-1}{x^{k}} = \Delta^{-1}x^{k} \big|_{x=N} - \Delta^{-1}x^{k} \big|_{x=n_{0}}
$$
but such anti-difference is unknown. Therefore put the last identity in:
$$
\sum_{x=n_{0}}^{N-1}{x^{k}} = \sum_{x=n_{0}}^{N-1}{\sum_{i=1}^{k}{\mathcal{S}_{k,i} (x)_{i}}} =
\sum_{i=1}^{k}{\mathcal{S}_{k,i}\sum_{x=n_{0}}^{N-1}{ (x)_{i}}} = \sum_{i=1}^{k}{\mathcal{S}_{k,i}\left(\frac{{\left(N\right)}_{\left(i + 1\right)}}{i + 1} - \frac{{\left(n_{0}\right)}_{\left(i + 1\right)}}{i + 1}\right)}
$$
where we recognize $\sum_{n=n_{0}}^{N - 1} {\left(n\right)}_{i} = w_{N} - w_{n_{0}} + \frac{{\left(N\right)}_{\left(i + 1\right)}}{i + 1} - \frac{{\left(n_{0}\right)}_{\left(i + 1\right)}}{i + 1}$ where $w_{N}-w_{n_{0}}=0$ since $\{w_{n}\}_{n\in\mathbb{n}}$ is a constant sequence.

### applications

```python
def power_summation_thm():
    n, S, (N, x, k, i) = IndexedBase('n'), IndexedBase('\mathcal{S}'), symbols('N x k i')
    inf, sup = n[0], N-1
    return (Eq(Sum(x**k, (x, inf, sup)), Sum(s[k,i]*((ff(sup+1, i+1)/(i+1))-(ff(inf, i+1)/(i+1))), (i, 1, k))),
            [n, S, inf, sup, N, x, k, i])

def expand_Sum(aSumExpr):

    generic_sum_coeff, (sum_index, starting_bound, ending_bound) = aSumExpr.args

    summands = [generic_sum_coeff.subs(sum_index, n) for n in range(starting_bound, ending_bound+1)]
    result = Add(*summands, evaluate=False)

    return result

def stirling_row(row, indexed=None, *args, **kwds):
    return {indexed[row, i] if indexed else (row,i):stirling(row, i, *args, **kwds) for i in range(row+1)}

def do_powers_summation(power, bottom=1, top=Symbol('n'), expand=True):
    eq, (n, S, inf, sup, N, x, k, i) = power_summation_thm()
    inst_eq = eq.subs({inf:bottom, k:power, N:top+1}, simultaneous=True)
    rhs = expand_Sum(inst_eq.rhs)
    rhs = rhs.subs(stirling_row(power, S)).factor() if expand else rhs
    return Eq(inst_eq.lhs, rhs)

```

```python
power_summation_thm()
```

```python
do_powers_summation(power=1, bottom=1)
```

```python
do_powers_summation(power=2, bottom=1)
```

```python
do_powers_summation(power=3, bottom=1)
```

```python
do_powers_summation(power=3, bottom=1, expand=False)
```

```python
ten_powers = do_powers_summation(power=10)
ten_powers
```

```python
ten_powers.replace(Symbol('n'),20).doit()
```

## $\mathbb{R} \rightarrow \mathbb{N}$, via Stirling numbers of the *first* type

It is possible to revert the previous argument and find a characterization for $(x)_{i}$ using powers $x^{i}$ as follows:
$$
\sum_{i=1}^{n}{\mathcal{s}_{n,i} x^{i}} = (x)_{n}
$$
where coefficients $\mathcal{s}_{n,i}$ are Stirling's numbers of the first kind, tabulated in the following matrix:

```python
stirling_matrix_first_kink = Matrix(11,11,lambda i, j: stirling(i,j, kind=1, signed=True))
stirling_matrix_first_kink
```

Stirling's matrices of numbers are inverses the one of the other, namely:

```python
stirling_matrix_second_kink**(-1)
```

therefore their product yields the identity matrix:

```python
stirling_matrix_second_kink*stirling_matrix_first_kink
```

---

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.


# Section 3.1 RREF exercises

```python
from sympy import *
init_printing()

%matplotlib notebook
import matplotlib.pyplot as mpl
from util.plot_helpers import plot_augmat # helper function
```

## RREF

### E3.1

```python
AUG = Matrix([
  [3,      3, 6],
  [2, S(3)/2, 5]])
AUG
```

```python
AUG.rref()
# solution is x=4, y=-2
```

### Verify the solution geometrically

```python
AUG = Matrix([
  [3,      3,  6],
  [2, S(3)/2,  5]])

fig = mpl.figure()
plot_augmat(AUG)
# the solution (4,-2) is where the two lines intersect
```

    <IPython.core.display.Javascript object>

    Line 1:  3x +3y = 6
    Line 2:  2x +1y = 5

### E3.2

```python
A = Matrix([
         [3,       3,  6],
         [2,  S(3)/2,  5]])
A
```

```python
# First row operation:  R1  <-- 1/3*R1
A[0,:] = A[0,:]/3
A
```

```python
# Second row operation:  R2  <-- R2 - 2*R1
A[1,:] = A[1,:] - 2*A[0,:]
A
```

```python
# Third row operation:  R2  <-- -2*R2
A[1,:] = -2*A[1,:]
A
```

```python
# Fourth row operation:  R1  <-- R1 - R2
A[0,:] = A[0,:] - A[1,:]
A
```

### E3.3

```python
AUGA = Matrix([
 [3, 3,  6],
 [1, 1,  5]])
AUGA.rref()
# no solutions since second row   0x+0y == 1  is impossible
```

```python
# verify geomtetrically
AUGA = Matrix([
 [3, 3,  6],
 [1, 1,  5]])

fig = mpl.figure()
plot_augmat(AUGA)
# no solution since lines two lines are parallel
```

    <IPython.core.display.Javascript object>

    Line 1:  3x +3y = 6
    Line 2:  1x +1y = 5

```python

```

```python
AUGB = Matrix([
  [3,     3,  6],
  [2, S(3)/2, 3]])
AUGB.rref()
# one solution x=0, y=2
```

```python
# verify geomtetrically
AUGB = Matrix([
  [3,     3,  6],
  [2, S(3)/2, 3]])

fig = mpl.figure()
plot_augmat(AUGB)
# the solution (0,2) is where the two lines intersect
```

    <IPython.core.display.Javascript object>

    Line 1:  3x +3y = 6
    Line 2:  2x +1y = 3

```python
AUGC = Matrix([
  [3, 3,  6],
  [1, 1,  2]])
AUGC.rref()
# infinitely many soln's of the form  point + s*dir, for s in \mathbb{R}
```

```python
# to complete the solution,
# observe the second column is a free varible y = s
# and thus we have these equations:
#   x +  s = 2
#  0x + 0s = 0   (trivial eqn.)
#        y = s   (def'n of free variable)
# thus solution is:
#      [x,y]  =  [2-s,s]  =  [2,0] + s*[-1,1]     for s in \mathbb{R}
```

```python
# verify geomtetrically
AUGC = Matrix([
  [3, 3,  6],
  [1, 1,  2]])

fig = mpl.figure()
plot_augmat(AUGC)
# the solution is infinite since two lines are the same
```

    <IPython.core.display.Javascript object>

    Line 1:  3x +3y = 6
    Line 2:  1x +1y = 2

```python

```

```python

```

## Matrix equations

```python

```

```python

```

```python

```

```python

```

## Matrix product

### E3.5

Compute the following matrix products:
$$
P =
\begin{bmatrix} 	1&2 \\   3&4  \end{bmatrix}
\begin{bmatrix} 	5&6 \\   7&8  \end{bmatrix}
\quad
\textrm{and}
\quad
Q =
\begin{bmatrix}
3& 1 &  2&  2 \\ 0 & 2 & -2 & 1
\end{bmatrix}\!\!		%
\begin{bmatrix}
    -2 &  3 \\
    \ \ 1 &  0 \\
    -2 & \!\!-2 \\
    \ \ 2 &  2
  \end{bmatrix}\!.
$$

```python
# define matrices
P1 = Matrix([
    [1, 2],
    [3, 4]])
P2 = Matrix([
    [5, 6],
    [7, 8]])

P = P1*P2
P
```

```python
# define matrices
Q1 = Matrix([[3, 1,  2, 2],
             [0, 2, -2, 1]])
Q2 = Matrix([[-2,  3],
             [ 1,  0],
             [-2, -2],
             [ 2,  2]])
```

## Determinants

```python

```

```python

```

```python

```

## Matrix inverse

```python

```