```python
import numpy as np  #Importa libreria numerica
import sympy as sym #simbolica
import matplotlib.pyplot as plt #importa matplotlib solo pyplot
import matplotlib.image as mpimg
from sympy.plotting import plot  #para plotear 2 variables
from sympy.plotting import plot3d  # para 3
from sympy.plotting import plot3d_parametric_surface
from IPython.display import Image
import ipympl  #Para importar gestor de imagenes
sym.init_printing() #activa a jupyter para mostrar simbolicamente el output

%matplotlib inline
```

```python
Image(filename='LAB1_2.png',width=300)
```

```python
Vin, Vo, V3, V2 = sym.symbols('V_{in}, V_o, V_+, V_-')
V2=V3
Ir1, Ir2, Ir3, Ir4 = sym.symbols('I_{R1}, I_{R2},I_{R3},I_{R4}')
R1, R2, R3, R4, RL = sym.symbols('R1, R2, R3, R4, R_{L}')

Irl = sym.Function('I_{R_L}')(Vin,RL) #Defino viarible simbolica Irl como una funcion de Vin y RL
sym.pprint(Irl)

#SUM(I) en nodo 3 (+) es CERO =>
eq_Irl = sym.Eq(Irl,Ir1+Ir3)
sym.pprint(eq_Irl)

```

    I_{R_L}(V_{in}, R_{L})
    I_{R_L}(V_{in}, R_{L}) = I_{R1} + I_{R3}

```python
#La corriente en Ir1 es: (Vo-V+)/R1 = Irl - Ir3
# Ir3 = (Vin - V+)/R3
Ir1 = (Vo-V3)/R1
Ir3= (Vin - V3)/R3
eq_Irl = sym.Eq(Irl,Ir1+Ir3)
sym.pprint(eq_Irl)
res=sym.solve(eq_Irl,(Vo-V3))
eq_Vo3=sym.Eq(Vo-V3,sym.expand(res[0]))
sym.pprint(eq_Vo3)
```

                             -V₊ + V_{in}   -V₊ + Vₒ
    I_{R_L}(V_{in}, R_{L}) = ──────────── + ────────
                                  R₃           R₁
                                           R₁⋅V₊   R₁⋅V_{in}
    -V₊ + Vₒ = R₁⋅I_{R_L}(V_{in}, R_{L}) + ───── - ─────────
                                             R₃        R₃

```python
#SUM(I) en nodo 2 (-) es CERO =>
eq_Ir2 = sym.Eq(Ir2,Ir4)
sym.pprint(eq_Ir2)
Ir2 = (Vo-V3)/R2
Ir4= (V2)/R4
eq_Ir2 = sym.Eq(Ir2,Ir4)
sym.pprint(eq_Ir2)
res2=sym.solve(eq_Ir2,(Vo-V3))
eq_Vo3_=sym.Eq(Vo-V3,sym.expand(res2[0]))
sym.pprint(eq_Vo3_)
```

    I_{R2} = I_{R4}
    -V₊ + Vₒ   V₊
    ──────── = ──
       R₂      R₄
               R₂⋅V₊
    -V₊ + Vₒ = ─────
                 R₄

```python
#Remplazando
eq_=sym.Eq(eq_Vo3.rhs-eq_Vo3_.rhs)
sym.pprint(eq_)
```

                                R₁⋅V₊   R₁⋅V_{in}   R₂⋅V₊
    R₁⋅I_{R_L}(V_{in}, R_{L}) + ───── - ───────── - ───── = 0
                                  R₃        R₃        R₄

```python
# Irl = V+/RL => V+=Irl*RL
# R1 = 100Ω; R2 = 10KΩ; R3 = 1KΩ y R4 = 100KΩ
res=sym.solve([eq_.subs({V3:Irl/RL,(R2/R4):(R1/R3)})],Irl)
sym.pprint(res)
```

    ⎧                        V_{in}⎫
    ⎨I_{R_L}(V_{in}, R_{L}): ──────⎬
    ⎩                          R₃  ⎭

```python
Vo= sym.Function('Vo')(Vin,RL) #Defino viarible simbolica Vo como una funcion de Vin y RL
Irl= sym.Symbol('I_{RL}')
sym.pprint(Vo)
sym.pprint(eq_Vo3) #De esa ecuacion se remplaza V+= RL * Irl
sym.pprint(sym.Eq(V3,RL*Irl))
```

    Vo(V_{in}, R_{L})
                                           R₁⋅V₊   R₁⋅V_{in}
    -V₊ + Vₒ = R₁⋅I_{R_L}(V_{in}, R_{L}) + ───── - ─────────
                                             R₃        R₃
    V₊ = I_{RL}⋅R_{L}

```python
eq_Vo=sym.Eq(Vo,Irl*(RL+R1+RL*R1/R3)-Vin*(R1/R3))
sym.pprint(eq_Vo)
#REMPLAZO DEL RESULTADO Irl(V1,RL)
sym.pprint(sym.simplify((eq_Vo.subs(Irl,(Vin/R3)))))
```

                               ⎛     R₁⋅R_{L}        ⎞   R₁⋅V_{in}
    Vo(V_{in}, R_{L}) = I_{RL}⋅⎜R₁ + ──────── + R_{L}⎟ - ─────────
                               ⎝        R₃           ⎠       R₃
                        R_{L}⋅V_{in}⋅(R₁ + R₃)
    Vo(V_{in}, R_{L}) = ──────────────────────
                                   2
                                 R₃

```python
#RL MAX como VoMAX=10v
#R1 = 100Ω; R2 = 10KΩ; R3 = 1KΩ y R4 = 100KΩ
Vo = sym.Symbol('Vo')
RLM=sym.Function('R_{L_{MAX}}')(Vin)
eq_RLM=sym.Eq(RLM,Vo/(Vin*(R3+R1)/R3**2))
sym.pprint(eq_RLM)
sym.pprint(eq_RLM.subs({Vo:10,R1:100,R2:10e3,R3:1e3,R4:100e3}))
```

                                 2
                               R₃ ⋅Vo
    R_{L_{MAX}}(V_{in}) = ────────────────
                          V_{in}⋅(R₁ + R₃)
                          9090.90909090909
    R_{L_{MAX}}(V_{in}) = ────────────────
                               V_{in}

```python

```


# Multivariate Distribution

> In this post, we will build multivariate distribution. This is the summary of lecture "Probabilistic Deep Learning with Tensorflow 2" from Imperial College London.

- toc: true
- badges: true
- comments: true
- author: Chanseok Kang
- categories: [Python, Coursera, Tensorflow_probability, ICL]
- image: images/2d_gaussian.png

## Packages

```python
import tensorflow as tf
import tensorflow_probability as tfp

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

tfd = tfp.distributions
plt.rcParams['figure.figsize'] = (10, 6)
```

```python
print("Tensorflow Version: ", tf.__version__)
print("Tensorflow Probability Version: ", tfp.__version__)
```

    Tensorflow Version:  2.5.0
    Tensorflow Probability Version:  0.13.0

## Multivariate Distribution

Mutlivariate distribution is the joint distribution that consists of multiple univariate random variable. Usually, one widely-used multivariate distribution is Multivariate Normal Distribution (or MVN for short). From [wikipedia](https://en.wikipedia.org/wiki/Multivariate_normal_distribution),

> he multivariate normal distribution, multivariate Gaussian distribution, or joint normal distribution is a generalization of the one-dimensional (univariate) normal distribution to higher dimensions. One definition is that a random vector is said to be k-variate normally distributed if every linear combination of its k components has a univariate normal distribution. Its importance derives mainly from the multivariate central limit theorem. The multivariate normal distribution is often used to describe, at least approximately, any set of (possibly) correlated real-valued random variables each of which clusters around a mean value.

This distribution is the element for constructing neural network architecture, such as Variational AutoEncoder (VAE).

### Basic multivariate distributions

We can make 2D multivariate Normal Distribution with diagonal covariance matrix. The formal form is like this,

$$ X \sim \mathcal{N}(\mu, \Sigma) $$

This distribution contains mean vector $\mu$,

$\mu = E[X] = (E[X_1], E[X_2], \dots, E[X_k])^T$

and $k \times k$ covariance Matrix.

$\Sigma_{i,j} = E[(X_i-\mu_i)(X_j - \mu_j)] = Cov[X_i, X_j] $

```python
# Define 2D multivariate Normal distribution with diagonal covariance matrix
normal_diag = tfd.MultivariateNormalDiag(loc=[0, 1], scale_diag=[1, 2])
normal_diag
```

    <tfp.distributions.MultivariateNormalDiag 'MultivariateNormalDiag' batch_shape=[] event_shape=[2] dtype=float32>

The usage is the same as univariate distribution. You need to define loc (mean) and scale (standard deviation) of each random variables. Above case, we used 2 random variables that one has 0 mean and 1 standard deviation, and the other is 1 mean and 2 standard deviation. Note that, this distribution has event_shape of 2.

```python
normal_diag.sample(5)
```

    <tf.Tensor: shape=(5, 2), dtype=float32, numpy=
    array([[ 2.8616788 , -0.38970935],
           [-0.23513892, -1.5368791 ],
           [-0.42713124, -0.48392642],
           [ 0.9403765 ,  2.914402  ],
           [-0.45310387, -0.61969984]], dtype=float32)>

```python
samples = normal_diag.sample(10000)
plt.plot(0, 1, c='r', marker='.') # Center point (0, 1)
plt.scatter(samples[:, 0], samples[:, 1], marker='.', alpha=0.05)
plt.axis('equal')
plt.show()
```

### Batches of multivariate distributions

```python
# Create three "batches" of multivariate normals
normal_diag_batch = tfd.MultivariateNormalDiag(loc=[[0, 0], [0, 0], [0, 0]],
                                               scale_diag=[[1, 2], [2, 1], [2, 2]])
normal_diag_batch
```

    <tfp.distributions.MultivariateNormalDiag 'MultivariateNormalDiag' batch_shape=[3] event_shape=[2] dtype=float32>

In this case, we built 3 batches of multivariate normal distribution. You can see the batch_shape of 3.

```python
samples = normal_diag_batch.sample(5)
samples
```

    <tf.Tensor: shape=(5, 3, 2), dtype=float32, numpy=
    array([[[ 0.54938745,  3.2343671 ],
            [ 2.0630057 , -0.22117338],
            [ 1.4602997 ,  0.6154384 ]],

           [[-0.48758066, -3.6894617 ],
            [-0.68254   , -1.9999902 ],
            [ 0.09972456, -0.68538326]],

           [[ 1.8619668 ,  1.7146002 ],
            [ 2.2729723 ,  1.2265631 ],
            [-0.7838083 , -2.096915  ]],

           [[ 0.31185022, -1.4191006 ],
            [-1.9356333 ,  1.8224419 ],
            [ 0.7653813 ,  1.0019186 ]],

           [[-1.159586  , -3.834509  ],
            [ 3.2854838 , -0.89201176],
            [ 0.26501343,  0.49291056]]], dtype=float32)>

In above code, the tensor shape has `(5, 3, 2)`. First element shows the number of sample, and second element is batch_size. And last one is the number of random variable.

```python
# Compute log probs
normal_diag_batch.log_prob(samples)
```

    <tf.Tensor: shape=(5, 3), dtype=float32, numpy=
    array([[-3.9895787, -3.087482 , -3.5380764],
           [-4.3514075, -4.589237 , -3.2841334],
           [-4.631966 , -3.9290533, -3.8505974],
           [-2.8313804, -4.660006 , -3.4228776],
           [-5.0412765, -4.2781672, -3.2633204]], dtype=float32)>

```python
samples_batch = normal_diag_batch.sample(10000)
samples_batch.shape
```

    TensorShape([10000, 3, 2])

If you are not sure about this distribution, you can plot it with each random variable.

```python
fig, ax = plt.subplots(1, 3, sharex=True, sharey=True, figsize=(10, 5))
titles = ['cov_diag=[1, 2]', 'cov_diag=[2, 1]', 'cov_diag=[2, 2]']

for i, (ax, title) in enumerate(zip(ax, titles)):
    samples = samples_batch[:, i, :]
    ax.scatter(samples[:, 0], samples[:, 1], marker='.', alpha=0.05)
    ax.set_title(title)
plt.show()
```

## Multivariate Normal Distribution with full covariance.
In the previous section, we covered multivariate distribution with diagonal covariance matrix, i.e. $\Sigma = \sigma^2 I$. This is known as a **spherical** or **isotropic** Gaussian. This name comes from the spherical (or circular) contours of its probability density function, as you can see from the plot below for the two-dimensional case.

```python
spherical_2d_gaussian = tfd.MultivariateNormalDiag(loc=[0, 0])
samples = spherical_2d_gaussian.sample(10000)
```

```python
sns.jointplot(x=samples[:, 0], y=samples[:, 1], kind='kde', space=0)
plt.show()
```

Note that, diagonal covariance matrix results in the components of the random vector being independent.

### Full covariance with `MultivariateNormalFullTriL`

You can define a full covariance Gaussian distribution in TensorFlow using the Distribution `tfd.MultivariateNormalTriL`.

For the reference, `FullTriL` stands for Full covariance with Lower Triangular matrix.

Mathematically, the parameters of a multivariate Gaussian are a mean $\mu$ and a covariance matrix $\Sigma$, and so the `tfd.MultivariateNormalTriL` constructor requires two arguments:

- `loc`, a Tensor of floats corresponding to $\mu$,
- `scale_tril`, a a lower-triangular matrix $L$ such that $LL^T = \Sigma$.

For a $d$-dimensional random variable, the lower-triangular matrix $L$ looks like this:

\begin{equation}
    L = \begin{bmatrix}
            l_{1, 1} & 0 & 0 & \cdots & 0 \\
            l_{2, 1} & l_{2, 2} & 0 & \cdots & 0  \\
            l_{3, 1} & l_{3, 2} & l_{3, 3} & \cdots & 0  \\
            \vdots  & \vdots & \vdots & \ddots & \vdots \\
            l_{d, 1} & l_{d, 2} & l_{d, 3} & \cdots & l_{d, d}
        \end{bmatrix},
\end{equation}

where the diagonal entries are positive: $l_{i, i} > 0$ for $i=1,\ldots,d$.

Here is an example of creating a two-dimensional Gaussian with non-diagonal covariance:

```python
mu = [0., 0.]
scale_tril = [[1., 0.],
              [0.6, 0.8]]

sigma = tf.matmul(tf.constant(scale_tril), tf.transpose(tf.constant(scale_tril)))
sigma
```

    <tf.Tensor: shape=(2, 2), dtype=float32, numpy=
    array([[1. , 0.6],
           [0.6, 1. ]], dtype=float32)>

```python
# Create the 2D Gaussian with full covariance
nonspherical_2d_gaussian = tfd.MultivariateNormalTriL(loc=mu, scale_tril=scale_tril)
nonspherical_2d_gaussian
```

    <tfp.distributions.MultivariateNormalTriL 'MultivariateNormalTriL' batch_shape=[] event_shape=[2] dtype=float32>

```python
nonspherical_2d_gaussian.mean()
```

    <tf.Tensor: shape=(2,), dtype=float32, numpy=array([0., 0.], dtype=float32)>

```python
nonspherical_2d_gaussian.covariance()
```

    <tf.Tensor: shape=(2, 2), dtype=float32, numpy=
    array([[1. , 0.6],
           [0.6, 1. ]], dtype=float32)>

```python
samples = nonspherical_2d_gaussian.sample(10000)
sns.jointplot(x=samples[:, 0], y=samples[:, 1], kind='kde', space=0, color='r')
plt.show()
```

As you can see, the approximate density contours are now elliptical rather than circular. This is because the components of the Gaussian are correlated.

Also note that the marginal distributions (shown on the sides of the plot) are both univariate Gaussian distributions.

### The Cholesky decomposition

In the above example, we defined the lower triangular matrix $L$ and used that to build the multivariate Gaussian distribution. The covariance matrix is easily computed from $L$ as $\Sigma = LL^T$.

The reason that we define the multivariate Gaussian distribution in this way - as opposed to directly passing in the covariance matrix - is that not every matrix is a valid covariance matrix. The covariance matrix must have the following properties:

1. It is symmetric
2. It is positive (semi-)definite

> A symmetric matrix $M \in \mathbb{R}^{d\times d}$ is positive semi-definite if it satisfies $b^TMb \ge 0$ for all nonzero $b\in\mathbb{R}^d$. If, in addition, we have $b^TMb = 0 \Rightarrow b=0$ then $M$ is positive definite.

The [**Cholesky decomposition**](https://en.wikipedia.org/wiki/Cholesky_decomposition) is a useful way of writing a covariance matrix. The decomposition is described by this result:

> For every real-valued symmetric positive-definite matrix $M$, there is a unique lower-diagonal matrix $L$ that has  positive diagonal entries for which
>
> \begin{equation}
     LL^T = M
 \end{equation}
> This is called the _Cholesky decomposition_ of $M$.

This result shows us why Gaussian distributions with full covariance are completely represented by the `MultivariateNormalTriL` Distribution.

#### `tf.linalg.cholesky`

In case you have a valid covariance matrix $\Sigma$ and would like to compute the lower triangular matrix $L$ above to instantiate a `MultivariateNormalTriL` object, this can be done with the `tf.linalg.cholesky` function.

```python
# define a symmetric positive-definite matrix

sigma = [[10., 5.], [5., 10.]]
```

```python
# Compute the lower triangular matrix L from the Cholesky decomposition

scale_tril = tf.linalg.cholesky(sigma)
scale_tril
```

    <tf.Tensor: shape=(2, 2), dtype=float32, numpy=
    array([[3.1622777, 0.       ],
           [1.5811388, 2.7386127]], dtype=float32)>

You can check the result with formal definition.

```python
tf.linalg.matmul(scale_tril, tf.transpose(scale_tril))
```

    <tf.Tensor: shape=(2, 2), dtype=float32, numpy=
    array([[10.      ,  5.      ],
           [ 5.      ,  9.999999]], dtype=float32)>

If the argument to the `tf.linalg.cholesky` is not positive definite, then it will fail:

```python
# Try to compute the Cholesky decomposition for a matrix with negative eigenvalues

bad_sigma = [[10., 11.], [11., 10.]]

try:
    scale_tril = tf.linalg.cholesky(bad_sigma)
except Exception as e:
    print(e)
```

    Cholesky decomposition was not successful. The input might not be valid. [Op:Cholesky]

### What about positive semi-definite matrices?

In cases where the matrix is only positive semi-definite, the Cholesky decomposition exists (if the diagonal entries of $L$ can be zero) but it is not unique.

For covariance matrices, this corresponds to the degenerate case where the probability density function collapses to a subspace of the event space. This is demonstrated in the following example:

```python
# Create a multivariate Gaussian with a positive semi-definite covariance matrix
psd_mvn = tfd.MultivariateNormalTriL(loc=[0., 0.], scale_tril=[[1., 0.], [0.4, 0.]])
psd_mvn
```

    <tfp.distributions.MultivariateNormalTriL 'MultivariateNormalTriL' batch_shape=[] event_shape=[2] dtype=float32>

```python
samples = psd_mvn.sample(10000)

plt.scatter(samples[:, 0], samples[:, 1], alpha=0.5)
plt.xlim(-5, 5)
plt.ylim(-5, 5)
plt.title('Scatter plot of samples')
plt.show()
```

If the input to the function `tf.linalg.cholesky` is positive semi-definite but not positive definite, it will also fail:

```python
# Try to compute the Cholesky decomposition for a positive semi-definite matrix

another_bad_sigma = [[10., 0.], [0., 0.]]

try:
    scale_tril = tf.linalg.cholesky(another_bad_sigma)
except Exception as e:
    print(e)
```

    Cholesky decomposition was not successful. The input might not be valid. [Op:Cholesky]

In summary: if the covariance matrix $\Sigma$ for your multivariate Gaussian distribution is positive-definite, then an algorithm that computes the Cholesky decomposition of $\Sigma$ returns a lower-triangular matrix $L$ such that $LL^T = \Sigma$. This $L$ can then be passed as the `scale_tril` of `MultivariateNormalTriL`.

### Putting it all together

You are now ready to put everything that you have learned in this reading together.

To create a multivariate Gaussian distribution with full covariance you need to:

1. Specify parameters $\mu$ and either $\Sigma$ (a symmetric positive definite matrix) or $L$ (a lower triangular matrix with positive diagonal elements), such that $\Sigma = LL^T$.

2. If only $\Sigma$ is specified, compute `scale_tril = tf.linalg.cholesky(sigma)`.

3. Create the distribution: `multivariate_normal = tfd.MultivariateNormalTriL(loc=mu, scale_tril=scale_tril)`.

```python
# Create a multivariate Gaussian distribution

mu = [1., 2., 3.]
sigma = [[0.5, 0.1, 0.1],
         [0.1, 1., 0.6],
          [0.1, 0.6, 2.]]

scale_tril = tf.linalg.cholesky(sigma)
multivariate_normal = tfd.MultivariateNormalTriL(loc=mu, scale_tril=scale_tril)
```

```python
multivariate_normal.covariance()
```

    <tf.Tensor: shape=(3, 3), dtype=float32, numpy=
    array([[0.4999999 , 0.09999999, 0.09999999],
           [0.09999999, 1.0000001 , 0.6000001 ],
           [0.09999999, 0.6000001 , 2.0000002 ]], dtype=float32)>

```python
multivariate_normal.mean()
```

    <tf.Tensor: shape=(3,), dtype=float32, numpy=array([1., 2., 3.], dtype=float32)>

### Deprecated: `MultivariateNormalFullCovariance`

There was previously a class called `tfd.MultivariateNormalFullCovariance` which takes the full covariance matrix in its constructor, but this is being deprecated. Two reasons for this are:

* covariance matrices are symmetric, so specifying one directly involves passing redundant information, which involves writing unnecessary code.
* it is easier to enforce positive-definiteness through constraints on the elements of a decomposition than through a covariance matrix itself. The decomposition's only constraint is that its diagonal elements are positive, a condition that is easy to parameterize for.

## Further reading and resources
* https://www.tensorflow.org/probability/api_docs/python/tfp/distributions/MultivariateNormalTriL
* https://www.tensorflow.org/api_docs/python/tf/linalg/cholesky


# Problema:
Solução da cadeia cinemática do robô antropomorphico mostrado abaixo.

O mecanismo possui 2 elos e 3 juntas.

## Sistema de Coordenadas
Primeiramente, temos que determinar a localização dos referenciais. O primeiro referencial, ```B0```, está fixo e será colocado na intercessão dos eixos das duas primeiras juntas. O segundo referencial, ```B1``` , está em rotação pura de $\theta_1$ em relação ao eixo $y$ do referencial ```B0```, e suas origens são coincidentes. A origem do referencial ```B2``` é a mesma de ```B1```  e ele segue o movimento da segunda junta, ou seja, ele está em rotação pura de $\theta_2$ em relação ao eixo $z$ de ```B1```. Por fim, a origem do referencial ```B3``` coincide com a intercessão do eixo $x$ de ```B2``` e o eixo de rotação da terceira junta, além de ser uma rotação pura de $\theta_3$ em relação ao eixo $z$ de ```B2```. Os elos são as partes rígidas que conectam as juntas; assim, nesse caso, temos três elos. Como o primeiro referencial é conincidente com os dois próximos, só precisaremos definir o comprimento de dois dos elos. Definindo o ponto $A$ na junta 2, o ponto $B$ na junta 3 e o ponto $C$ na ponta do último elo, podemos definir a distância entre os pontos  A e B, e B e C como sendo $l_1$ e $l_2$, respectivamente.
Assim, já podemos iniciar a implementação da solução do problema.

```python
# Funções das Bibliotecas Utilizadas
from sympy import symbols
from sympy.physics.mechanics import dynamicsymbols
from sympy.physics.vector import ReferenceFrame

# Variáveis Simbólicas
theta_1, theta_2, theta_3 = dynamicsymbols('theta_1 theta_2 theta_3')
l_1, l_2 = symbols('l_1 l_2', positive = True)

# Referenciais
B0 = ReferenceFrame('B0')                 # Referencial Parado
B1 = ReferenceFrame('B1')
B1.orient(B0, 'Axis', [theta_1, B0.y])    # Referencial móvel: theta_1 em relação a B0.y
B2 = ReferenceFrame('B2')
B2.orient(B1, 'Axis', [theta_2, B1.z])    # Referencial móvel: theta_2 em relação a B1.z
B3 = ReferenceFrame('B3')
B3.orient(B2, 'Axis', [theta_3, B2.z])    # Referencial móvel: theta_3 em relação a B2.z
```

Esse código cria as variáveis simbolicas e os referenciais que serão usados pelo ```sympy```. $\theta_1, \theta_2$ e $\theta_3$ foram definidas usando ```dynammicsymbols``` para mostrar que as variáveis são funções do tempo, e o método ```symbols``` foi usado junto com o argumento ```positive=True``` para definir que os comprimentos dos elos assumem apenas valores positivos.
Através na classe ```ReferenceFrame``` podemos facilmente criar referenciais inerciais. Todos os referenciais foram definidos a partir de uma rotação de outro referencial definido anteriormente, com exceção do referencial fixo ```B0```.

### Matrizes de Rotação
Então com essas definições fica fácil representar as matrizes de rotação entre qualquer um dos referenciais definidos. Para isso, invocamos o método ```.dcm``` no referencial que queremos definir a partir do referencial do argumento. A seguir estão alguns exemplos, junto com o representação dos resultados em LaTeX.

```python
# Matrizes de Rotação
T_B0B1 = B0.dcm(B1)    # Matriz de rotação de B1 para B0
T_B1B2 = B1.dcm(B2)    # Matriz de rotação de B2 para B1
T_B2B3 = B2.dcm(B3)    # Matriz de rotação de B3 para B2

T_B0B3 = (B0.dcm(B3)).simplify()    # Matriz de rotação de B0 para B3
T_B0B2 = (B0.dcm(B2)).simplify()

# Resultados em LaTeX
from IPython.display import Latex
from sympy import latex

Latex("\\begin{eqnarray}" +
     "T_{\\theta_1}&=" + "&" +str(latex(T_B0B1)) + " T_{\\theta_2}="
     + str(latex(T_B1B2)) + "T_{\\theta_3}=" + str(latex(T_B2B3)) +
     "\\\\T_{\\theta_1, \\theta_3}&=" + "&" + str(latex(T_B0B3))
     + "\\end{eqnarray}")
```

\begin{eqnarray}T_{\theta_1}&=&\left[\begin{matrix}\cos{\left (\theta_{1}{\left (t \right )} \right )} & 0 & \sin{\left (\theta_{1}{\left (t \right )} \right )}\\0 & 1 & 0\\- \sin{\left (\theta_{1}{\left (t \right )} \right )} & 0 & \cos{\left (\theta_{1}{\left (t \right )} \right )}\end{matrix}\right] T_{\theta_2}=\left[\begin{matrix}\cos{\left (\theta_{2}{\left (t \right )} \right )} & - \sin{\left (\theta_{2}{\left (t \right )} \right )} & 0\\\sin{\left (\theta_{2}{\left (t \right )} \right )} & \cos{\left (\theta_{2}{\left (t \right )} \right )} & 0\\0 & 0 & 1\end{matrix}\right]T_{\theta_3}=\left[\begin{matrix}\cos{\left (\theta_{3}{\left (t \right )} \right )} & - \sin{\left (\theta_{3}{\left (t \right )} \right )} & 0\\\sin{\left (\theta_{3}{\left (t \right )} \right )} & \cos{\left (\theta_{3}{\left (t \right )} \right )} & 0\\0 & 0 & 1\end{matrix}\right]\\T_{\theta_1, \theta_3}&=&\left[\begin{matrix}\cos{\left (\theta_{2}{\left (t \right )} + \theta_{3}{\left (t \right )} \right )} \cos{\left (\theta_{1}{\left (t \right )} \right )} & - \sin{\left (\theta_{2}{\left (t \right )} + \theta_{3}{\left (t \right )} \right )} \cos{\left (\theta_{1}{\left (t \right )} \right )} & \sin{\left (\theta_{1}{\left (t \right )} \right )}\\\sin{\left (\theta_{2}{\left (t \right )} + \theta_{3}{\left (t \right )} \right )} & \cos{\left (\theta_{2}{\left (t \right )} + \theta_{3}{\left (t \right )} \right )} & 0\\- \sin{\left (\theta_{1}{\left (t \right )} \right )} \cos{\left (\theta_{2}{\left (t \right )} + \theta_{3}{\left (t \right )} \right )} & \sin{\left (\theta_{2}{\left (t \right )} + \theta_{3}{\left (t \right )} \right )} \sin{\left (\theta_{1}{\left (t \right )} \right )} & \cos{\left (\theta_{1}{\left (t \right )} \right )}\end{matrix}\right]\end{eqnarray}

### Posições Relativas de $A$, $B$ e $C$
Por termos definido um referencial para cada grau de liberdade do mecanismo, podemos facilmente expressar a posição de cada um dos pontos $A$, $B$ e $C$ em relação a esses referênciais. O ponto $A$ coincide com a origem do referencial fixo, ou seja, $\vec{r_{OA}}=\vec{0}$. O ponto $B$ é facilmente expresso em relação ao referencial ```B2```, nesse caso temos que $\vec{r_{AB}}={l_1 \vec{{i}_{B2}}}$. Finalmente, o ponto $C$ é representado por $\vec{r_{BC}}={l_2 \vec{{i}_{B3}}}$. Sendo $\vec{{i}_{B2}}$ e $\vec{{i}_{B3}}$ os versores na direção $x$ dos referenciais ```B2``` e  ```B3``` respectivamente. A seguir, nota-se que a definição desses vetores posição é simples de ser implementado em ```sympy```. A principal vantagem é que não precisamos nos preocupar em relação a qual referencial estamos definindo os vetores, pois as relações entre os referenciais já foram criadas nas suas definições. Ou seja, podemos reescrever um vetor em relação a diferentes referenciais de maneira imediata, como mostrado abaixo.

```python
from sympy.physics.vector import Vector

# Vetores Posição entre os Pontos
r_OA = Vector(0)      # Vetor Nulo
r_AB = l_1 * B2.x     # Vetor que liga os pontos A e B expresso no referencial móvel B2
r_BC = l_2 * B3.x     # Vetor que liga os pontos B e C expresso no referencial móvel B3

R_AB = r_AB.express(B0) # Vetor que liga os pontos A e B expresso no referencial fixo B0
R_BC = r_BC.express(B0) # Vetor que liga os pontos B e C expresso no referencial fixo B0

# Resultado em LaTeX na Forma Vetorial ou Vetor Coluna
Latex("\\begin{eqnarray}" +
     "R_{AB}&=" + "&" +str(latex(R_AB)) + "\\\\" +
     "R_{AB}&=" + "&" +str(latex(R_AB.to_matrix(B0))) +
     "\\end{eqnarray}")
```

\begin{eqnarray}R_{AB}&=&l_{1} \operatorname{cos}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2}\right)\mathbf{\hat{b0}_x} + l_{1} \operatorname{sin}\left(\theta_{2}\right)\mathbf{\hat{b0}_y} -  l_{1} \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2}\right)\mathbf{\hat{b0}_z}\\R_{AB}&=&\left[\begin{matrix}l_{1} \cos{\left (\theta_{1}{\left (t \right )} \right )} \cos{\left (\theta_{2}{\left (t \right )} \right )}\\l_{1} \sin{\left (\theta_{2}{\left (t \right )} \right )}\\- l_{1} \sin{\left (\theta_{1}{\left (t \right )} \right )} \cos{\left (\theta_{2}{\left (t \right )} \right )}\end{matrix}\right]\end{eqnarray}

## Resultados
Assim, já podemos encontrar os resultados da cinemática do ponto $C$ em relação a ```B0```. Para tal, só precisamos calcular a os vetores posição dos pontos $A$, $B$ e $C$, e suas primeiras e segunda derivadas no tempo, já que as variáveis $\theta_1$, $\theta_2$ e $\theta_3$ foram definidas como funções do tempo. Para melhor eficiência, poderíamos calcular os vetores aproveitando as fórmulas de Cinemática. Adicionalmente, a função ```trigsimp``` foi utilizada em cada elementos dos vetores colunas resultantes para simplificá-los. Então, segue o resultado.

```python
from sympy.physics.vector import time_derivative, vlatex
from sympy import trigsimp

# Cinemática do ponto A em relação ao referencial B0
r_A  = r_OA.express(B0)
v_A = time_derivative(r_A, B0)
a_A = time_derivative(v_A, B0)

# Cinemática do ponto B em relação ao referencial B0
r_B  = r_A.express(B0) + r_AB.express(B0)
v_B = time_derivative(r_B, B0)
a_B = time_derivative(v_B, B0)

# Cinemática do ponto C em relação ao referencial B0
r_C  = r_B.express(B0) + r_BC.express(B0)
v_C = (time_derivative(r_C, B0))
a_C = (time_derivative(v_C, B0))

# Simplifcação dos Resultados
r_A = (r_A.to_matrix(B0)).applyfunc(trigsimp)
v_A = (v_A.to_matrix(B0)).applyfunc(trigsimp)
a_A = (a_A.to_matrix(B0)).applyfunc(trigsimp)
r_B = (r_B.to_matrix(B0)).applyfunc(trigsimp)
v_B = (v_B.to_matrix(B0)).applyfunc(trigsimp)
a_B = (a_B.to_matrix(B0)).applyfunc(trigsimp)
r_C = (r_C.to_matrix(B0)).applyfunc(trigsimp)
v_C = (v_C.to_matrix(B0)).applyfunc(trigsimp)
a_C = (a_C.to_matrix(B0)).applyfunc(trigsimp)
```

```python
# Resultados em LaTeX na forma de Vetores Coluna
Latex("\\begin{align}"
     "r_{C}=&" + str(vlatex(r_C)) + "\\\\" + "\\\\"
     "v_{C}=&" + str(vlatex(v_C)) + "\\\\" + "\\\\"
     "a_{C}=& " + str(vlatex(a_C)) + "\\end{align}")
```

\begin{align}
_{C}=&\left[\begin{matrix}\left(l_{1} \operatorname{cos}\left(\theta_{2}\right) + l_{2} \operatorname{cos}\left(\theta_{2} + \theta_{3}\right)\right) \operatorname{cos}\left(\theta_{1}\right)\\l_{1} \operatorname{sin}\left(\theta_{2}\right) + l_{2} \operatorname{sin}\left(\theta_{2} + \theta_{3}\right)\\- \left(l_{1} \operatorname{cos}\left(\theta_{2}\right) + l_{2} \operatorname{cos}\left(\theta_{2} + \theta_{3}\right)\right) \operatorname{sin}\left(\theta_{1}\right)\end{matrix}\right]\\\\v_{C}=&\left[\begin{matrix}- l_{1} \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2}\right) \dot{\theta}_{1} - l_{1} \operatorname{sin}\left(\theta_{2}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{2} - l_{2} \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{2} - l_{2} \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{3} - l_{2} \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \dot{\theta}_{1}\\l_{1} \operatorname{cos}\left(\theta_{2}\right) \dot{\theta}_{2} + l_{2} \left(\dot{\theta}_{2} + \dot{\theta}_{3}\right) \operatorname{cos}\left(\theta_{2} + \theta_{3}\right)\\l_{1} \operatorname{sin}\left(\theta_{1}\right) \operatorname{sin}\left(\theta_{2}\right) \dot{\theta}_{2} - l_{1} \operatorname{cos}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2}\right) \dot{\theta}_{1} + l_{2} \left(\operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{sin}\left(\theta_{1}\right) \dot{\theta}_{2} + \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{sin}\left(\theta_{1}\right) \dot{\theta}_{3} - \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{1}\right)\end{matrix}\right]\\\\a_{C}=& \left[\begin{matrix}2 l_{1} \operatorname{sin}\left(\theta_{1}\right) \operatorname{sin}\left(\theta_{2}\right) \dot{\theta}_{1} \dot{\theta}_{2} - l_{1} \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2}\right) \ddot{\theta}_{1} - l_{1} \operatorname{sin}\left(\theta_{2}\right) \operatorname{cos}\left(\theta_{1}\right) \ddot{\theta}_{2} - l_{1} \operatorname{cos}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2}\right) \dot{\theta}_{1}^{2} - l_{1} \operatorname{cos}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2}\right) \dot{\theta}_{2}^{2} + l_{2} \left(2 \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{sin}\left(\theta_{1}\right) \dot{\theta}_{1} \dot{\theta}_{2} + 2 \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{sin}\left(\theta_{1}\right) \dot{\theta}_{1} \dot{\theta}_{3} - \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \ddot{\theta}_{2} - \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \ddot{\theta}_{3} - \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \ddot{\theta}_{1} - \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{1}^{2} - \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{2}^{2} - 2 \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{2} \dot{\theta}_{3} - \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{3}^{2}\right)\\- l_{1} \operatorname{sin}\left(\theta_{2}\right) \dot{\theta}_{2}^{2} + l_{1} \operatorname{cos}\left(\theta_{2}\right) \ddot{\theta}_{2} + l_{2} \left(- \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \dot{\theta}_{2}^{2} - 2 \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \dot{\theta}_{2} \dot{\theta}_{3} - \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \dot{\theta}_{3}^{2} + \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \ddot{\theta}_{2} + \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \ddot{\theta}_{3}\right)\\l_{1} \operatorname{sin}\left(\theta_{1}\right) \operatorname{sin}\left(\theta_{2}\right) \ddot{\theta}_{2} + l_{1} \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2}\right) \dot{\theta}_{1}^{2} + l_{1} \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2}\right) \dot{\theta}_{2}^{2} + 2 l_{1} \operatorname{sin}\left(\theta_{2}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{1} \dot{\theta}_{2} - l_{1} \operatorname{cos}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2}\right) \ddot{\theta}_{1} + l_{2} \left(\operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{sin}\left(\theta_{1}\right) \ddot{\theta}_{2} + \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{sin}\left(\theta_{1}\right) \ddot{\theta}_{3} + 2 \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{1} \dot{\theta}_{2} + 2 \operatorname{sin}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \dot{\theta}_{1} \dot{\theta}_{3} + \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \dot{\theta}_{1}^{2} + \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \dot{\theta}_{2}^{2} + 2 \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \dot{\theta}_{2} \dot{\theta}_{3} + \operatorname{sin}\left(\theta_{1}\right) \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \dot{\theta}_{3}^{2} - \operatorname{cos}\left(\theta_{2} + \theta_{3}\right) \operatorname{cos}\left(\theta_{1}\right) \ddot{\theta}_{1}\right)\end{matrix}\right]\end{align}


# ДЗ №1 - обучение модели линейной регресии методом градиентного спуска

Реализовать обучение модели линейной регрессии методом градиентного спуска.

В качестве подводящего упражнения в этом задании предлагается реализовать функции потерь и саму модель линейной регрессии в манере, схожей с построением модулей фреймворка pytorch (см. пояснения в шаблонах кода)

В решении ожидается наличие следующих ключевых составляющих:

#### Текстовое описание в решении:
- формулировка задачи, формулировка признакового описания объектов, формулировка функции ошибки, формулировка меры качества модели; +
- исследование исходных данных на предмет скоррелированности признаков; фильтрация признаков; порождение признаков (при необходимости); +
- оценка параметров модели линейной регрессии (обучение модели) методом градиентного спуска; +
- оценка качества модели на валидационной выборке.+

#### Код решения:
(используйте предлагаемые шаблоны)
- формулировка модели линейной регрессии; +
- формулировка функции ошибки; +
- формулировка метрики (метрик); +
- формулировка цикла оптимизации параметров.+

#### Визуализация в решении:
- распределение признаков; +
- распределение целевой переменной; +
- эволюция функции ошибки и выбранных метрик качества по ходу обучения. +

#### Выводы (в форме текста!)
- вывод о том, насколько модель подходит для описания данных +
- вывод о достаточности или избыточности данных для оценки параметров модели +
- вывод о соотношении выразительности модели и ее обобщающей способности (наблюдаются ли явления переобучения или недообучения).+

Примечания:

Допустимо порождение признаков (полиномиальных, экспоненциальных, логарифмических, etc.)

Реализация линейной регрессии может быть написана только с использованием библиотеки Numpy. Решения с использованием библиотек автоматического вычисления градиентов не засчитываются.

Из готовых реализаций (напр., из пакета scikit-learn) в этом задании допускается использовать только порождение полиномиальных признаков `PolynomialFeatures`.

## Примечание на предмет реализации градиента функции потерь

Нелишним будет вспомнить способ вычисления градиента сложной функции. Здесь функция ошибки (обозначено как $\mathscr{L}$) представлена как сложная функция $\mathscr{L}\left( G\left( \theta \right) \right)$. Для простоты приведена сразу матричная запись.

$$
\nabla_{\theta}{\mathscr{L}} = \nabla_{\theta}{G}\cdot\left(\nabla_{G}{\mathscr{L}}\right)
$$

В качестве шпаргалки можно подсмотреть правила матричного дифференцирования здесь

Например, в случае функции потерь MSE это может выглядеть следующим образом:

$$
\nabla_{\theta}{\mathscr{L}} = X^T\cdot2\left(X\theta - Y\right)
$$

При этом логически имеет смысл реализовать компоненту градиента, относящуюся собственно к функции потерь $\nabla_{G}{\mathscr{L}}$ - в коде класса функции потерь, а компоненту, относящуюся к модели $\nabla_{\theta}{G}$ - в коде модели.

Именно поэтому классы `loss` и `linear_regression` в предложенном шаблоне реализованы наследующими `Differentiable` - для общности восприятия этих модулей как дифференцируемых по своим аргументам.

Решение задачи

## Описание задачи

В данной задаче требуется реализовать алгоритм линейной регрессии для обучения с учителем, с оптимизацией функции ошибки методом градиентного спуска. По условию предоставлена некоторая выборка объектов, происхождение которых мне неизвестно. Соответственно под $X$ будем понимать матрицу с признаковым описанием объектов, а под $y$ - вектор-столбец, состоящих из того, что нам надо предсказать на этих объектах. Так как задача регрессии, то соответственно $y$ - это вектор действительных чисел.

Алгоритм линейной регресси в матричном виде представляется тогда как:
$$
a(X) = Xw,
$$
где $w$ - вектор-столбец весов, a $a(X)$ - ответ алгоритма (вектор-столбец) на матрице объектов $X$.

Если перемножить, то в развернутом виде получается вот такая набор выражений:
$$
\begin{equation}
    \begin{cases}
    \sum\limits_{i=1}^n x_i^{1} w_i = y_{pred}^{1}
    \\
    \sum\limits_{i=1}^n x_i^{2} w_i = y_{pred}^{2}
    \\
    ...
    \\
    \sum\limits_{i=1}^n x_i^{m} w_i = y_{pred}^{m}
    \end{cases}
\end{equation}
$$

Теперь, когда мы знаем, как нам предсказывать ответ на объектах, нам надо понять, а хорошо ли мы вообще это делаем. Для этого вводится функция ошибки $L^i(y^i, y^i_{pred})$, которая принимает истинный ответ и нами предсказанный на i-ом объекте, и возвращает некоторое число. В задаче регрессии, как правило, чем это число меньше, тем лучше. Так как обычно объектов больше, чем один и лучший по близости ответ мы хотим получать сразу на всех объектах, то введем функционал ошибки $L(y, y_{pred}) = \sum\limits^m_{i=1} L^i(y^i, y^i_{pred})$
Предполагая, что ответы на алгоритме имеют распределение $N(0, 1)$, а также, что объекты выборки независимо одинаково распределенные тогда метод максимального правдоподобия говорит, что за функционал ошибки стоить взять $MSE$ (mean squared error), который выглядит, как
$$
\frac{1}{m} \sum\limits_{i=1}^{m} (y^i - y^i_{pred})^2
$$
Таким образом,  задача свелась к минимизации выпуклой функции. Очевидно, что искать нам надо минимум по вектору весов $w$, а сам процесс оптимизации мы будем делать методом градиентного спуска, как было заявлено выше.

После того, как мы разделим данные на `train/test`, на train-данных мы будем минимизировать $MSE$, а затем проверять итоговое качество на тестовых данных, оценивая также $MSE$, но уже как `метрику`.

## Описание метода градиентного спуска

Так как направление вектора градиента совпадает с направлением наискорейшего возрастания функции, то тогда можно итеративно уменьшать вектор весов следующим образом:
$$
w^{n+1} = w^{n} - \alpha\nabla_{w}{L}
,$$
где $\alpha$ - learning rate, в простом случае какая-то константа, которая отражает скорость движения в направлении градиента.

В нашем случае с $MSE$ градиент можно найти следующим образом: $X^T\cdot2\left(Xw - y\right)$

```python
import numpy as np
import pandas as pd
from abc import ABC, abstractmethod
from tqdm import tqdm

%matplotlib inline
import seaborn as sns
import matplotlib.pylab as plt
plt.style.use('ggplot')

from warnings import filterwarnings
filterwarnings('ignore')
```

## Предварительный анализ данных

```python
X = np.load('./X_train.npy')
y = np.load('./y_train.npy')
```

Для начала, создадим таблицу из фичей и целевых значений и немножко посмотрим, какого рода данные достались

```python
from itertools import chain

columns = chain((f"feature_{i}" for i in range(1, X.shape[1] + 1)), ('target',))

data = pd.DataFrame(
    np.concatenate((X, y), axis=1),
    columns=columns
)
data.head()
```

      feature_1
      feature_2
      feature_3
      feature_4
      target

      0
      -1.379859
      -1.639242
      1.904012
      -2.627268
      2.468626

      1
      -5.411598
      -5.495438
      29.285391
      -158.480757
      9.998146

      2
      -4.299786
      -4.594803
      18.488158
      -79.495119
      9.279000

      3
      -6.005137
      -5.921348
      36.061665
      -216.555224
      8.634806

      4
      -6.420339
      -6.364172
      41.220748
      -264.651158
      9.567111

```python
data.shape
```

    (10000, 5)

```python
data.describe()
```

      feature_1
      feature_2
      feature_3
      feature_4
      target

      count
      10000.000000
      10000.000000
      10000.000000
      10000.000000
      10000.000000

      mean
      0.023107
      0.022998
      28.835876
      1.692730
      14.527903

      std
      5.370123
      5.375967
      20.666320
      266.036383
      18.980914

      min
      -12.479018
      -12.515073
      0.000007
      -1943.306171
      -57.872737

      25%
      -4.966595
      -4.963999
      13.170098
      -122.511325
      5.350223

      50%
      -0.029218
      -0.036860
      24.895115
      -0.000025
      8.754593

      75%
      5.033995
      5.024538
      40.093336
      127.567014
      15.689532

      max
      11.898036
      11.955627
      155.725889
      1684.324907
      193.520096

Можно заметить, что `feature_1` и `feature_2` довольно сильно похожи по своим описательным статистикам. В дальнейшем, посмотрим на это еще с точки зрения корреляции.

```python
data.dtypes
```

    feature_1    float64
    feature_2    float64
    feature_3    float64
    feature_4    float64
    target       float64
    dtype: object

```python
 data.isnull().sum()
```

    feature_1    0
    feature_2    0
    feature_3    0
    feature_4    0
    target       0
    dtype: int64

Как видно, все признаки числовые, в данных пропусков нет.

Построим теперь матрицу корреляций Пирсона

```python
def calc_corr(feature_1, feature_2):
    # подсчет среднего
    mean_1 = np.mean(feature_1)
    mean_2 = np.mean(feature_2)

    # подсчет дисперсии
    var_1 = np.mean((feature_1 - mean_1)**2)
    var_2 = np.mean((feature_2 - mean_2)**2)

    # подсчет ковариации
    cov_1_2 = np.mean((feature_1 - mean_1) * (feature_2 - mean_2))

    return cov_1_2 / np.sqrt(var_1 * var_2)

def get_correlation_table(dataset):
    corr_matrix = pd.DataFrame(index=dataset.columns, columns=dataset.columns, dtype=np.float64)
    np.fill_diagonal(corr_matrix.values, 1.0)

    for column_1 in dataset.columns:
        for column_2 in dataset.columns:
            corr_matrix.loc[column_2, column_1] = calc_corr(dataset[column_1], dataset[column_2])
            corr_matrix.loc[column_1, column_2] = calc_corr(dataset[column_1], dataset[column_2])

    return corr_matrix
```

```python
corr_table =  get_correlation_table(data)
sns.heatmap(
    corr_table,
    annot=True,
);
```

Как видно из матрицы корреляций, `feature_1` и `feature_2` буквально идентичные. Скорее всего, данные были просто ошибочно продублированы. Следует удалить одну из этих фичей, так как в противном случае мультиколлинеарность - прямая дорога к переобучению.

```python
data = data.drop(labels='feature_1', axis=1)
data.head()
```

      feature_2
      feature_3
      feature_4
      target

      0
      -1.639242
      1.904012
      -2.627268
      2.468626

      1
      -5.495438
      29.285391
      -158.480757
      9.998146

      2
      -4.594803
      18.488158
      -79.495119
      9.279000

      3
      -5.921348
      36.061665
      -216.555224
      8.634806

      4
      -6.364172
      41.220748
      -264.651158
      9.567111

Посмотрим далее графически на совместные зависимости данных, а также на распределения

```python
sns.heatmap(data.corr(method='spearman'), annot=True);
```

По корреляции Спирмена, `feature_2` и `feature_4` также сильно скоррелированы сильно, поэтому также удалим `feature_4`

```python
data = data.drop('feature_4', axis=1)
```

```python
sns.pairplot(
    data,
);
```

```python
data['target'].plot(kind='hist', log=True);
```

Видно, что `feature_2` и `feature_3` связаны квадратично, поэтому генерить квадраты не стоит, так как это приведет опять же к мультиколлинеарности. Можно попробовать сгененерировать куб `feature_2`.

## Подготовка данных к обучению и тесту

Далее разобьем данные на `train/test` (80% оставим на обучение, 20% - на тест). Также отнормируем их, причем фитить scaler будем только на train'e, чтобы не произошло утечки в test

```python
# аналог train_test_split
def make_train_test(X, y, train_size=0.8):
    perm = np.random.permutation(range(len(X)))
    train_indx = perm[:int(train_size * len(X))]
    test_indx = perm[int(train_size * len(X)):]

    return (
        X[train_indx], X[test_indx],
        y[train_indx], y[test_indx],
    )

# приводим данные к стандартной форме (среднее == 0 && дисперсия == 1)
def scale_data(X_train, X_test, y_train, y_test):
    X_mean = np.mean(X_train, axis=0)
    y_mean = np.mean(y_train, axis=0)
    X_std = np.std(X_train, axis=0)
    y_std = np.std(y_train, axis=0)

    X_train = (X_train - X_mean) / X_std
    X_test = (X_test - X_mean) / X_std
    y_train = (y_train - y_mean) / y_std
    y_test = (y_test - y_mean) / y_std

    return (
        X_train, X_test,
        y_train, y_test,
    )

# объединение всей подготовки данных в одную функцию
def prepare_data(X, y):
    X_train, X_test, y_train, y_test = make_train_test(X, y)
    X_train, X_test, y_train, y_test = scale_data(X_train, X_test, y_train, y_test)

    return (
        X_train, X_test,
        y_train, y_test,
    )
```

## Создание модели

1) класс `Differentiable`: это абстракный базовый класс для всех последующих моделей, связанных с регрессией

2) класс `Loss`: данный класс описывает функционал потерь. У него есть методы для подсчета части градиента, которая относится к внешней части лосса, а также метод, возвращающий само значение лосса. В даанном конкретном случае - это **MSE**

3) класс `LinearRegression`: класс описывает модель линейной регрессии. Есть метод для подсчета части градиента, относящейся к внутренней части функционала лосса, а также метод для подсчета самого значения линейной регрессии (Xw)

4) функция `train_loop`: принимает исходные данные, модель, функционал лосса, количество эпох и learning_rate. Итеративно (количество повторений == количеству эпох). На каждой итерации мы делаем шаг градиентного спуска и считаем лосс. Функция возвращает list из значений функции ошибки на каждом шаге.

```python
class Differentiable(ABC):
    def __init__(self):
        pass

    @abstractmethod
    def forward(self, **kwargs):
        pass

    @abstractmethod
    def backward(self, **kwargs):
        pass
```

```python
class Loss(Differentiable):
    def __init__(self):
        super(Loss, self).__init__()

    def forward(self, y_pred, y_true):
        # Этот метод реализует вычисление значения функции потерь
        # Подсказка: метод должен возвращать единственный скаляр - значение функции потерь
        assert y_pred.shape == y_true.shape, f'y_true shape != y_pred shape ({y_true.shape} != {y_pred.shape})'
        n = y_pred.shape[0]

        loss_value = (1 / n) * np.sum((y_pred - y_true)**2)

        return loss_value

    def backward(self, y_pred, y_true):
        # Этот метод реализует вычисление градиента функции потерь по аргументу y_pred
        # Подсказка: метод должен возвращать вектор градиента функции потерь
        #           размерностью, совпадающей с размерностью аргумента y_pred

        partial_grad = np.zeros_like(y_pred-y_true)
        n = y_pred.shape[0]

        partial_grad = (2 / n) * (y_pred - y_true)

        assert y_pred.shape == partial_grad.shape, f'y_pred shape != partial_grad shape ({y_pred.shape} != {partial_grad.shape})'
        return partial_grad
```

```python
class LinearRegression(Differentiable):
    def __init__(self, fit_intercept=False):
        super(LinearRegression, self).__init__()
        self.theta = None
        self.fit_intercept = fit_intercept

    def __call__(self, X):
        # этот метод предназначен для вычисления значения целевой переменной
        return self.forward(X)

    def backward(self, X):
        # Этот метод реализует вычисление компоненты градиента функции потерь

        partial_grad = X.T

        return partial_grad

    def forward(self, X):
        # этот метод предназначен для применения модели к данным
        assert X.ndim == 2, "X should be 2-dimensional: (N of objects, n of features)"

        if self.theta is None:
            # Если вектор параметров еще не инициализирован, его следует инициализировать
            # Подсказка: длина вектора параметров может быть получена из размера матрицы X
            self.theta = np.zeros((X.shape[1], 1))

        X = np.concatenate((X, np.ones(X.shape[0], 1)), axis=1) if self.fit_intercept else X
        y_pred = np.dot(X, self.theta)

        return y_pred
```

```python
def train_loop(X_train, y_train, model, loss_fn, epochs=100, learning_rate=0.05):
    loss_history = []
    pbar = tqdm(total=epochs)
    for epoch in range(epochs):
        # В этом цикле следует реализовать итеративную процедуру оптимизации параметров модели model,
        #        руководствуясь функцией потерь loss_fn

        y_pred = model.forward(X_train)
        loss_value = loss_fn.forward(y_pred, y_train)  # вычисление значения лосса
        grad = np.dot(model.backward(X_train), loss_fn.backward(y_pred, y_train))  # вычисление градиента

        model.theta -= learning_rate * grad  # шаг градиетного спуска

        loss_history.append(loss_value)
        pbar.update(1)
        pbar.set_postfix({'loss': loss_value})
    pbar.close()
    return loss_history
```

## Процесс обучения

```python
X_train, X_test, y_train, y_test = prepare_data(X, y)
```

```python
obj_fn = Loss()
lr_model = LinearRegression()
loss_history = train_loop(X_train, y_train, lr_model, obj_fn, epochs=1000);
```

    100%|██████████| 1000/1000 [00:01<00:00, 918.61it/s, loss=0.00137]

```python
print(loss_history[-1])
```

    0.0013678957342207812

```python
plt.plot(loss_history);
```

## Оценка работы модели

```python
from sklearn.metrics import mean_squared_error

y_pred = lr_model.forward(X_test)

print(f"MSE on test_data: {obj_fn.forward(y_pred, y_test)}")
```

    MSE on test_data: 0.0015037588977660668

## Порождение полиномиальных признаков

```python
from sklearn.preprocessing import PolynomialFeatures

def make_poly_features(X, degree=2):
    poly_transformer = PolynomialFeatures(degree=degree)

    return poly_transformer.fit_transform(X)

feature_2_triple = data['feature_2'] ** 3
data_poly = pd.concat([data, feature_2_triple], axis=1)
X_poly = data_poly.drop('target', axis=1)
y_poly = data_poly['target']
```

```python
X_poly.columns = ['feature_2', 'feature_3', 'feature_2_triple']
```

```python
X_poly_train, X_poly_test, y_poly_train, y_poly_test = prepare_data(X_poly.values, y_poly.values)
```

## ВЫВОД
в целом, то что у меня получилось, работает довольно  неплохо, поэтому линейная регрессия должна подходить для подобной задачи

По моим результатам переобучения не наблюдается, избыточность данных очевидно есть, в процессе описания решения задачи, я пытался от нее избавиться по средствам удаления лишиних фичей, к примеру.


# Lecture 21

## Covariance, Correlation, Variance of a sum, Variance of Binomial & Hypergeometric
----

### Definition

Covariance of any 2 random variables $X, Y$ is defined as

\begin{align}
  Cov(X, Y) &= \mathbb{E}\left( (X - \mathbb{E}(X)) (Y - \mathbb{E}(Y)) \right) \\
  &= \mathbb{E}(XY) - \mathbb{E}(X) \, \mathbb{E}(Y) & \quad \text{similar to definition of variance}
\end{align}

Covariance is a measure of how $X, Y$ might vary _in tandem_.

If the product of $(X - \mathbb{E}(X))$ and $(Y - \mathbb{E}(Y))$ is _positive_ then that means that both values are either _positive_ ($X,Y$ tend to be greater than their respective means); or they are both _negative_ ($X,Y$ tend to be less than their means).

Correlation is defined in terms of covariance, as you will see in a bit.

### Properties

\begin{align}
  & \text{[1]} & Cov(X,X) &= Var(X) \\
  \\
  & \text{[2]} & Cov(X,Y) &= Cov(Y,X) \\
  \\
  & \text{[3]} & Cov(X, c) &= 0 & \quad \text{for some constant }c \\
  \\
  & \text{[4]} & Cov(cX, Y) &= c \, Cov(X,Y) & \quad \text{bilinearity} \\
  \\
  & \text{[5]} & Cov(X, Y+Z) &= Cov(X,Y) + Cov(X,Z) & \quad \text{bilinearity} \\
  \\
  & \text{[6]} & Cov(X+Y, Z+W) &= Cov(X,Z) + Cov(X,W) + Cov(Y,Z) + Cov(Y,W) & \quad \text{applying (5)} \\
  \\
  & \Rightarrow & Cov \left( \sum_{i=1}^{m} a_{i} \, X_{i},  \sum_{j=1}^{n} b_{j} \, Y_{j} \right) &= \sum_{i,j} a_{i} \, b_{j} \, Cov \left( X_{i}, Y_{j} \right) \\
  \\
  & \text{[7]} & Var(X_1 + X_2) &=  Cov(X_1+X_2, X_1+X_2) \\
  & & &= Cov(X_1, X_1) + Cov(X_1, X_2) + Cov(X_2, X_1) + Cov(X_2, X_2) \\
  & & &= Var(X_1) + Var(X_2) + 2 \, Cov(X_1, X_2) \\
  \\
  & \Rightarrow & Var(X_1 + X_2) &= Var(X_1) + Var(X_2) & \quad \text{if }X_1, X_2 \text{ are uncorrelated} \\
  \\
  & \Rightarrow & Var(X_1 + \dots + X_n) &= Var(X_1) + \dots + Var(X_n) + 2 \, \sum_{i \lt j} Cov(X_i, X_j) \\
\end{align}

#### Bilinearity

If you imagine treating one variable as fixed and then start working with the other, it sort of looks like _linearity_. It also sort of looks like the distributive property, too.

Bilinearity lets us

\begin{align}
  Cov(\lambda X, Y) &= \lambda \, Cov(X, Y) = Cov(X, \lambda Y) & \quad \text{freely move scaling factors} \\
  \\
  Cov(X, Y_1 + Y_2) &= Cov(X, Y_1) + Cov(X, Y_2) \\
  Cov(X_1 + X_2, Y) &= Cov(X_1, Y) + Cov(X_2, Y) & \quad \text{distribution in }Cov \text{ operator} \\
\end{align}

### Theorem If $X,Y$ are independent, then they are also uncorrelated, i.e., $Cov(X,Y) = 0$

The converse is not true, however: $Cov(X,Y) = 0$ does not necessarily mean that $X,Y$ are independent.

Consider $Z \sim \mathcal{N}(0,1)$, and $X \sim Z, Y \sim Z^2$.

\begin{align}
  Cov(X,Y) &= \mathbb{E}(XY) - \mathbb{E}(X)\,\mathbb{E}(Y) \\
  &= \mathbb{E}(Z^3) - \mathbb{E}(Z) \, \mathbb{E}(Z^2) \\
  &= 0 - 0 & \quad \text{odd moments of }Z \text{ are 0} \\
  &= 0
\end{align}

But given $X \sim Z$, we know *exactly* everything about $Y$. And knowing $Y$, gives us everything about $X$, _except for the sign_.

So $X,Y$ are dependent, yet their covariance (and hence correlation) is 0.

## Definition

Correlation is defined in terms of covariance.

\begin{align}
  Corr(X,Y) &= \frac{Cov(X,Y)}{\sigma_{X} \, \sigma_{Y}} = \frac{Cov(X,Y)}{\sqrt{Var(X)} \, \sqrt{Var(Y)}} \\
  \\
  &= Cov \left( \frac{X-\mathbb{E}(X)}{\sigma_{X} }, \frac{Y-\mathbb{E}(Y)}{\sigma_{Y}} \right) & \quad \text{standardize first, then find covariance} \\
\end{align}

Correlation is dimensionless, and like standard deviation, is unit-less.

Correlation also ranges from -1 to 1. This is what happens when we divide by $\sqrt{Var(X)} \, \sqrt{Var(Y)}$

### Theorem $-1 \le Corr(X,Y) \le 1$

Without loss of generality, assume that $X,Y$ are already standarized (mean 0, variance 1).

\begin{align}
  Var(X + Y) &= Var(X) + Var(Y) + 2\, Corr(X,Y) \\
  &= 1 + 1 + 2 \, \rho & \quad \text{ where } \rho = Corr(X,Y) \\
  &= 2 + 2 \, \rho \\
  0 &\le 2 + 2 \, \rho & \quad \text{since }Var \ge 0 \\
  0 &\le 1 + \rho & \Rightarrow \rho \text{ has a floor of }-1 \\
  \\
  Var(X - Y) &= Var(X) + Var(-Y) - 2\, Corr(X,Y) \\
  &= 1 + 1 - 2 \, \rho & \quad \text{ where } \rho = Corr(X,Y) \\
  &= 2 - 2 \, \rho \\
  0 &\le 2 - 2 \, \rho & \quad \text{since }Var \ge 0 \\
  0 &\le 1 - \rho & \Rightarrow \rho \text{ has a ceiling of }1 \\
  \\
  &\therefore -1 \le Corr(X,Y) \le 1 &\quad \blacksquare
\end{align}

## Cov in a Multinomial

Given $(X_1, \dots , X_k) \sim Mult(n, \vec{p})$, find $Cov(X_i, X_j)$ for all $i,j$.

\begin{align}
  \text{case 1, where } i=j \text{ ...} \\
  \\
  Cov(X_i, X_j) &= Var(X_i) \\
  &= n \, p_i \, (1 - p_i) \\
  \\
  \\
  \text{case 2, where } i \ne j \text{ ...} \\
  \\
  Var(X_i + X_j) &= Var(X_i) + Var(X_j) + 2 \, Cov(X_i, X_j) & \quad \text{property [7] above} \\
  n \, (p_i + p_j) \, (1 - (p_i + p_j)) &= n \, p_i \, (1 - p_i) + n \, p_j \, (1 - p_j) + 2 \, Cov(X_i, X_j) & \quad \text{lumping property} \\
  2 \, Cov(X_i, X_j) &= n \, (p_i + p_j) \, (1 - (p_i + p_j)) - n \, p_i \, (1 - p_i) - n \, p_j \, (1 - p_j) \\
  &= n \left( (p_i + p_j) - (p_i + p_j)^2 - p_i + p_i^2 - p_j + p_j^2  \right) \\
  &= n ( -2 \, p_i \, p_j ) \\
  Cov(X_i, X_j) &= - n \, p_i \, p_j
\end{align}

Notice how case 2 where $i \ne j$ yields $Cov(X_i, X_j) = - n \, p_i \, p_j$, which is negative. This should agree with your intuition that for $(X_1, \dots , X_k) \sim Mult(n, \vec{p})$, categories $i,j$ are competing with another, and so they cannot be positively correlated nor independent of each other.

## Variance of a Binomial

Applying what we now know of covariance, we can obtain the variance of $X \sim Bin(n, p)$.

We can describe $X = X_1 + \dots + X_n$ where $X_i$ are i.i.d. $Bern(p)$.

Now consider _indicator random variables_. Let $I_A$ be the indicator of event $A$.

\begin{align}
  I_A &\in \{0, 1\} \\
  \\
  \Rightarrow I_A^2 &= I_A \\
  I_A^3 &= I_A \\
  & \text{...} \\
  \\
  \Rightarrow I_A \, I_B &= I_{A \cap B} & \quad \text{for another event } B \\
  \\
  Var(X_i) &=  \mathbb{E}X_i^2 - ( \mathbb{E}X_i)^2 \\
  &= p_i - p_i^2 \\
  &= p_i \, (1 - p_i) \\
  &= p \, q & \quad \text{variance of Bernoulli} \\
  \\
  \Rightarrow Var(X) &= Var(X_i + \dots + X_n) \\
  &= Var(X_i) + \dots + Var(X_n) + 2 \, \left( \sum_{i \ne j} Cov(X_i, X_j) \right) \\
  &= n \, p \, q + 2 \, (0) & \quad Cov(X_i,X_j) = 0 \text{ since } X_i \text{ are i.i.d.} \\
  &= n \, p \, q
\end{align}

## Variance of Hypergeometric

Given $X \sim HGeom(w, b, n)$ where

\begin{align}
  X &= X_1 + \dots + X_n \\
  \\
  X_j &=
  \begin{cases}
    1, &\text{ if }j^{th} \text{ ball is White}  \\
    0, &\text{ otherwise } \\
  \end{cases} \\
\end{align}

Recall that the balls are sampled _without replacement_, so the draws are not independent.

However, for any draw, it is not the case that any particular ball would prefer to be drawn for that particular iteration. So there is some symmetry we can leverage!

\begin{align}
  Var(X) &= n \, Var(X_1) + 2 \, \binom{n}{2} Cov(X_1, X_2) & \quad \text{symmetry!} \\
  \\
  \text{now } Cov(X_1, X_2) &= \mathbb{E}(X_1 X_2) - \mathbb{E}(X_1) \, \mathbb{E}(X_2) \\
  &= \frac{w}{w+b} \, \frac{w-1}{w+b-1} - \left( \frac{w}{w+b} \right)^2 & \quad \text{recall } I_A \, I_B = I_{A \cap B} \text{; and symmetry} \\
\end{align}

Prof. Blitzstein runs out of time, but the rest is just algebra. _To be concluded in the next lecture._

----

# Appendix: Variance, Covariance and Linear Algebra

Let's relate what we know about variance, covariance and correlation with the concept of the [dot product](https://www.mathsisfun.com/algebra/vectors-dot-product.html) in linear algebra:

\begin{align}
  Cov(X, Y) &= X \cdot Y \\
            &= x_1 \, y_1 + \dots + x_n \, y_n \\
  \\
  Var(X) &= Cov(X, X) \\
         &= X \cdot X \\
         &= x_1 \, x_1 + \dots + x_n \, x_n \\
         &= |X|^2 \\
  \\
  \Rightarrow |X| &=  \sigma_{X} \\
                  &= \sqrt{Var(X)} \\
                  &= \sqrt{x_1 \, x_1 + \dots + x_n \, x_n} & \quad \text{the size of }\vec{X} \text{ is its std. deviation} \\
\end{align}

Now let's take this a bit further...

\begin{align}
  X \cdot Y &= |X| \, |Y| \, cos \theta \\
  \\
  \Rightarrow cos \theta &= \frac{X \cdot Y}{|X| \, |Y|} \\
                         &= \frac{Cov(X, Y)}{\sigma_{X} \, \sigma_{Y}} \\
                         &= \rho & \quad \text{... otherwise known as }Corr(X,Y) \\
\end{align}

Since $\rho_{X,Y} = cos_{X,Y} \theta$, we can see that the correlation of $X,Y$ is also the factor that projects vector $X$ onto $Y$, or vice versa. Now you can see why we can use cosine similarity as a metric to measure how close 2 vectors $X,Y$ are, since it is $Corr(X,Y)$.


# Trading Lists and Trading Trajectories

### Introduction

[Almgren and Chriss](https://cims.nyu.edu/~almgren/papers/optliq.pdf) provided a solution to the optimal liquidation problem by assuming the that stock prices follow a discrete arithmetic random walk, and that the permanent and temporary market impact functions are linear functions of the trading rate.

Almgren and Chriss showed that for each value of risk aversion there is a unique optimal execution strategy. This optimal execution strategy is determined by a trading trajectory and its associated trading list. The optimal trading trajectory is given by:

\begin{equation}
x_j = \frac{\sinh \left( \kappa \left( T-t_j\right)\right)}{ \sinh (\kappa T)}X, \hspace{1cm}\text{ for } j=0,...,N
\end{equation}

and the associated trading list is given by:

\begin{equation}
n_j = \frac{2 \sinh \left(\frac{1}{2} \kappa \tau \right)}{ \sinh \left(\kappa T\right) } \cosh \left(\kappa \left(T - t_{j-\frac{1}{2}}\right)\right) X, \hspace{1cm}\text{ for } j=1,...,N
\end{equation}

where $t_{j-1/2} = (j-\frac{1}{2}) \tau$.

Given some initial parameters, such as the number of shares, the liquidation time, the trader's risk aversion, etc..., the trading list will tell us how many shares we should sell at each trade to minimize our transaction costs.

In this notebook, we will see how the trading list varies according to some initial trading parameters.

## Visualizing Trading Lists and Trading Trajectories

Let's assume we have 1,000,000 shares that we wish to liquidate. In the code below, we will plot the optimal trading trajectory and its associated trading list for different trading parameters, such as trader's risk aversion, number of trades, and liquidation time.

```python
%matplotlib inline

import matplotlib.pyplot as plt

import utils

# We set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Set the number of days to sell all shares (i.e. the liquidation time)
l_time = 60

# Set the number of trades
n_trades = 60

# Set the trader's risk aversion
t_risk = 1e-6

# Plot the trading list and trading trajectory. If show_trl = True, the data frame containing the values of the
# trading list and trading trajectory is printed
utils.plot_trade_list(lq_time = l_time, nm_trades = n_trades, tr_risk = t_risk, show_trl = True)
```

```python
# low risk aversion
utils.plot_trade_list(lq_time = l_time, nm_trades = n_trades, tr_risk = 1e-8, show_trl = False)
```

```python
# high risk aversion: few trades as early as posible
utils.plot_trade_list(lq_time = l_time, nm_trades = n_trades, tr_risk = 1e-4, show_trl = False)
```

# Implementing a Trading List

Once we have the trading list for a given set of initial parameters, we can actually implement it. That is, we can sell our shares in the stock market according to the trading list and see how much money we made or lost. To do this, we are going to simulate the stock market with a simple trading environment. This simulated trading environment uses the same price dynamics and market impact functions as the Almgren and Chriss model. That is, stock price movements evolve according to a discrete arithmetic random walk and the permanent and temporary market impact functions are linear functions of the trading rate. We are going to use the same environment to train our Deep Reinforcement Learning algorithm later on.

We will describe the details of the trading environment in another notebook, for now we will just take a look at its default parameters. We will distinguish between financial parameters, such the annual volatility in stock price, and the parameters needed to calculate the trade list using the Almgren and Criss model, such as the trader's risk aversion.

```python
import utils

# Get the default financial and AC Model parameters
financial_params, ac_params = utils.get_env_param()
```

### Default Financial Parameters

```python
type(financial_params)
```

    statsmodels.iolib.table.SimpleTable

```python
financial_params
```

Financial Parameters

  Annual Volatility:	  12%	   Bid-Ask Spread:    	     0.125

  Daily Volatility:	  0.8%	   Daily Trading Volume:	 5,000,000

### Parameters for the Almgren and Chriss Model

```python
ac_params
```

Almgren and Chriss Model Parameters

  Total Number of Shares to Sell:	                  1,000,000	   Fixed Cost of Selling per Share:	 $0.062

  Starting Price per Share:	                         $50.00	     Trader's Risk Aversion:	           1e-06

  Price Impact for Each 1% of Daily Volume Traded:	 $2.5e-06	    Permanent Impact Constant:	       2.5e-07

  Number of Days to Sell All the Shares:	              60	       Single Step Variance:	             0.144

  Number of Trades:	                                   60	       Time Interval between trades:	      1.0

The code below implements the trading list resulting from different trading parameters, such as trader's risk aversion, number of trades, and liquidation time. All other parameters, such as total number shares to sell, are taken from the simulated trading environment (see above).

```python
%matplotlib inline

import matplotlib.pyplot as plt

import utils

# We set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Set the random seed
sd = 0

# Set the number of days to sell all shares (i.e. the liquidation time)
l_time = 60

# Set the number of trades
n_trades = 60

# Set the trader's risk aversion
t_risk = 1e-6

# Implement the trading list for the given parameters
utils.implement_trade_list(seed = sd, lq_time = l_time, nm_trades = n_trades, tr_risk = t_risk)
```

```python
# low risk aversion

utils.implement_trade_list(seed = sd, lq_time = l_time, nm_trades = n_trades, tr_risk = 1e-8)
```

```python
# high risk aversion: few trades as early as possible

utils.implement_trade_list(seed = sd, lq_time = l_time, nm_trades = n_trades, tr_risk = 1e-4)
```

Expectation shortfall should be computed as an expectation over many simulations

```python

```