# 线性代数

线性代数是许多学科的基本工具,而SymPy也提供了对它的支持.主要包括:

+ 矩阵运算

+ 张量运算

```python
from sympy import init_printing
init_printing(use_unicode=True)
```

## 矩阵运算

矩阵运算已经是现在科学计算的最常见形式了,算法向量化,利用gpu进行高效的矩阵运算也是现今单机最高效的计算方式,sympy中有类型Matrix用来定义矩阵

```python
from sympy import Matrix
```

```python
Matrix([[1, -1], [3, 4], [0, 2]])
```

$\displaystyle \left[\begin{matrix}1 & -1\\3 & 4\\0 & 2\end{matrix}\right]$

单列矩阵默认是竖置的(注意于numpy不同)

```python
Matrix([1, 2, 3])
```

$\displaystyle \left[\begin{matrix}1\\2\\3\end{matrix}\right]$

### 内置矩阵构造函数

> 单位矩阵 `eye(n)`

```python
from sympy import eye
```

```python
eye(4)
```

$\displaystyle \left[\begin{matrix}1 & 0 & 0 & 0\\0 & 1 & 0 & 0\\0 & 0 & 1 & 0\\0 & 0 & 0 & 1\end{matrix}\right]$

> 全零矩阵 `zeros(m,n)`

```python
from sympy import zeros
```

```python
zeros(2,3)
```

$\displaystyle \left[\begin{matrix}0 & 0 & 0\\0 & 0 & 0\end{matrix}\right]$

> 全1矩阵 `ones(m,n)`

```python
from sympy import ones
```

```python
ones(3, 2)
```

$\displaystyle \left[\begin{matrix}1 & 1\\1 & 1\\1 & 1\end{matrix}\right]$

### 对角矩阵`diag(eles...)`

```python
from sympy import diag
```

```python
diag(1, 2, 3)
```

$\displaystyle \left[\begin{matrix}1 & 0 & 0\\0 & 2 & 0\\0 & 0 & 3\end{matrix}\right]$

结合上面的构造函数,我们可以构造比较复杂的矩阵

```python
diag(-1, ones(2, 2), Matrix([5, 7, 5]))
```

$\displaystyle \left[\begin{matrix}-1 & 0 & 0 & 0\\0 & 1 & 1 & 0\\0 & 1 & 1 & 0\\0 & 0 & 0 & 5\\0 & 0 & 0 & 7\\0 & 0 & 0 & 5\end{matrix}\right]$

### 矩阵的基本属性和操作

```python
M = Matrix([[1, 2, 3], [3, 2, 1]])
N = Matrix([0, 1, 1])
```

> 查看矩阵的shape

```python
M.shape
```

> 查看矩阵的秩

```python
M.rank()
```

> 获取单独一行(一列)

```python
M.row(0)
```

$\displaystyle \left[\begin{matrix}1 & 2 & 3\end{matrix}\right]$

```python
M.col(-1)
```

$\displaystyle \left[\begin{matrix}3\\1\end{matrix}\right]$

> 矩阵切片

矩阵切片规则为`[纵轴,横轴]`

```python
M[:,:2]
```

$\displaystyle \left[\begin{matrix}1 & 2\\3 & 2\end{matrix}\right]$

需要注意的是matrix是可以修改的,可以用`row_del`或`col_del`方法删行删列, `row_insert`或`col_insert`添加行或者列,不过最好别这么用.

### 矩阵运算

> 矩阵加法

对应项相加

```python
M = Matrix([[1, 3], [-2, 3]])
N = Matrix([[0, 3], [0, 7]])
```

```python
M+N
```

$\displaystyle \left[\begin{matrix}1 & 6\\-2 & 10\end{matrix}\right]$

> 矩阵乘常数

```python
3*M
```

$\displaystyle \left[\begin{matrix}3 & 9\\-6 & 9\end{matrix}\right]$

> 矩阵乘向量

向量使用只有一列的矩阵表示

```python
Matrix([[1, 2, 3], [3, 2, 1]]) * Matrix([0, 1, 1])
```

$\displaystyle \left[\begin{matrix}5\\3\end{matrix}\right]$

> 矩阵乘法

```python
M*N
```

$\displaystyle \left[\begin{matrix}0 & 24\\0 & 15\end{matrix}\right]$

> 矩阵的幂

```python
M**2
```

$\displaystyle \left[\begin{matrix}-5 & 12\\-8 & 3\end{matrix}\right]$

```python
M**-1
```

$\displaystyle \left[\begin{matrix}\frac{1}{3} & - \frac{1}{3}\\\frac{2}{9} & \frac{1}{9}\end{matrix}\right]$

> 矩阵转置

```python
M = Matrix([[1, 2, 3], [4, 5, 6]])
M
```

$\displaystyle \left[\begin{matrix}1 & 2 & 3\\4 & 5 & 6\end{matrix}\right]$

```python
M.T
```

$\displaystyle \left[\begin{matrix}1 & 4\\2 & 5\\3 & 6\end{matrix}\right]$

> 求解行列式

```python
M = Matrix([[1, 0, 1], [2, -1, 3], [4, 3, 2]])
M
```

$\displaystyle \left[\begin{matrix}1 & 0 & 1\\2 & -1 & 3\\4 & 3 & 2\end{matrix}\right]$

```python
M.det()
```

> `rref()`方法找出向量组的最大无关组

通过初等行变换,找出向量组的最大无关组,对矩阵操作,转化为最简形矩阵.

```python
M = Matrix([[1, 0, 1, 3], [2, 3, 4, 7], [-1, -3, -3, -4]])
M
```

$\displaystyle \left[\begin{matrix}1 & 0 & 1 & 3\\2 & 3 & 4 & 7\\-1 & -3 & -3 & -4\end{matrix}\right]$

```python
M.rref()
```

$\displaystyle \left( \left[\begin{matrix}1 & 0 & 1 & 3\\0 & 1 & \frac{2}{3} & \frac{1}{3}\\0 & 0 & 0 & 0\end{matrix}\right], \  \left( 0, \  1\right)\right)$

> 计算零空间

定义：已知$A$为一个$m\times n$矩阵.的零空间(nullspace),又称核(kernel),是一组由下列公式定义的$n$维向量：

$$
ker(A) = {x \in C^n : Ax = 0}
$$

即线性方程组$AX=0$的所有解x的集合。

```python
M = Matrix([[1, 2, 3, 0, 0], [4, 10, 0, 0, 1]])
M
```

$\displaystyle \left[\begin{matrix}1 & 2 & 3 & 0 & 0\\4 & 10 & 0 & 0 & 1\end{matrix}\right]$

```python
M.nullspace()
```

$\displaystyle \left[ \left[\begin{matrix}-15\\6\\1\\0\\0\end{matrix}\right], \  \left[\begin{matrix}0\\0\\0\\1\\0\end{matrix}\right], \  \left[\begin{matrix}1\\- \frac{1}{2}\\0\\0\\1\end{matrix}\right]\right]$

> Columnspace列空间

列空间是指一个由矩阵的所有的列进行线性组合而形成的空间.sympy中使用`columnspace`方法计算,结果返回构成该空间的基向量

```python
M = Matrix([[1, 1, 2], [2 ,1 , 3], [3 , 1, 4]])
M
```

$\displaystyle \left[\begin{matrix}1 & 1 & 2\\2 & 1 & 3\\3 & 1 & 4\end{matrix}\right]$

```python
M.columnspace()
```

$\displaystyle \left[ \left[\begin{matrix}1\\2\\3\end{matrix}\right], \  \left[\begin{matrix}1\\1\\1\end{matrix}\right]\right]$

> 特征值eigenvals,特征向量eigenvects和对角化diagonalize

```python
M = Matrix([[3, -2,  4, -2], [5,  3, -3, -2], [5, -2,  2, -2], [5, -2, -3,  3]])
M
```

$\displaystyle \left[\begin{matrix}3 & -2 & 4 & -2\\5 & 3 & -3 & -2\\5 & -2 & 2 & -2\\5 & -2 & -3 & 3\end{matrix}\right]$

```python
M.eigenvals()
```

```python
M.eigenvects()
```

$\displaystyle \left[ \left( -2, \  1, \  \left[ \left[\begin{matrix}0\\1\\1\\1\end{matrix}\right]\right]\right), \  \left( 3, \  1, \  \left[ \left[\begin{matrix}1\\1\\1\\1\end{matrix}\right]\right]\right), \  \left( 5, \  2, \  \left[ \left[\begin{matrix}1\\1\\1\\0\end{matrix}\right], \  \left[\begin{matrix}0\\-1\\0\\1\end{matrix}\right]\right]\right)\right]$

```python
P, D = M.diagonalize()
P
```

$\displaystyle \left[\begin{matrix}0 & 1 & 1 & 0\\1 & 1 & 1 & -1\\1 & 1 & 1 & 0\\1 & 1 & 0 & 1\end{matrix}\right]$

```python
D
```

$\displaystyle \left[\begin{matrix}-2 & 0 & 0 & 0\\0 & 3 & 0 & 0\\0 & 0 & 5 & 0\\0 & 0 & 0 & 5\end{matrix}\right]$

```python
P*D*P**-1
```

$\displaystyle \left[\begin{matrix}3 & -2 & 4 & -2\\5 & 3 & -3 & -2\\5 & -2 & 2 & -2\\5 & -2 & -3 & 3\end{matrix}\right]$

```python
P*D*P**-1 == M
```

    True

## 张量计算

张量是向量的扩展,在工程上由很广泛的应用,在机器学习领域,随着深度学习的流行,张量计算也被大家广泛使用.通常标量,矢量,矩阵都是特殊的张量,0维的张量是标量,一维的张量是向量,二维的张量是矩阵.

SymPy中支持张量计算,它的最基本结构是N维数组(ARRAY)

### 数组操作

```python
from sympy import Array
```

```python
a1 = Array([[1, 2], [3, 4], [5, 6]])
a1
```

$\displaystyle \left[\begin{matrix}1 & 2\\3 & 4\\5 & 6\end{matrix}\right]$

数组和矩阵一样,有属性`shape`有`rank`

```python
a1.shape
```

```python
a1.rank()
```

同时,数组允许使用`reshape`操作改变其形状,其原理是按行拉平然后在重新切分

```python
a1.reshape(2,3)
```

$\displaystyle \left[\begin{matrix}1 & 2 & 3\\4 & 5 & 6\end{matrix}\right]$

> 广播操作

数组中的元素可以是数字,也可以是符号或者表达式,如果是表达式,那么对整个数组的操作会广播到所有元素上

```python
from sympy.abc import x, y, z
```

```python
m3 = Array([x**3, x*y, z])
m3
```

$\displaystyle \left[\begin{matrix}x^{3} & x y & z\end{matrix}\right]$

```python
m3.diff(x)
```

$\displaystyle \left[\begin{matrix}3 x^{2} & y & 0\end{matrix}\right]$

我们也可以使用接口`applyfunc()`来自己定义要广播的函数

```python
m3.applyfunc(lambda x: x/2)
```

$\displaystyle \left[\begin{matrix}\frac{x^{3}}{2} & \frac{x y}{2} & \frac{z}{2}\end{matrix}\right]$

当rank为2时我们可以使用接口`.tomatrix()`将数组对象转化为矩阵

```python
a1.tomatrix()
```

$\displaystyle \left[\begin{matrix}1 & 2\\3 & 4\\5 & 6\end{matrix}\right]$

### 张量乘法

张量乘法$ P=A⊗B $定义为:$$ P_{i_1,\ldots,i_n,j_1,\ldots,j_m} := A_{i_1,\ldots,i_n}\cdot B_{j_1,\ldots,j_m} $$

它使用接口`tensorproduct(*arrays)`来实现,

> 两个rank为1的张量相乘构造出一个rank为2的张量

```python
from sympy import Array, tensorproduct
from sympy.abc import x,y,z,t
A = Array([x, y, z, t])
B = Array([1, 2, 3, 4])
C = tensorproduct(A, B)
C
```

$\displaystyle \left[\begin{matrix}x & 2 x & 3 x & 4 x\\y & 2 y & 3 y & 4 y\\z & 2 z & 3 z & 4 z\\t & 2 t & 3 t & 4 t\end{matrix}\right]$

```python
C.rank()
```

> rank为1的张量和矩阵之间的乘积产生rank为3的张量

```python
p1 = tensorproduct(A, eye(4))
p1
```

$\displaystyle \left[\begin{matrix}\left[\begin{matrix}x & 0 & 0 & 0\\0 & x & 0 & 0\\0 & 0 & x & 0\\0 & 0 & 0 & x\end{matrix}\right] & \left[\begin{matrix}y & 0 & 0 & 0\\0 & y & 0 & 0\\0 & 0 & y & 0\\0 & 0 & 0 & y\end{matrix}\right] & \left[\begin{matrix}z & 0 & 0 & 0\\0 & z & 0 & 0\\0 & 0 & z & 0\\0 & 0 & 0 & z\end{matrix}\right] & \left[\begin{matrix}t & 0 & 0 & 0\\0 & t & 0 & 0\\0 & 0 & t & 0\\0 & 0 & 0 & t\end{matrix}\right]\end{matrix}\right]$

### 张量的切片

```python
p1[0,:,:]
```

$\displaystyle \left[\begin{matrix}x & 0 & 0 & 0\\0 & x & 0 & 0\\0 & 0 & x & 0\\0 & 0 & 0 & x\end{matrix}\right]$

### 张量收缩

张量收缩指在指定轴上求和,可以使用接口`tensorcontraction`,比如矩阵(2维张量)做收缩

$$ A_{m,n} \implies \sum_k A_{k,k} $$

```python
from sympy import tensorcontraction
```

```python
C = Array([[x, y], [z, t]])
C
```

$\displaystyle \left[\begin{matrix}x & y\\z & t\end{matrix}\right]$

```python
tensorcontraction(C, (0, 1))
```

矩阵的积相当于两个rank为2的张量求张量积后在第二和第三轴做收缩

$$ A_{m,n}\cdot B_{i,j} \implies \sum_k A_{m, k}\cdot B_{k, j} $$

```python
C = Array([[x, y], [z, t]])
C
```

$\displaystyle \left[\begin{matrix}x & y\\z & t\end{matrix}\right]$

```python
D = Array([[2, 1], [0, -1]])
D
```

$\displaystyle \left[\begin{matrix}2 & 1\\0 & -1\end{matrix}\right]$

```python
M_1 = tensorproduct(C, D)
M_1
```

$\displaystyle \left[\begin{matrix}\left[\begin{matrix}2 x & x\\0 & - x\end{matrix}\right] & \left[\begin{matrix}2 y & y\\0 & - y\end{matrix}\right]\\\left[\begin{matrix}2 z & z\\0 & - z\end{matrix}\right] & \left[\begin{matrix}2 t & t\\0 & - t\end{matrix}\right]\end{matrix}\right]$

```python
tensorcontraction(M_1,(1, 2))
```

$\displaystyle \left[\begin{matrix}2 x & x - y\\2 z & - t + z\end{matrix}\right]$

```python
C.tomatrix()*D.tomatrix()
```

$\displaystyle \left[\begin{matrix}2 x & x - y\\2 z & - t + z\end{matrix}\right]$

### 在数组上求导

如果数组上的元素都可导,那么我们也可以用`derive_by_array(func,array)`把求导操作广播到数组上

```python
from sympy import derive_by_array
from sympy.abc import x, y, z, t
from sympy import sin, exp
```

```python
derive_by_array(sin(x*y), x)
```

```python
derive_by_array(sin(x*y), [x, y, z])
```

$\displaystyle \left[\begin{matrix}y \cos{\left(x y \right)} & x \cos{\left(x y \right)} & 0\end{matrix}\right]$

以函数组成的数组为基础求导:

$$ B^{nm} := \frac{\partial A^m}{\partial x^n} $$

```python
basis = [x, y, z]
ax = derive_by_array([exp(x), sin(y*z), t], basis)
ax
```

$\displaystyle \left[\begin{matrix}e^{x} & 0 & 0\\0 & z \cos{\left(y z \right)} & 0\\0 & y \cos{\left(y z \right)} & 0\end{matrix}\right]$

再对结果做收缩

```python
tensorcontraction(ax, (0, 1))
```


# Optimización matemática

¿Qué significa [óptimo](https://dle.rae.es/%C3%B3ptimo)?

> Sumamente bueno, que no puede ser mejor

Pero el significado de "mejor" depende de la situación. Considere por ejemplo el problema de escoger la ruta que debe tomar un vehículo de reparto de carga

La "mejor" ruta pudiera ser

- la ruta más rápida
- la ruta más barata
- la ruta más directa

:::{important}

Para encontrar la mejor ruta primero debemos definir el **criterio** en que basaremos nuestra decisión

:::

En el ejemplo anterior tenemos

- la ruta de mínimo **tiempo**
- la ruta de mínimo **costo**
- la ruta de mínima **distancia**

La selección de la mejor ruta es un ejemplo de **optimización**. Más en general, la optimización es:

> El proceso de encontrar la mejor solución para un problema dentro de un conjunto de posibilidades

La optimización es un área bastante estudiada de las matemáticas y algunos problemas de optimización requieren de soluciones muy específicas

El objetivo de esta lección es entregar una revisión general a los problemas de optimización que podemos resolver usando las herramientas del módulo `scipy.optimize`

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize
```

## Modelamiento matemático de un problema

Para encontrar la mejor solución de un problema debemos primero modelarlo matemáticamente. En su forma más básica esto significa escribir el criterio de decisión como una función

> Llamamos a esta función, la **función objetivo** del problema

Para el problema del vehículo de reparto que comentamos en la introducción, si tenemos una variable $x$ que define la ruta y una función $f(x)$ que retorna el tiempo de dicha ruta, podemos escribir

$$
\min_x f(x)
$$

Supongamos ahora que el vehículo debe pasar por un cierto punto de la ciudad durante el reparto. La mejor ruta es ahora la de mínimo tiempo que además pasa por ese punto

> Llamamos **restricciones**, a las condiciones adicionales que debe cumplir la mejor solución

Incluyendo restricciones el modelo matemático de nuestro problema sería

$$
\begin{split}
\min_x &f(x)  \\
\text{sujeto a: } & g_i(x) = 0, i=1,2\ldots, I \\
& h_j(x) \leq 0, j=1,2,\ldots J
\end{split}
$$

donde

- $x \in \mathbb{R}^D$ son las **variables de decisión**
- $f : \mathbb{R}^D \to \mathbb{R}$ es la **función objetivo**
- $g_i : \mathbb{R}^D \to \mathbb{R}$ son las **restricciones de igualdad**
- $h_j : \mathbb{R}^D \to \mathbb{R}$ son las **restricciones de desigualdad**

> Optimizar es el proceso de encontrar el valor óptimo (mínimo) de la función objetivo en el espacio definido por las restricciones

:::{note}

En algunos problemas podríamos necesitar un máximo en lugar de un mínimo de la función objetivo

:::

Sin embargo considere que

$$
\max_x f(\vec x) \equiv \min_x - f(\vec x),
$$

Por lo que podemos hablar de minimización sin pérdida de generalidad

## Reconocer y clasificar problemas de optimización

Estudiando algunas características del problema podemos seleccionar más fácilmente un algoritmo  apropiado para resolverlo. Algunas preguntas guía que podemos realizar son

¿Es mi función objetivo de una variable ($D=1$) versus multi-variable ($D>1$)?

> Esto define la dimensionalidad o escala del problema

¿Existen restricciones de igualidad y/o desigualidad que debo cumplir?

> Algunos algoritmos sólo pueden resolver problemas sin restricciones

¿Es mi función objetivo lineal o no lineal con respecto a la entrada?

> Si todas las funciones son lineales entonces se pueden usar técnicas de **programación lineal**. Esto problemas son más simples que los no lineales

¿Es mi función objetivo convexa o no convexa?

> Una función no-convexa (derecha) puede tener múltiples mínimos locales. Por el contrario una función convexa (izquierda) tiene un único mínimo

¿Es mi función objetivo continua y diferenciable o no-diferenciable?

> Si la función no es continua entonces no puedo calcular su gradiente. Muchos métodos se basan en el gradiente para encontrar óptimos

## Resolviendo problemas de optimización

### Solución de fuerza bruta

La forma más "simple" de resolver un problema de optimización es la **fuerza bruta**, es decir evaluar $f(x)$, $g_i(x)$ y $h_j(x)$ exhaustivamente para todos los valores de $x$ hasta encontrar el mejor

:::{note}

Si $x$ fuera una variable continua necesitariamos discretizarla para crear una grilla de búsqueda

:::

La ventaja del método de fuerza bruta es que si la grilla es lo suficientemente fina entonces el método no confudirá un mínimo local con el mínimo global

:::{warning}

El método de fuerza bruta es ingenuo pues se vuelve enormemente ineficiente a medida que crece el rango y la dimensionalidad de $x$

:::

### Solución analítica

Si nuestra función es continua y derivable podemos intentar encontrar una solución analítica igualando su gradiente a cero, es decir

$$
\nabla f (x^*) = \begin{pmatrix} \frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, \ldots, \frac{\partial f}{\partial x_D} \end{pmatrix} = \vec 0
$$

Estas soluciones se conocen como **puntos estacionarios** de $f$, que incluyen los mínimos, máximos y puntos silla

Para comprobar que el punto estacionario es un mínimo primero calculamos la matriz Hessiana $H$ donde sus componentes son

$$
H_{ij}  = \frac{\partial^2 f}{\partial x_i \partial x_j}
$$

Luego si el Hessiano evaluado en $x^*$ es definido positivo

$$
x^T H(x^*) x > 0 ~ \forall x,
$$

entonces $x^*$ es un **mínimo**

:::{note}

Si el problema tiene restricciones podríamos incluirlas utilizando multiplicadores de Lagrange

:::

:::{warning}

En muchos casos prácticos $f(x)$ es demasiado complicada como para despejar una expresión análitica de $x$ a partir de $\nabla f (x^*)=0$

:::

### Solución iterativa: Método de Newton y Gradiente Descedente

Si evaluar el espacio de soluciones completo es infactible podríamos intentar explorarlo de forma parcial con un método iterativo

Los métodos iterativos parten de una o varias "soluciones iniciales" y luego aplican criterios que las "mejoran" paso a paso, acercándolas a la solución óptima

Sea $x_t$ el valor actual de la variable de decisión. Podemos escribir el valor que tendrá en el siguiente paso como

$$
x_{t+1} = x_t + \Delta x
$$

y lo que queremos es encontrar el mejor $\Delta x$ según nuestra función objetivo $f(x)$. Para continuar consideremos la aproximación de Taylor de segundo orden de $f$ en torno a $x_{t+1}$

$$
f(x_{t} + \Delta x) \approx f(x_t) + \nabla f (x_t) \Delta x + \frac{1}{2} \Delta x^T H_f (x_t) \Delta x
$$

Luego, si derivamos en función de $\Delta x$ e igualamos a cero se tiene que

$$
\begin{align}
\nabla f (x_t)  +  H_f (x_t) \Delta x &= 0 \nonumber \\
\Delta x &= - [H_f (x_t)]^{-1}\nabla f (x_t)  \nonumber \\
x_{t+1} &= x_{t} - [H_f (x_t)]^{-1}\nabla f (x_t)  \nonumber
\end{align}
$$

Que se conoce como **la regla iterativa de Newton** y donde

- El gradiente $\nabla f (x_t)$ nos indica la dirección en que debemos desplazarnos
- El hessiano $H_f (x_t)$ nos indica la distancia que debemos desplazarnos

:::{caution}

- Usando Newton estamos asumiendo que la aproximación de segundo orden de $f$ es buena
- La solución del método de Newton depende de $x_0$ (solución inicial). Es conveniente probar varios valores de $x_0$
- Si la dimensionalidad de $x$ es grande, invertir el hessiano (matriz de $M\times M$) puede ser costoso

:::

Si el Hessiano es prohibitivo podemos usar una aproximación de primer orden de la regla de Newton. Esto resulta en el clásico método conocido como  **gradiente descendente**

$$
x_{t+1} = x_{t} - \eta \nabla f (x_t)
$$

donde se reemplaza el Hessiano por una constante $\eta$ llamado "paso" o "tasa de aprendizaje".

Es importante calibrar adecuadamente $\eta$. Si es muy grande la adaptación será inestable, y si es muy pequeño demoraremos demasiado en llegar al óptimo. La siguiente figura lo ejemplifica

## Tutorial de `scipy.optimize`

Podemos realizar optimización matemática utilizando el módulo [`scipy.optimize`](https://docs.scipy.org/doc/scipy/reference/optimize.html). La función principal de este módulo es `minimize` la cual engloba a una larga lista de métodos de optimización

Los argumentos esta función son

```python
from scipy.optimize import minimize
minimize(fun, # Función objetivo
         x0, # Valor inicial de la variable de decisión
         args=(), # Argumentos adicionales de fun
         method=None, # El método de optimización a usar
         jac=None, # Función que calcula la matriz de primeras derivadas (jacobiano)
         bounds=None, # Secuencia de tuplas (min, max) con cotas para x
         constraints=(), # Diccinario o lista de restricciones (más detalles a continuación)
         tol=None, # Tolerancia para el término de la optimización
         callback=None, # Una función que se ejecuta luego de cada iteración
         options=None, # Diccionario con las opciones especificas para cada método
         ...
        )
```

Para utilizar `minimize` la función objetivo debe estar definida de la siguiente forma

```python
def fun(x, *args):
    ...
    return foo
```

donde

- `x` debe ser un escalar o un ndarray
- `foo` debe ser un valor escalar flotante
- Los argumentos adicionales a `x` se deben desempaquetar de la tupla `args`

:::{warning}

Si el problema a resolver es de maximización, debemos convertirlo a un problema de minimización antes de usar `minimize`

:::

:::{note}

Si no se especifica `method` se utiliza por defecto [BFGS](https://es.wikipedia.org/wiki/L-BFGS), un método de tipo quasi-Newton que utiliza el gradiente de la función objetivo y una aproximación del inverso de su Hessiano

:::

**Ejemplo**

Sea la siguiente función objetivo con variable de decisión escalar

$$
f(x) = x^2 - 2x + 5 \sin(2x)
$$

Para optimizarla con `scipy` primero debemos definirla:

```python
def f(x):
    return x**2 - 2*x + 5*np.sin(2*x)
```

Luego la optimizamos con `minimize` especificando como mínimo la función misma y una solución inicial

```python
x0 = -2
solution = scipy.optimize.minimize(f, x0)
```

`solution` es un objeto de tipo [`OptimizeResult`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.OptimizeResult.html#scipy.optimize.OptimizeResult), cuyos atributos más importantes son

- `x`: Mejor valor encontrado de la variable de decisión
- `fun`: Valor de la función objetivo en el óptimo encontrado
- `jac`: Valor de la matriz de primeras derivadas en el óptimo encontrado
- `success`: Booleano que indica si la optimización se llevó a cabo con exito
- `message:` Mensaje indicando la razón de término, útil para debuggear

```python
solution.message
```

    'Optimization terminated successfully.'

Si visualizamos la solución con `matplotlib`

```python
x_plot = np.linspace(-5, 5, num=100)
fig, ax = plt.subplots(figsize=(7, 4), tight_layout=True)
ax.plot(x_plot, f(x_plot))
ax.scatter(x0, f(x0), s=100, c='k')
ax.arrow(x0, f(x0), solution.x[0]-x0, f(solution.x[0])-f(x0),
         head_width=0.3, head_length=3, color='k', length_includes_head=True)
ax.set_xlabel('Variable de decisión')
ax.set_ylabel('Función objetivo');
```

> La función es no convexa y el método quasi-Newton ha encontrado un mínimo local

:::{hint}

En problemas no-convexos es conveniente probar distintas soluciones iniciales

:::

```python
x0 = np.array([-4.5, -2, 4])
solution = []
for x0_ in x0:
    solution.append(scipy.optimize.minimize(f, x0_))

bests = np.array([solution.x[0] for solution in solution])
```

```python
fig, ax = plt.subplots(figsize=(7, 4), tight_layout=True)
ax.plot(x_plot, f(x_plot))
ax.scatter(x0, f(x0), label='solución inicial', s=100, c='k')
for x0_, best in zip(x0, bests):
    ax.arrow(x0_, f(x0_), best-x0_, f(best)-f(x0_),
             head_width=0.3, head_length=3, color='k', length_includes_head=True)
ax.set_xlabel('Variable de decisión')
ax.set_ylabel('Función objetivo');
```

El argumento `jac` de `minimize` espera la función de primeras derivadas o vector jacobiano. Este debe seguir una forma similar a la función objetivo

```python
def jac(x, *args):
    ...
    return dfoo
```

donde

- donde `x` y `args` deben coincidir con `fun`.
- `dfoo` debe tener la misma dimensionalidad de `x`

Si no se especifica `jac` las derivadas se calcularán de forma numérica, lo cual es menos eficiente

```python
def df(x):
    return 2*x - 2 + 2*5*np.cos(2*x)

%timeit -r10 -n10 scipy.optimize.minimize(f, 0.0)
%timeit -r10 -n10 scipy.optimize.minimize(f, 0.0, jac=df)
```

    2.4 ms ± 318 µs per loop (mean ± std. dev. of 10 runs, 10 loops each)
    1.09 ms ± 29.4 µs per loop (mean ± std. dev. of 10 runs, 10 loops each)

A continuación describiremos algunos de los métodos disponibles a través del argumento `method` de `minimize`

### Métodos para optimización sin restricciones

Con estos métodos no se pueden especificar los argumentos `constraint` y/o `bounds`.

Por ejemplo están

- [`method=CG`](https://docs.scipy.org/doc/scipy/reference/optimize.minimize-cg.html#optimize-minimize-cg): Gradiente conjugado. Es una versión de GD con tasa de aprendizaje adaptiva
- [`method=BFGS`](https://docs.scipy.org/doc/scipy/reference/optimize.minimize-bfgs.html#optimize-minimize-bfgs): Es un método de tipo [quasi-Newton](https://en.wikipedia.org/wiki/Quasi-Newton_method) con Hessiano inverso aproximado a cada paso.

Los cuales usan gradientes, ya sea numérico o especificado mediante el argumento `jac`. Si la derivada puede obtenerse analiticamente y es confiable estos métodos tendrán un desempeño superior a las alternativas

Luego están

- [`method=Nelder-Mead`](https://docs.scipy.org/doc/scipy/reference/optimize.minimize-neldermead.html#optimize-minimize-neldermead): Es una heurística tipo simplex. [Animación que muestra su funcionamiento](https://www.youtube.com/watch?v=HUqLxHfxWqU)
- [`method=Powell`](https://docs.scipy.org/doc/scipy/reference/optimize.minimize-powell.html#optimize-minimize-powell): Algoritmo de búsqueda de linea siguiendo una dirección a la vez.

Los cuales no usan gradientes. Estos métodos pueden usarse cuando la función objetivo es no-derivable o demasiado ruidosa para ser derivada

### Métodos para optimización con restricciones

Con estos métodos se pueden incorporar restricciones al problema ya sea en forma de cotas para las variables o ecuaciones de igualdad/desigualdad que las variables deben cumplir

- Las restricciones de igualdad deben ser siempre de la forma $g(x) = 0$
- Las restricciones de desigualdad deben ser siempre de la forma $h(x) \geq 0 $

En la práctica las restricciones se entregan como una tupla en el argumento `constraint` de `method`. Cada restricción es un diccionario con las llaves `type` y `fun` para especificar el tipo (string `eq` o `ineq`) y la función, respectivamente. Opcionalmente se puede especificar `jac`, la matriz de primeras derivadas de `fun` y `arg` una tupla con argumentos adicionales para `fun` y `jac`

Por ejemplo si se tiene la siguiente restricción

$$
x^2 \geq 1 + 2x
$$

se escribe como:

```python
h1 = {'type': 'ineq',
      'fun' : lambda x: x**2 - 2*x -1,
      'jac' : lambda x: np.array([2*x - 2])}
```

:::{warning}

Si tenemos restricciones del tipo $h(x)\leq 0$ debemos transformarlas a $-h(x)\geq 0$ antes de implementarlas

:::

Los métodos que permiten especificar restricciones son

- [`L-BFGS-B`](https://docs.scipy.org/doc/scipy/reference/optimize.minimize-lbfgsb.html): Similar a BFGS pero permite añadir cotas (`bounds`) para la variable de decisión
- [`SLSQP`](https://docs.scipy.org/doc/scipy/reference/optimize.minimize-slsqp.html): *Sequential Least Squares Programming*. Este método acepta cotas, restricciones de igualdad y restricciones de desigualdad

**Ejemplo**

Sea la siguiente función de costo con dos variables de decisión

$$
\min f(x, y) = -(2xy+2x-x^2-2y^2)
$$

sujeta a

$$
x^3 - y = 0 ~\wedge~y-(x-1)^4-2 \geq 0
$$

donde

$$
0.5\leq x \leq 1.5 ~\wedge~ 1.5 \leq y \leq 2.5
$$

Resolvamos utilizando `minimize`. La solución sin considerar restricciones sería:

```python
def f(z):
    return -(2*z[0]*z[1] + 2*z[0] - z[0]**2 - 2*z[1]**2)

def df(z):
    return np.array([-2*z[1] - 2 + 2*z[0], -2*z[0] + 4*z[1]])

x0 = np.array([0, 1])

solution_no_constraint = scipy.optimize.minimize(f, x0, jac=df, method='BFGS')
solution_no_constraint.x
```

    array([2., 1.])

La solución considerando cotas paras las variables de decisión sería:

```python
bounds = ((0.5, 1.5), (1.5, 2.5))
solution_with_bounds = scipy.optimize.minimize(f, x0, jac=df, method='L-BFGS-B', bounds=bounds)
solution_with_bounds.x
```

    array([1.5, 1.5])

Finalmente la solución considerando cotas y restricciones sería:

```python
cons = ({'type': 'eq',
         'fun': lambda z: z[0]**3 - z[1],
         'jac': lambda z: np.array([3*z[0]**2, -1.])
        },
        {'type': 'ineq',
         'fun': lambda z: z[1] - (z[0]-1)**4 - 2,
         'jac': lambda z: np.array([-4*(z[0]-1)**3, 1.])
        })

solution_with_constraints = scipy.optimize.minimize(f, x0, jac=df, method='SLSQP',
                                                    bounds=bounds, constraints=cons)
solution_with_constraints.x
```

    array([1.26089314, 2.00463288])

Podemos visualizar estas soluciones con `matplotlib`, el color de la superficie representa la función objetivo, el área oscura las cotas y las lineas punteadas las restricciones

```python
x = np.linspace(0, 3, 100)
y = np.linspace(0, 3, 100)
A, B = np.meshgrid(x, y)
C = f(np.vstack([A.ravel(), B.ravel()])).reshape((100,100))
fig, ax = plt.subplots(figsize=(9, 5))
cm = ax.contourf(A, B, C, cmap=plt.cm.Blues);
ax.plot(x, x**3, 'k--', lw=2)
ax.plot(x, 2+(x-1)**4, 'k--', lw=2)
ax.fill([0.5, 0.5, 1.5, 1.5], [2.5, 1.5, 1.5, 2.5], alpha=0.3)
ax.axis([0, 3, 0, 3])
ax.scatter(solution_no_constraint.x[0], solution_no_constraint.x[1],
           s=100, c='b', label='BFGS');
ax.scatter(solution_with_bounds.x[0], solution_with_bounds.x[1],
           s=100, c='g', label='L-BFGS-B');
ax.scatter(solution_with_constraints.x[0], solution_with_constraints.x[1],
           s=100, c='r', label='SLSQP');
fig.colorbar(cm)
plt.legend();
```

## Más allá de esta lección

:::{seealso}

Funcionalidades de SciPy que no se exploraron en esta lección:

- [Encontrando raices de una función](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.root.html#scipy.optimize.root)
- [Solución de problemas de programación lineal](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linprog.html#scipy.optimize.linprog)

:::

:::{warning}

Tenga en cuenta las siguientes limitaciones:

- Scipy puede ser lento para problemas de gran envergadura. Si este es el caso sugiero explorar los wrappers de Python de [CVXOPT](https://www.cvxpy.org/)
- Scipy no tiene optimizadores para variables de decisión discretas. En caso de necesitar utilizar un algoritmo de optimización externo/comercial (por ejemplo CPLEX, Gurobi o AMPL) , sugiero explorar la librería [Pyomo](http://www.pyomo.org/documentation)

:::

```python

```


# Clase 4a: Ecuaciones diferenciales ordinarias (EDOs)

_¿Te acuerdas de todos esos esquemas numéricos para integrar ecuaciones diferenciales ordinarias? Es bueno saber que existen y qué peculiaridades tiene cada uno, pero en este curso no queremos implementar esos esquemas: queremos resolver las ecuaciones. Los problemas de evolución están por todas partes en ingeniería y son de los más divertidos de programar._

Nos basaremos en algunos de los ejemplos de https://mat.caminos.upm.es/wiki/Categor%C3%ADa:Ecuaciones_Diferenciales

```python
import numpy as np
```

```python
%matplotlib inline
import matplotlib.pyplot as plt
```

## Decaimiento exponencial y oscilador armónico

### Fundamentos

Para integrar EDOs vamos a usar la función `odeint` del paquete `integrate`, que permite integrar sistemas del tipo:

$$ \frac{d\mathbf{y}}{dt}=\mathbf{f}\left(\mathbf{y},t\right)$$

con condiciones iniciales $\mathbf{y}(\mathbf{0}) = \mathbf{y_0}$.

**¡Importante!**: La función del sistema recibe como primer argumento $\mathbf{y}$ (un array) y como segundo argumento el instante $t$ (un escalar). Esta convención va exactamente al revés que en MATLAB y si se hace al revés obtendremos errores o, lo que es peor, resultados incorrectos.

```python
from scipy.integrate import odeint
```

Vamos a integrar primero una EDO elemental, cuya solución ya conocemos:

$$y' + y = 0$$

$$f(y, t) = \frac{dy}{dt} = -y$$

```python
def f(y, t):
    return -y
```

Condiciones iniciales:

```python
y0 = 1
```

Vector de tiempos donde realizamos la integración:

```python
t = np.linspace(0, 3)
```

Integramos y representamos la solución:

```python
sol = odeint(f, y0, t)
plt.plot(t, sol)
```

### EDOs de orden superior

Tendremos que acordarnos ahora de cómo reducir las ecuaciones de orden. De nuevo, vamos a probar con un ejemplo académico:

$$y + y'' = 0$$

$$\mathbf{y} \leftarrow \pmatrix{y \\ y'}$$

$$\mathbf{f}(\mathbf{y}) = \frac{d\mathbf{y}}{dt} =  \pmatrix{y \\ y'}' = \pmatrix{y' \\ y''} = \pmatrix{y' \\ -y}$$

```python
def f(y, t):
    return np.array([y[1], -y[0]])

t = np.linspace(0, 10)
y0 = np.array([1.0, 0.0])

sol = odeint(f, y0, t)

plt.plot(t, sol[:, 0], label="$y$")
plt.plot(t, sol[:, 1], '--k', label="$\dot{y}$")
plt.legend()
```

## Ecuación de vigas: Modelo de Euler-Bernoulli

*Problema tomado de https://mat.caminos.upm.es/wiki/Ecuaci%C3%B3n_de_vigas:_Modelo_de_Euler-Bernoulli_%2813A%29, por María Aguilera, Paula Martínez, Miguel Sánchez, Laura García, Isabel Roselló y Sarah Boufounas*

¿Qué puede haber más caminero que resolver la ecuación de la viga? En esta sección vamos a estudiar el comportamiento de una viga de 10 metros de longitud y sección rectangular al someterla a la acción de diferentes esfuerzos y variar las dimensiones de su sección transversal, y luego calcularemos la respuesta de esta misma viga al cambiar sus condiciones de apoyo.

En este caso, supondremos que el desplazamiento de la viga $y(x)$ y el momento flector $M(x)$ cumplen la curva elástica:

\begin{equation}
y''=\frac{M(x)}{E I(x)}
\end{equation}

### Viga biapoyada sometida a la acción de momentos flectores

Datos:

* $L = 10$
* $E=5 \cdot 10^4$
* $a=0.6$, $b=0.3$
* $M(x)=\frac{L}{2}−|x−\frac{L}{2}|$

Nos acordamos de la expresión para el momento de inercia:

\begin{equation}
I=\frac{1}{12} b a^3
\end{equation}

(Luego veremos un caso en el que las dimensiones no son constantes a lo largo de la longitud)

En este caso las condiciones de contorno son:

\begin{align}
y(0) & = 0 \\
y(L) & = 0
\end{align}

Como **ya no tenemos un problema de evolución sino un problema de contorno** hay que escoger un método apropiado, así que nosotros vamos a usar el método del disparo (_shooting method_). ¿Por qué?

* Porque en Matewiki está resuelto por diferencias finitas
* Porque así podemos aprovechar lo que ya sabemos de problemas de evolución
* Porque en el fondo a nadie le gusta ensamblar matrices :)

Como no tenemos todas las condiciones iniciales, el método del disparo consiste en:

1. Probar una solución inicial.
2. Resolver el problema y comprobar si el otro extremo coincide con la condición de contorno dada.
3. Iterar hasta converger.

¡Vamos allá!

En primer lugar, probamos a resolver el problema con estas condiciones iniciales:

\begin{align}
y(0) & = 0 \\
y'(0) & = -5 \cdot 10^{-2}
\end{align}

Para comprobar que la solución no coincide con lo que buscamos.

```python
L = 10
E = 5e4
a, b = 0.6, 0.3

I = b * a ** 3 / 12

x = np.linspace(0, L)

def M(x):
    return L/2 - np.abs(x - L/2)

def f(y, x):
    return np.array([y[1], M(x) / (E * I)])

yp0 = -6e-2
y0 = np.array([0, yp0])
sol = odeint(f, y0, x)
plt.plot(x, sol[:, 0], label="$y$")
```

Ahora iteramos sobre el desplazamiento en $L$ para converger a una solución buena. Para ello, vamos a crear una función:

* **Entrada**: $\tilde{y}'(0)$
* **Salida**: $y(L)$

```python
def viga_biap(yp0):
    y0 = np.array([0, yp0])
    sol = odeint(f, y0, x)
    return sol[-1, 0]

viga_biap(-6e-2)
```

    -0.13703719805719136

Y ahora la resolvemos utilizando el [método de la bisección](http://pybonacci.org/2012/04/18/ecuaciones-no-lineales-metodo-de-biseccion-y-metodo-de-newton-en-python/) o alguna función de `scipy.optimize`.

```python
from scipy.optimize import newton
newton(viga_biap, -5e-2)
```

    -0.046296287367577127

```python
viga_biap(-0.046296287367577127)
```

    8.3266726846886741e-17

¡Y ya lo tenemos!

```python
yp0 = newton(viga_biap, -5e-2)
y0 = np.array([0, yp0])
sol = odeint(f, y0, x)
plt.plot(x, sol[:, 0], label="$y$")
plt.legend()
```

## Para ampliar

* En nuestra edición anterior del curso de AeroPython puedes ver una aplicación muy interesante de lo que hemos visto hasta ahora al [**salto de Felix Baumgartner**](http://nbviewer.ipython.org/github/AeroPython/Curso_AeroPython/blob/v1.0/Notebooks/Clase6b_Finale.ipynb):

$$\displaystyle m \frac{d^2 y}{d t^2} = -m g + D$$

* Nuestro compañero Álex escribió un fenomenal artículo en Pybonacci sobre [el modelo presa depredador de Lotka Volterra](http://pybonacci.org/2015/01/05/ecuaciones-de-lotka-volterra-modelo-presa-depredador/):

\begin{align*}
\frac{dx}{dt} &= \alpha x - \beta x y \\
\frac{dy}{dt} &= -\gamma y + \delta y x
\end{align*}

* Puedes [modelizar problemas en derivadas parciales utilizando el método de los elementos finitos en Python con FEniCS](http://pybonacci.org/2015/01/20/fenics-resolucion-de-ecuaciones-diferenciales-en-python/), como la ecuación de Poisson:

\begin{equation*}
-\nabla^2 u = f \quad u \in \Omega.
\end{equation*}

---

Clase en vídeo, parte del [Curso de Python para científicos e ingenieros](http://cacheme.org/curso-online-python-cientifico-ingenieros/) grabado en la Escuela Politécnica Superior de la Universidad de Alicante.

```python
from IPython.display import YouTubeVideo

YouTubeVideo("1R_JnTajrRY", width=560, height=315, list="PLGBbVX_WvN7bMwYe7wWV5TZt1a58jTggB")
```

---

Si te ha gustado esta clase:

Tweet

---

####
¡Síguenos en Twitter!

###### Follow @AeroPython

#####
Curso AeroPython por Juan Luis Cano Rodriguez y Alejandro Sáez Mollejo se distribuye bajo una Licencia Creative Commons Atribución 4.0 Internacional.

#####

---
_Las siguientes celdas contienen configuración del Notebook_

_Para visualizar y utlizar los enlaces a Twitter el notebook debe ejecutarse como [seguro](http://ipython.org/ipython-doc/dev/notebook/security.html)_

    File > Trusted Notebook

```python
%%html
<a href="https://twitter.com/AeroPython" class="twitter-follow-button" data-show-count="false">Follow @AeroPython</a>

```

```python
# Esta celda da el estilo al notebook
from IPython.core.display import HTML
css_file = '../static/styles/style.css'
HTML(open(css_file, "r").read())
```


# Selección óptima de portafolios II

Entonces, tenemos que:
- La LAC describe las posibles selecciones de riesgo-rendimiento entre un activo libre de riesgo y un activo riesgoso.
- Su pendiente es igual al radio de Sharpe del activo riesgoso.
- La asignación óptima de capital para cualquier inversionista es el punto tangente de la curva de indiferencia del inversionista con la LAC.

Para todo lo anterior, supusimos que ya teníamos el portafolio óptimo (activo riesgoso).

En la clase pasada aprendimos a hallar este portafolio óptimo si el conjunto de activos riesgosos estaba conformado únicamente por dos activos:

$$w_{1,EMV}=\frac{(E[r_1]-r_f)\sigma_2^2-(E[r_2]-r_f)\sigma_{12}}{(E[r_2]-r_f)\sigma_1^2+(E[r_1]-r_f)\sigma_2^2-((E[r_1]-r_f)+(E[r_2]-r_f))\sigma_{12}}.$$

- Sin embargo, la complejidad del problema crece considerablemente con el número de variables, y la solución analítica deja de ser viable cuando mencionamos que un portafolio bien diversificado consta aproximadamente de 50-60 activos.
- En esos casos, este problema se soluciona con rutinas numéricas que hagan la optimización por nosotros, porque son una solución viable y escalable a más variables.

**Objetivos:**
- ¿Cuál es el portafolio óptimo de activos riesgosos cuando tenemos más de dos activos?
 - ¿Cómo construir la frontera de mínima varianza cuando tenemos más de dos activos?

*Referencia:*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Maximizando el radio de Sharpe

### ¿Qué pasa si tenemos más de dos activos riesgosos?

En realidad es algo muy similar a lo que teníamos con dos activos.
- Para dos activos, construir la frontera de mínima varianza es trivial: todas las posibles combinaciones.
- Con más de dos activos, recordar la definición: la frontera de mínima varianza es el lugar geométrico de los portafolios que proveen el mínimo riesgo para un nivel de rendimiento dado.

 Ver en el tablero.

Analíticamente:
- $n$ activos,
- caracterizados por $(\sigma_i,E[r_i])$,
- cada uno con peso $w_i$, con $i=1,2,\dots,n$.

Entonces, buscamos los pesos tales que

\begin{align}
\min_{w_1,\dots,w_n} & \quad \sum_{i=1}^{n}w_i^2\sigma_i^2+\sum_{i=1}^{n}\sum_{j=1,j\neq i}^{n}w_iw_j\sigma_{ij}\\
\text{s.a.}          & \quad \sum_{i=1}^{n}w_i=1, w_i\geq0\\
                     & \quad \sum_{i=1}^{n}w_iE[r_i]=\bar{\mu},
\end{align}

donde $\bar{\mu}$ corresponde a un nivel de rendimiento objetivo.

**Obviamente, tendríamos que resolver este problema para muchos niveles de rendimiento objetivo.**

-  Explicar relación con gráfica.

-  Recordar clase 10.

Lo anterior se puede escribir vectorialmente como:

\begin{align}
\min_{\boldsymbol{w}} & \quad \boldsymbol{w}^T\Sigma\boldsymbol{w}\\
\text{s.a.}          & \quad \boldsymbol{1}^T\boldsymbol{w}=1, \boldsymbol{w}\geq0\\
                     & \quad E[\boldsymbol{r}^T]\boldsymbol{w}=\bar{\mu},
\end{align}

donde:
- $\boldsymbol{w}=\left[w_1,\dots,w_n\right]^T$ es el vector de pesos,
- $\boldsymbol{1}=\left[1,\dots,1\right]^T$ es un vector de unos,
- $E[\boldsymbol{r}]=\left[E[r_1],\dots,E[r_n]\right]^T$ es el vector de rendimientos esperados, y
- $\Sigma=\left[\begin{array}{cccc}\sigma_{1}^2 & \sigma_{12}  & \dots  & \sigma_{1n} \\
                                   \sigma_{21}  & \sigma_{2}^2 & \dots  & \sigma_{2n} \\
                                   \vdots       & \vdots       & \ddots & \vdots      \\
                                   \sigma_{n1}  & \sigma_{n2}  & \dots  & \sigma_{n}^2\end{array}\right]$ es la matriz de varianza-covarianza.

**Esta última forma es la que comúnmente usamos al programar, por ser eficiente y escalable a problemas de N variables.**

### Entonces, ¿para cuántos niveles de rendimiento objetivo tendríamos que resolver el anterior problema con el fin de graficar la frontera de mínima varianza?
- Observar que el problema puede volverse muy pesado a medida que incrementamos el número de activos en nuestro portafolio...
- Una tarea bastante compleja.

### Sucede que, en realidad, sólo necesitamos conocer dos portafolios que estén sobre la *frontera de mínima varianza*.
- Si logramos encontrar dos portafolios sobre la frontera, entonces podemos a la vez encontrar todas las posibles combinaciones de estos dos portafolios para trazar la frontera de mínima varianza.
- Ver el caso de dos activos.

### ¿Qué portafolios usar?
Hasta ahora, hemos estudiando profundamente como hallar dos portafolios muy importantes que de hecho yacen sobre la frontera de mínima varianza:
1. Portafolio de EMV: máximo SR.
2. Portafolio de mínima varianza: básicamente, el mismo problema anterior, sin la restricción de rendimiento objetivo.

Luego, tomar todas las posibles combinaciones de dichos portafolios usando las fórmulas para dos activos de medias y varianzas:
- w: peso para el portafolio EMV,
- 1-w: peso para le portafolio de mínima varianza.

## 2. Ejemplo ilustrativo.

Retomamos el ejemplo de mercados de acciones en los países integrantes del $G5$: EU, RU, Francia, Alemania y Japón.

```python
# Importamos pandas y numpy
import pandas as pd
import numpy as np
```

```python
# Resumen en base anual de rendimientos esperados y volatilidades
annual_ret_summ = pd.DataFrame(columns=['EU', 'RU', 'Francia', 'Alemania', 'Japon'], index=['Media', 'Volatilidad'])
annual_ret_summ.loc['Media'] = np.array([0.1355, 0.1589, 0.1519, 0.1435, 0.1497])
annual_ret_summ.loc['Volatilidad'] = np.array([0.1535, 0.2430, 0.2324, 0.2038, 0.2298])

annual_ret_summ.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      Media
      0.1355
      0.1589
      0.1519
      0.1435
      0.1497

      Volatilidad
      0.1535
      0.243
      0.2324
      0.2038
      0.2298

```python
# Matriz de correlación
corr = pd.DataFrame(data= np.array([[1.0000, 0.5003, 0.4398, 0.3681, 0.2663],
                                    [0.5003, 1.0000, 0.5420, 0.4265, 0.3581],
                                    [0.4398, 0.5420, 1.0000, 0.6032, 0.3923],
                                    [0.3681, 0.4265, 0.6032, 1.0000, 0.3663],
                                    [0.2663, 0.3581, 0.3923, 0.3663, 1.0000]]),
                    columns=annual_ret_summ.columns, index=annual_ret_summ.columns)
corr.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      EU
      1.0000
      0.5003
      0.4398
      0.3681
      0.2663

      RU
      0.5003
      1.0000
      0.5420
      0.4265
      0.3581

      Francia
      0.4398
      0.5420
      1.0000
      0.6032
      0.3923

      Alemania
      0.3681
      0.4265
      0.6032
      1.0000
      0.3663

      Japon
      0.2663
      0.3581
      0.3923
      0.3663
      1.0000

```python
# Tasa libre de riesgo
rf = 0.05
```

Esta vez, supondremos que tenemos disponibles todos los mercados de acciones y el activo libre de riesgo.

#### 1. Construir la frontera de mínima varianza

##### 1.1. Encontrar portafolio de mínima varianza

```python
# Importamos funcion minimize del modulo optimize de scipy
from scipy.optimize import minimize
```

```python
## Construcción de parámetros
# 1. Sigma: matriz de varianza-covarianza Sigma = S.dot(corr).dot(S)
S = np.diag(annual_ret_summ.loc['Volatilidad'])
Sigma = S.dot(corr).dot(S)
# 2. Eind: rendimientos esperados activos individuales
Eind = annual_ret_summ.loc['Media']
```

```python
# Función objetivo
def varianza(w, Sigma):
    return w.T.dot(Sigma).dot(w)
```

```python
n = len(Eind)
# Dato inicial
# w0 = np.array([0.5, 0, 0.25, 0, 0.25])
w0 = np.ones((n,)) / n
# Cotas de las variables
bnds = ((0, 1),) * n
# Restricciones
cons = {'type': 'eq', 'fun': lambda w: w.sum() - 1}
```

```python
# Portafolio de mínima varianza
min_var = minimize(fun=varianza,
                   x0=w0,
                   args=(Sigma,),
                   bounds=bnds,
                   constraints=cons
                  )
min_var
```

         fun: 0.01861776391061502
         jac: array([0.03718246, 0.03881475, 0.03859101, 0.03755156, 0.0370423 ])
     message: 'Optimization terminated successfully.'
        nfev: 49
         nit: 7
        njev: 7
      status: 0
     success: True
           x: array([6.17797049e-01, 3.46944695e-18, 0.00000000e+00, 2.09394358e-01,
           1.72808594e-01])

```python
# Pesos, rendimiento y riesgo del portafolio de mínima varianza
w_minvar = min_var.x
E_minvar = Eind.T.dot(w_minvar)
s_minvar = varianza(w_minvar, Sigma)**0.5
RS_minvar = (E_minvar - rf) / s_minvar
w_minvar, E_minvar, s_minvar, RS_minvar
```

    (array([6.17797049e-01, 3.46944695e-18, 0.00000000e+00, 2.09394358e-01,
            1.72808594e-01]),
     0.13962903688859557,
     0.13644692708381168,
     0.6568783834431207)

##### 1.2. Encontrar portafolio EMV

```python
# Función objetivo
def m_RS(w, Sigma, rf, Eind):
    Ep = Eind.T.dot(w)
    sp = varianza(w, Sigma)**0.5
    RS = (Ep - rf) / sp
    return -RS
```

```python
n = len(Eind)
# Dato inicial
w0 = np.ones((n,)) / n
# Cotas de las variables
bnds = ((0, 1),) * n
# Restricciones
cons = {'type': 'eq', 'fun': lambda w: w.sum() - 1}
```

```python
# Portafolio EMV
EMV = minimize(fun=m_RS,
               x0=w0,
               args=(Sigma, rf, Eind),
               bounds=bnds,
               constraints=cons
              )
EMV
```

         fun: -0.6644372965632436
         jac: array([-0.36088951, -0.36076408, -0.36036385, -0.36108153, -0.36062376])
     message: 'Optimization terminated successfully.'
        nfev: 35
         nit: 5
        njev: 5
      status: 0
     success: True
           x: array([0.50714174, 0.07470888, 0.02471533, 0.18943972, 0.20399434])

```python
# Pesos, rendimiento y riesgo del portafolio EMV
w_EMV = EMV.x
E_EMV = Eind.T.dot(w_EMV)
s_EMV = varianza(w_EMV, Sigma)**0.5
RS_EMV = (E_EMV - rf) / s_EMV
w_EMV, E_EMV, s_EMV, RS_EMV
```

    (array([0.50714174, 0.07470888, 0.02471533, 0.18943972, 0.20399434]),
     0.1420657564717856,
     0.13856199365687238,
     0.6644372965632436)

```python
RS_EMV, RS_minvar
```

    (0.6644372965632436, 0.6568783834431207)

```python
s_EMV, s_minvar
```

    (0.13856199365687238, 0.13644692708381168)

##### 1.3. Construir frontera de mínima varianza

También debemos encontrar la covarianza (o correlación) entre estos dos portafolios:

```python
# Covarianza entre los portafolios
cov_EMV_minvar = w_EMV.T.dot(Sigma).dot(w_minvar)
cov_EMV_minvar
```

    0.018690275386034134

```python
# Correlación entre los portafolios
corr_EMV_minvar = cov_EMV_minvar / (s_EMV * s_minvar)
corr_EMV_minvar
```

    0.9885708894197612

```python
# Vector de w
wp = np.linspace(0, 7, 100)
```

```python
# DataFrame de portafolios:
# 1. Índice: i
# 2. Columnas 1-2: w, 1-w
# 3. Columnas 3-4: E[r], sigma
# 4. Columna 5: Sharpe ratio
front_min_var = pd.DataFrame({'w(EMV)': wp,
                              'w(minvar)': 1 - wp,
                              'Media': E_EMV * wp + E_minvar * (1 - wp),
                              'Vol': ((wp * s_EMV)**2 + ((1 - wp) * s_minvar)**2 + 2 * wp * (1 - wp) * cov_EMV_minvar)**0.5
                             })
front_min_var['RS'] = (front_min_var['Media'] - rf) / front_min_var['Vol']
front_min_var.head()
```

      w(EMV)
      w(minvar)
      Media
      Vol
      RS

      0
      0.000000
      1.000000
      0.139629
      0.136447
      0.656878

      1
      0.070707
      0.929293
      0.139801
      0.136492
      0.657921

      2
      0.141414
      0.858586
      0.139974
      0.136554
      0.658887

      3
      0.212121
      0.787879
      0.140146
      0.136632
      0.659774

      4
      0.282828
      0.717172
      0.140318
      0.136725
      0.660583

```python
# Importar librerías de gráficos
from matplotlib import pyplot as plt
```

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a SR, los activos individuales
# y los portafolios hallados
plt.figure(figsize=(6, 4))
# Frontera
plt.scatter(front_min_var['Vol'],
            front_min_var['Media'],
            c=front_min_var['RS'],
            cmap='inferno',
            label='Front. Min. Var.')
# Activos ind
for i in range(n):
    plt.plot(annual_ret_summ.iloc[1, i],
             annual_ret_summ.iloc[0, i],
             'o', ms=10,
             label=annual_ret_summ.columns[i])
# Port. óptimos
plt.plot(s_minvar, E_minvar, '*g', ms=10, label='Port. Min. Var.')
plt.plot(s_EMV, E_EMV, '*r', ms=10, label='Port. EMV')
plt.xlabel('Volatilidad $\sigma$')
plt.ylabel('Rendimiento esperado $E[r]$')
plt.legend(loc='best')
plt.colorbar()
plt.grid()
```

**A partir de lo anterior, solo restaría construir la LAC y elegir la distribución de capital de acuerdo a las preferencias (aversión al riesgo).**
___

## 3. Comentarios finales

### 3.1. Restricciones adicionales

Los inversionistas pueden tener restricciones adicionales:
1. Restricciones en posiciones cortas.
2. Pueden requerir un rendimiento mínimo.
3. Inversión socialmente responsable: prescinden de inversiones en negocios o paises considerados éticamente o políticamente indeseables.

Todo lo anterior se puede incluir como restricciones en el problema de optimización, y puede ser llevado a cabo a costa de un cociente de Sharpe menor.

### 3.2. Críticas a la optimización media varianza
1. Solo importan medias y varianzas: recordar que la varianza subestima el riesgo en algunos casos.
2. Preferencias media-varianza tratan las ganancias y pérdidas simétricamente: el sentimiento de insatisfacción de una perdida es mayor al sentimiento de satisfacción de una ganancia (aversión a pérdidas).
3. La aversión al riesgo es constante: la actitud frente al riesgo puede cambiar, por ejemplo con el estado de la economía.
4. Horizonte corto (un periodo).
5. Basura entra - basura sale: la optimización media varianza es supremamente sensible a las entradas: estimaciones de rendimientos esperados y varianzas.
___

# Anuncios parroquiales

## 1. Quiz la próxima clase (clases 12, 13, y 14).
## 2. Revisar archivo Tarea 6. Miércoles, 1 de Julio.
## 3. Fin de módulo. Abrir Clase0 y revisar objetivos del módulo.
## 4. [Nota interesante](http://yetanothermathprogrammingconsultant.blogspot.com/2016/08/portfolio-optimization-maximize-sharpe.html)

Created with Jupyter by Esteban Jiménez Rodríguez.


## Accleration from motion
Calcualtion of 2 DOF acceleration in some point based on 2 DOF motion in some other point

```python
import warnings
#warnings.filterwarnings('ignore')
%matplotlib inline
%load_ext autoreload
%autoreload 2
```

```python
import sympy as sp
import sympy.physics.mechanics as me
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from signal_lab.substitute_dynamic_symbols import substitute_dynamic_symbols, find_name, find_derivative_name, lambdify

```

```python
x0,y0,z0 = me.dynamicsymbols('x0 y0 z0')
x01d,y01d,z01d = me.dynamicsymbols('x0 y0 z0',1)
x02d,y02d,z02d = me.dynamicsymbols('x0 y0 z0',2)

phi,theta,psi = me.dynamicsymbols('phi theta psi')
p,q,r = me.dynamicsymbols('phi theta psi',1)
p1d,q1d,r1d = me.dynamicsymbols('phi theta psi',2)

p_x = sp.Symbol('p_x')
p_y = sp.Symbol('p_y')
p_z = sp.Symbol('p_z')

```

```python
x02d
```

$\displaystyle \frac{d^{2}}{d t^{2}} \operatorname{x_{0}}{\left(t \right)}$

```python
N = me.ReferenceFrame('N')
```

```python
S = N.orientnew('S', 'Axis', [theta,N.y])
```

```python
#S.set_ang_vel(N, q * N.y)
#S.set_ang_acc(N, q1d * N.y)
```

```python
S.ang_vel_in(N)
```

$\displaystyle \dot{\theta}\mathbf{\hat{n}_y}$

```python
S.ang_acc_in(N)
```

$\displaystyle \ddot{\theta}\mathbf{\hat{n}_y}$

```python
M = me.Point('M')  # Origo of measurement system
M.set_vel(N, 0)

O = M.locatenew('O', x0*N.x + z0*N.z) # Origo of ship
O.set_vel(N, x01d*N.x + z01d*N.z)
O.set_acc(N, x02d*N.x + z02d*N.z)

```

```python
P = O.locatenew('P', p_x*S.x + p_z*S.z)
P.set_vel(S, 0)

```

```python
S.dcm(N)
```

$\displaystyle \left[\begin{matrix}\cos{\left(\theta{\left(t \right)} \right)} & 0 & - \sin{\left(\theta{\left(t \right)} \right)}\\0 & 1 & 0\\\sin{\left(\theta{\left(t \right)} \right)} & 0 & \cos{\left(\theta{\left(t \right)} \right)}\end{matrix}\right]$

```python
vector = P.pos_from(M)
motion_P = vector.express(N)
motion_P
```

$\displaystyle (p_{x} \operatorname{cos}\left(\theta\right) + p_{z} \operatorname{sin}\left(\theta\right) + x_{0})\mathbf{\hat{n}_x} + (- p_{x} \operatorname{sin}\left(\theta\right) + p_{z} \operatorname{cos}\left(\theta\right) + z_{0})\mathbf{\hat{n}_z}$

```python
motion_matrix = vector.to_matrix(N)
motion_matrix
```

$\displaystyle \left[\begin{matrix}p_{x} \cos{\left(\theta{\left(t \right)} \right)} + p_{z} \sin{\left(\theta{\left(t \right)} \right)} + \operatorname{x_{0}}{\left(t \right)}\\0\\- p_{x} \sin{\left(\theta{\left(t \right)} \right)} + p_{z} \cos{\left(\theta{\left(t \right)} \right)} + \operatorname{z_{0}}{\left(t \right)}\end{matrix}\right]$

```python
motion_lambda = lambdify(motion_matrix)
```

```python
motion_lambda
```

    <function _lambdifygenerated(z0, p_z, theta, x0, p_x)>

```python
number_of_points = 100
t = np.linspace(0,2*np.pi,number_of_points)
df = pd.DataFrame(index = t)
df['theta'] = np.sin(t)
df['p_z'] = 1
df['p_x'] = 20
df['x0'] = np.sin(t)
df['z0'] = -np.sin(t)
motions = motion_lambda(**df)

df_result = df.copy()
df_result['x'] = motions[0][0]
df_result['y'] = motions[1][0]
df_result['z'] = motions[2][0]

fig,ax=plt.subplots()
df_result.plot(y='x', ax=ax)

fig,ax=plt.subplots()
df_result.plot(y='z', ax=ax)

```

## Velocity

```python
P.v2pt_theory(O,N,S)
```

$\displaystyle \dot{x}_{0}\mathbf{\hat{n}_x} + \dot{z}_{0}\mathbf{\hat{n}_z} + p_{z} \dot{\theta}\mathbf{\hat{s}_x} -  p_{x} \dot{\theta}\mathbf{\hat{s}_z}$

```python
velocity_matrix = P.vel(N).express(N).to_matrix(N)
velocity_matrix
```

$\displaystyle \left[\begin{matrix}- p_{x} \sin{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} \theta{\left(t \right)} + p_{z} \cos{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} \theta{\left(t \right)} + \frac{d}{d t} \operatorname{x_{0}}{\left(t \right)}\\0\\- p_{x} \cos{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} \theta{\left(t \right)} - p_{z} \sin{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} \theta{\left(t \right)} + \frac{d}{d t} \operatorname{z_{0}}{\left(t \right)}\end{matrix}\right]$

```python
velocity_lambda = lambdify(velocity_matrix)
```

```python
q.expr
```

$\displaystyle \theta{\left(t \right)}$

```python
q
```

$\displaystyle \frac{d}{d t} \theta{\left(t \right)}$

```python
q1d.args[1][1]
```

$\displaystyle 2$

```python
velocity_lambda
```

    <function _lambdifygenerated(theta, p_z, z01d, x01d, p_x, theta1d)>

```python
number_of_points = 100
t = np.linspace(0,2*np.pi,number_of_points)
df = pd.DataFrame(index = t)
df['theta'] = np.sin(t)
df['theta1d'] = np.cos(t)
df['p_z'] = 1
df['p_x'] = 20
df['x01d'] = 10
df['z01d'] = 0

velocities = velocity_lambda(**df)

df_result = df.copy()
df_result['x01d'] = velocities[0][0]
df_result['y01d'] = velocities[1][0]
df_result['z01d'] = velocities[2][0]

fig,ax=plt.subplots()
df_result.plot(y='x01d', ax=ax)

fig,ax=plt.subplots()
df_result.plot(y='z01d', ax=ax)
```

## Acceleration

```python
P.a2pt_theory(O,N,S)
```

$\displaystyle \ddot{x}_{0}\mathbf{\hat{n}_x} + \ddot{z}_{0}\mathbf{\hat{n}_z} + (- p_{x} \dot{\theta}^{2} + p_{z} \ddot{\theta})\mathbf{\hat{s}_x} + (- p_{x} \ddot{\theta} - p_{z} \dot{\theta}^{2})\mathbf{\hat{s}_z}$

```python
acceleration_matrix = P.acc(N).express(N).to_matrix(N)
acceleration_matrix
```

$\displaystyle \left[\begin{matrix}\left(- p_{x} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2} + p_{z} \frac{d^{2}}{d t^{2}} \theta{\left(t \right)}\right) \cos{\left(\theta{\left(t \right)} \right)} + \left(- p_{x} \frac{d^{2}}{d t^{2}} \theta{\left(t \right)} - p_{z} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2}\right) \sin{\left(\theta{\left(t \right)} \right)} + \frac{d^{2}}{d t^{2}} \operatorname{x_{0}}{\left(t \right)}\\0\\- \left(- p_{x} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2} + p_{z} \frac{d^{2}}{d t^{2}} \theta{\left(t \right)}\right) \sin{\left(\theta{\left(t \right)} \right)} + \left(- p_{x} \frac{d^{2}}{d t^{2}} \theta{\left(t \right)} - p_{z} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2}\right) \cos{\left(\theta{\left(t \right)} \right)} + \frac{d^{2}}{d t^{2}} \operatorname{z_{0}}{\left(t \right)}\end{matrix}\right]$

```python
acceleration_lambda = lambdify(acceleration_matrix)
```

```python
acceleration_lambda
```

    <function _lambdifygenerated(theta2d, theta, p_z, x02d, z02d, p_x, theta1d)>

### Centrifugal acceleration
Absolute acceleration can be calcualted as:

$ |a| = r\cdot\dot{\theta}^2 $

With the components:

$ \ddot{x_0} = -a\cdot cos(\theta) $

$ \ddot{z_0} = a\cdot sin(\theta) $

```python
number_of_points = 100
t = np.linspace(0,2*np.pi,number_of_points)
r_ = 10
w_ = 1
df = pd.DataFrame(index = t)
df['theta'] = w_*t
df['theta1d'] = w_
df['theta2d'] = 0
df['p_z'] = 0
df['p_x'] = r_
df['x02d'] = 0
df['z02d'] = 0

accelerations = acceleration_lambda(**df)

df_result = df.copy()
df_result['x02d'] = accelerations[0][0]
df_result['y02d'] = accelerations[1][0]
df_result['z02d'] = accelerations[2][0]

a = r_* w_**2
df_result['x02d_check'] = -a*np.cos(df['theta'])
df_result['z02d_check'] = a*np.sin(df['theta'])

fig,ax=plt.subplots()
df_result.plot(y=['x02d','x02d_check'], ax=ax, alpha=0.7)

fig,ax=plt.subplots()
df_result.plot(y=['z02d','z02d_check'], ax=ax, alpha=0.7)
```

## Save lambdas

```python
import dill
dill.settings['recurse'] = True
```

```python
with open('test.dill',mode='wb') as file:
    dill.dump(acceleration_lambda, file=file)
```

```python
#with open('test',mode='rb') as file:
#    s = dill.load(file)
```

```python

```


# 2022-03-16 Beyond Linear Models

## Last time

* Discussion
* Bias-variance tradeoff
* Linear models

## Today
* Assumptions of linear models
* Look at your data!
* Partial derivatives
* Loss functions

```julia
using LinearAlgebra
using Plots
default(linewidth=4, legendfontsize=12)

function vander(x, k=nothing)
    if isnothing(k)
        k = length(x)
    end
    m = length(x)
    V = ones(m, k)
    for j in 2:k
        V[:, j] = V[:, j-1] .* x
    end
    V
end

function vander_chebyshev(x, n=nothing)
    if isnothing(n)
        n = length(x) # Square by default
    end
    m = length(x)
    T = ones(m, n)
    if n > 1
        T[:, 2] = x
    end
    for k in 3:n
        #T[:, k] = x .* T[:, k-1]
        T[:, k] = 2 * x .* T[:,k-1] - T[:, k-2]
    end
    T
end

function chebyshev_regress_eval(x, xx, n)
    V = vander_chebyshev(x, n)
    vander_chebyshev(xx, n) / V
end

runge(x) = 1 / (1 + 10*x^2)
runge_noisy(x, sigma) = runge.(x) + randn(size(x)) * sigma

CosRange(a, b, n) = (a + b)/2 .+ (b - a)/2 * cos.(LinRange(-pi, 0, n))

vcond(mat, points, nmax) = [cond(mat(points(-1, 1, n))) for n in 2:nmax]
```

    vcond (generic function with 1 method)

# Bias-variance tradeoff

The expected error in our approximation $\hat f(x)$ of noisy data $y = f(x) + \epsilon$ (with $\epsilon \sim \mathcal N(0, \sigma)$), can be decomposed as
$$ E[(\hat f(x) - y)^2] = \sigma^2 + \big(\underbrace{E[\hat f(x)] - f(x)}_{\text{Bias}}\big)^2 + \underbrace{E[\hat f(x)^2] - E[\hat f(x)]^2}_{\text{Variance}} . $$
The $\sigma^2$ term is irreducible error (purely due to observation noise), but bias and variance can be controlled by model selection.
More complex models are more capable of expressing the underlying function $f(x)$, thus are capable of reducing bias.  However, they are also more affected by noise, thereby increasing variance.

# Stacking many realizations

```julia
degree = 5
x = LinRange(-1, 1, 500)
Y = []
for i in 1:100
    yi = runge_noisy(x, 0.25)
    push!(Y, chebyshev_regress_eval(x, x, degree) * yi)
end

Y = hcat(Y...)
@show size(Y) # (number of points in each fit, number of fits)
plot(x, Y, label=nothing);
plot!(x, runge.(x), color=:black)
```

    size(Y) = (500, 100)

# Interpretation

* Re-run the cell above for different values of `degree`.  (Set it back to a number around 7 to 10 before moving on.)
* Low-degree polynomials are not rich enough to capture the peak of the function.
* As we increase degree, we are able to resolve the peak better, but see more eratic behavior near the ends of the interval.  This erratic behavior is **overfitting**, which we'll quantify as *variance*.
* This tradeoff is fundamental: richer function spaces are more capable of approximating the functions we want, but they are more easily distracted by noise.

# Mean and variance over the realizations

```julia
Ymean = sum(Y, dims=2) / size(Y, 2)
plot(x, Ymean, label="\$ E[\\hat{f}(x)] \$")
plot!(x, runge.(x), label="\$ f(x) \$")
```

```julia
function variance(Y)
    """Compute the Variance as defined at the top of this activity"""
    n = size(Y, 2)
    sum(Y.^2, dims=2)/n - (sum(Y, dims=2) / n) .^2
end

Yvar = variance(Y)
@show size(Yvar)
plot(x, Yvar)
```

    size(Yvar) = (500, 1)

# Why do we call it a linear model?

We are currently working with algorithms that express the regression as a linear function of the model parameters.  That is, we search for coefficients $c = [c_1, c_2, \dotsc]^T$ such that

$$ V(x) c \approx y $$

where the left hand side is linear in $c$.  In different notation, we are searching for a predictive model

$$ f(x_i, c) \approx y_i \text{ for all $(x_i, y_i)$} $$

that is linear in $c$.

# Standard assumptions for regression
## (the way we've been doing it so far)

1. The independent variables $x$ are error-free
1. The prediction (or "response") $f(x,c)$ is linear in $c$
1. The noise in the measurements $y$ is independent (uncorrelated)
1. The noise in the measurements $y$ has constant variance

There are reasons why all of these assumptions may be undesirable in practice, thus leading to more complicated methods.

# [Anscombe's quartet](https://en.wikipedia.org/wiki/Anscombe%27s_quartet)

# Loss functions

The error in a single prediction $f(x_i,c)$ of an observation $(x_i, y_i)$ is often measured as
$$ \frac 1 2 \big( f(x_i, c) - y_i \big)^2, $$
which turns out to have a statistical interpretation when the noise is normally distributed.
It is natural to define the error over the entire data set as
\begin{align} L(c; x, y) &= \sum_i \frac 1 2 \big( f(x_i, c) - y_i \big)^2 \\
&= \frac 1 2 \lVert f(x, c) - y \rVert^2
\end{align}
where I've used the notation $f(x,c)$ to mean the vector resulting from gathering all of the outputs $f(x_i, c)$.
The function $L$ is called the "loss function" and is the key to relaxing the above assumptions.

# Gradient of scalar-valued function

Let's step back from optimization and consider how to differentiate a function of several variables.  Let $f(\boldsymbol x)$ be a function of a vector $\boldsymbol x$.  For example,

$$ f(\boldsymbol x) = x_1^2 + \sin(x_2) e^{3x_3} . $$

We can evaluate the **partial derivative** by differentiating with respect to each component $x_i$ separately (holding the others constant), and collect the result in a vector,

\begin{align}
\frac{\partial f}{\partial \boldsymbol x} &= \begin{bmatrix} \frac{\partial f}{\partial x_1} & \frac{\partial f}{\partial x_2} & \frac{\partial f}{\partial x_3} \end{bmatrix} \\
&= \begin{bmatrix} 2 x_1 & \cos(x_2) e^{3 x_3} & 3 \sin(x_2) e^{3 x_3} \end{bmatrix}.
\end{align}

# Gradient of vector-valued functions

Now let's consider a vector-valued function $\boldsymbol f(\boldsymbol x)$, e.g.,

$$ \boldsymbol f(\boldsymbol x) = \begin{bmatrix} x_1^2 + \sin(x_2) e^{3x_3} \\ x_1 x_2^2 / x_3 \end{bmatrix} . $$

and write the derivative as a matrix,

\begin{align}
\frac{\partial \boldsymbol f}{\partial \boldsymbol x} &=
\begin{bmatrix} \frac{\partial f_1}{\partial x_1} & \frac{\partial f_1}{\partial x_2} & \frac{\partial f_1}{\partial x_3} \\
\frac{\partial f_2}{\partial x_1} & \frac{\partial f_2}{\partial x_2} & \frac{\partial f_2}{\partial x_3} \\
\end{bmatrix} \\
&= \begin{bmatrix} 2 x_1 & \cos(x_2) e^{3 x_3} & 3 \sin(x_2) e^{3 x_3} \\
x_2^2 / x_3 & 2 x_1 x_2 / x_3 & -x_1 x_2^2 / x_3^2
\end{bmatrix}.
\end{align}

# Geometry of partial derivatives

* Handy resource on partial derivatives for matrices and vectors: https://explained.ai/matrix-calculus/index.html#sec3

# Derivative of a dot product

Let $f(\boldsymbol x) = \boldsymbol y^T \boldsymbol x = \sum_i y_i x_i$ and compute the derivative

$$ \frac{\partial f}{\partial \boldsymbol x} = \begin{bmatrix} y_0 & y_1 & \dotsb \end{bmatrix} = \boldsymbol y^T . $$

Note that $\boldsymbol y^T \boldsymbol x = \boldsymbol x^T \boldsymbol y$ and we have the product rule,

$$ \frac{\partial \lVert \boldsymbol x \rVert^2}{\partial \boldsymbol x} = \frac{\partial \boldsymbol x^T \boldsymbol x}{\partial \boldsymbol x} = 2 \boldsymbol x^T . $$

Also,
$$ \frac{\partial \lVert \boldsymbol x - \boldsymbol y \rVert^2}{\partial \boldsymbol x} = \frac{\partial (\boldsymbol x - \boldsymbol y)^T (\boldsymbol x - \boldsymbol y)}{\partial \boldsymbol x} = 2 (\boldsymbol x - \boldsymbol y)^T .$$

# Variational notation

It's convenient to express derivatives in terms of how they act on an infinitessimal perturbation. So we might write

$$ \delta f = \frac{\partial f}{\partial x} \delta x .$$

(It's common to use $\delta x$ or $dx$ for these infinitesimals.) This makes inner products look like a normal product rule

$$ \delta(\mathbf x^T \mathbf y) = (\delta \mathbf x)^T \mathbf y + \mathbf x^T (\delta \mathbf y). $$

A powerful example of variational notation is differentiating a matrix inverse

$$ 0 = \delta I = \delta(A^{-1} A) = (\delta A^{-1}) A + A^{-1} (\delta A) $$
and thus
$$ \delta A^{-1} = - A^{-1} (\delta A) A^{-1} $$

# Practice

1. Differentiate $f(x) = A x$ with respect to $x$
2. Differentiate $f(A) = A x$ with respect to $A$

# Optimization
Given data $(x,y)$ and loss function $L(c; x,y)$, we wish to find the coefficients $c$ that minimize the loss, thus yielding the "best predictor" (in a sense that can be made statistically precise).  I.e.,
$$ \bar c = \arg\min_c L(c; x,y) . $$

It is usually desirable to design models such that the loss function is differentiable with respect to the coefficients $c$, because this allows the use of more efficient optimization methods.  Recall that our forward model is given in terms of the Vandermonde matrix,

$$ f(x, c) = V(x) c $$

and thus

$$ \frac{\partial f}{\partial c} = V(x) . $$

# Derivative of loss function

We can now differentiate our loss function
$$ L(c; x, y) = \frac 1 2 \lVert f(x, c) - y \rVert^2 = \frac 1 2 \sum_i (f(x_i,c) - y_i)^2 $$
term-by-term as
\begin{align} \nabla_c L(c; x,y) = \frac{\partial L(c; x,y)}{\partial c} &= \sum_i \big( f(x_i, c) - y_i \big) \frac{\partial f(x_i, c)}{\partial c} \\
&= \sum_i \big( f(x_i, c) - y_i \big) V(x_i)
\end{align}
where $V(x_i)$ is the $i$th row of $V(x)$.

# Alternative derivative
Alternatively, we can take a more linear algebraic approach to write the same expression is
\begin{align} \nabla_c L(c; x,y) &= \big( f(x,c) - y \big)^T V(x) \\
&= \big(V(x) c - y \big)^T V(x) \\
&= V(x)^T \big( V(x) c - y \big) .
\end{align}
A necessary condition for the loss function to be minimized is that $\nabla_c L(c; x,y) = 0$.

* Is the condition sufficient for general $f(x, c)$?
* Is the condition sufficient for the linear model $f(x,c) = V(x) c$?
* Have we seen this sort of equation before?


# 14 Linear Algebra: String Problem — Students (2)
## Motivating problem: Two masses on three strings
Two masses $M_1$ and $M_2$ are hung from a horizontal rod with length $L$ in such a way that a rope of length $L_1$ connects the left end of the rod to $M_1$, a rope of length $L_2$ connects $M_1$ and $M_2$, and a rope of length $L_3$ connects $M_2$ to the right end of the rod. The system is at rest (in equilibrium under gravity).

Find the angles that the ropes make with the rod and the tension forces in the ropes.

## Theoretical background
Treat $\sin\theta_i$ and $\cos\theta_i$ together with $T_i$, $1\leq i \leq 3$, as unknowns that have to simultaneously fulfill the nine equations
\begin{align}
-T_1 \cos\theta_1 + T_2\cos\theta_2 &= 0\\
 T_1 \sin\theta_1 - T_2\sin\theta_2 - W_1 &= 0\\
                   -T_2\cos\theta_2 + T_3\cos\theta_3 &= 0\\
                    T_2\sin\theta_2 + T_3\sin\theta_3 - W_2 &= 0\\
 L_1\cos\theta_1 + L_2\cos\theta_2 + L_3\cos\theta_3 - L &= 0\\
-L_1\sin\theta_1 -L_2\sin\theta_2 + L_3\sin\theta_3  &= 0\\
\sin^2\theta_1 + \cos^2\theta_1 - 1 &= 0\\
\sin^2\theta_2 + \cos^2\theta_2 - 1 &= 0\\
\sin^2\theta_3 + \cos^2\theta_3 - 1 &= 0
\end{align}

Consider the nine equations a vector function $\mathbf{f}$ that takes a 9-vector $\mathbf{x}$ of the unknowns as argument:
\begin{align}
\mathbf{f}(\mathbf{x}) &= 0\\
\mathbf{x} &= \left(\begin{array}{c}
x_0 \\ x_1 \\ x_2 \\
x_3 \\ x_4 \\ x_5 \\
x_6 \\ x_7 \\ x_8
\end{array}\right)
 =
\left(\begin{array}{c}
\sin\theta_1 \\ \sin\theta_2 \\ \sin\theta_3 \\
\cos\theta_1 \\ \cos\theta_2 \\ \cos\theta_3 \\
T_1 \\ T_2 \\ T_3
\end{array}\right) \\
\mathbf{L} &= \left(\begin{array}{c}
L \\ L_1 \\ L_2 \\ L_3
\end{array}\right), \quad
\mathbf{W} = \left(\begin{array}{c}
W_1 \\ W_2
\end{array}\right)
\end{align}

Solve with generalized Newton-Raphson:
$$
\mathsf{J}(\mathbf{x}) \Delta\mathbf{x} = -\mathbf{f}(\mathbf{x})
$$
and
$$
\mathbf{x} \leftarrow \mathbf{x} + \Delta\mathbf{x}.
$$

## Problem setup
Set the problem parameters and the objective function $\mathbf{f}(\mathbf{x})$

```python
import numpy as np

# problem parameters
W = np.array([10, 20])
L = np.array([8, 3, 4, 4])

def f_2masses(x, L, W):
    return np.array([
            -x[6]*x[3] + x[7]*x[4],
             x[6]*x[0] - x[7]*x[1] - W[0],
            -x[7]*x[4] + x[8]*x[5],
             x[7]*x[1] + x[8]*x[2] - W[1],
             L[1]*x[3] + L[2]*x[4] + L[3]*x[5] - L[0],
            -L[1]*x[0] - L[2]*x[1] + L[3]*x[2],
            x[0]**2 + x[3]**2 - 1,
            x[1]**2 + x[4]**2 - 1,
            x[2]**2 + x[5]**2 - 1,
        ])

def fLW(x):
    return f_2masses(x, L, W)
```

### Initial values
Guess some initial values (they don't have to fullfil the equations!):

```python
# initial parameters
theta0 = np.deg2rad([45, 45, 90])
T0 = np.array([1, 1, 2])
x0 = np.concatenate([np.sin(theta0), np.cos(theta0), T0])
```

```python
x0
```

    array([  7.07106781e-01,   7.07106781e-01,   1.00000000e+00,
             7.07106781e-01,   7.07106781e-01,   6.12323400e-17,
             1.00000000e+00,   1.00000000e+00,   2.00000000e+00])

```python
f_2masses(x0, L, W)
```

    array([  0.        , -10.        ,  -0.70710678, -17.29289322,
            -3.05025253,  -0.94974747,   0.        ,   0.        ,   0.        ])

### Visualization
Plot the positions of the 2 masses and the 3 strings for any solution vector $\mathbf{x}$:

```python
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
def plot_2masses(x, L, W, **kwargs):
    """Plot 2 mass/3 string problem for parameter vector x and parameters L and W"""

    kwargs.setdefault('linestyle', '-')
    kwargs.setdefault('marker', 'o')
    kwargs.setdefault('linewidth', 1)

    r0 = np.array([0, 0])
    r1 = r0 + np.array([L[0], 0])
    rod = np.transpose([r0, r1])

    L1 = r0 + np.array([L[1]*x[3], -L[1]*x[0]])
    L2 = L1 + np.array([L[2]*x[4], -L[2]*x[1]])
    L3 = L2 + np.array([L[3]*x[5], L[3]*x[2]])
    strings = np.transpose([r0, L1, L2, L3])

    ax = plt.subplot(111)
    ax.plot(rod[0], rod[1], color="black", marker="d", linewidth=4)
    ax.plot(strings[0], strings[1], **kwargs)
    ax.set_aspect(1)
    return ax
```

What does the initial guess look like?

```python
plot_2masses(x0, L, W)
```

## Jacobian
Write a function `Jacobian(f, x, h=1e-5)` that computes the Jacobian matrix numerically (use the central difference algorithm).

```python
def Jacobian(f, x, h=1e-5):
    """df_i/dx_j with central difference (f(x+h/2)-f(x-h/2))/h"""
    J = np.zeros((len(f(x)), len(x)), dtype=np.float64)
    hvec = np.zeros_like(x, dtype=np.float64)
    for j in range(len(x)):
        hvec *= 0
        hvec[j] = 0.5*h
        J[:, j] = (f(x + hvec) - f(x - hvec))/h
    return J
```

Test Jacobian on
$$
\mathbf{f}(\mathbf{x}) = \left( \begin{array}{c}
  x_0^2 - x_1 \\ x_0
  \end{array}\right)
$$
with analytical result
$$
\mathsf{J} = \frac{\partial f_i}{\partial x_j} =
\left( \begin{array}{cc}
  2 x_0 & -1\\
  1 & 0
\end{array}\right)
$$

```python
def ftest(x):
    return np.array([
            x[0]**2 - x[1],
            x[0]
        ])
x0test = np.array([1, 0])
J = Jacobian(ftest, x0test)
print(J)
```

    [[ 2. -1.]
     [ 1.  0.]]

Test that it also works for our starting vector:

```python
Jacobian(fLW, x0)
```

    array([[ 0.        ,  0.        ,  0.        , -1.        ,  1.        ,
             0.        , -0.70710678,  0.70710678,  0.        ],
           [ 1.        , -1.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.70710678, -0.70710678,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.        , -1.        ,
             2.        ,  0.        , -0.70710678,  0.        ],
           [ 0.        ,  1.        ,  2.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.70710678,  1.        ],
           [ 0.        ,  0.        ,  0.        ,  3.        ,  4.        ,
             4.        ,  0.        ,  0.        ,  0.        ],
           [-3.        , -4.        ,  4.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ,  0.        ],
           [ 1.41421356,  0.        ,  0.        ,  1.41421356,  0.        ,
             0.        ,  0.        ,  0.        ,  0.        ],
           [ 0.        ,  1.41421356,  0.        ,  0.        ,  1.41421356,
             0.        ,  0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  2.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ,  0.        ]])

## n-D Newton-Raphson Root Finding
Write a function `newton_raphson(f, x, Nmax=100, tol=1e-8, h=1e-5)` to find a root for a vector function `f(x)=0`. (See also [11 Root-finding by trial-and-error](http://asu-compmethodsphysics-phy494.github.io/ASU-PHY494//2016/03/17/11_Root_finding/) and the _1D Newton-Raphson algorithm_ in [11_Root_finding.ipynb](https://github.com/ASU-CompMethodsPhysics-PHY494/PHY494-resources/blob/master/11_rootfinding/11_Root_finding.ipynb).) As a convergence criterion we demand that the length of the vector `f(x)` (the norm --- see `np.linalg.norm`) be less than the tolerance.

```python
def newton_raphson(f, x, Nmax=100, tol=1e-8, h=1e-5):
    """n-D Newton-Raphson: solves f(x) = 0.

    Iterate until |f(x)| < tol or nmax steps.
    """
    x = x.copy()

    raise NotImplementedError

    else:
        print("Newton-Raphson: no root found after {0} iterations (eps={1}); "
            "best guess is {2} with error {3}".format(Nmax, tol, x, fx))
    return x
```

### Solve 2 masses/3 strings string problem

#### Solution

```python

```

Plot the starting configuration and the solution:

```python
plot_2masses(x0, L, W)
plot_2masses(x, L, W)
```

Pretty-print the solution (angles in degrees):

```python
def pretty_print(x):
    theta = np.rad2deg(np.arcsin(x[0:3]))
    tensions = x[6:]
    print("theta1 = {0[0]:.1f} \t theta2 = {0[1]:.1f} \t theta3 = {0[2]:.1f}".format(theta))
    print("T1     = {0[0]:.1f} \t T2     = {0[1]:.1f} \t T3     = {0[2]:.1f}".format(tensions))
```

```python
print("Starting values")
pretty_print(x0)
print()
print("Solution")
pretty_print(x)
```

#### Show intermediate steps
Create a new function `newton_raphson_intermediates()` based on `newton_raphson()` that returns *all* trial `x` values including the last one.

```python
def newton_raphson_intermediates(f, x, Nmax=100, tol=1e-8, h=1e-5):
    """n-D Newton-Raphson: solves f(x) = 0.

    Iterate until |f(x)| < tol or nmax steps.

    Returns all intermediates.
    """
    intermediates = []
    x = x.copy()

    raise NotImplementedError

    else:
        print("Newton-Raphson: no root found after {0} iterations (eps={1}); "
            "best guess is {2} with error {3}".format(Nmax, tol, x, fx))
    return np.array(intermediates)
```

Visualize the intermediate configurations:

```python
x_series = newton_raphson_intermediates(fLW, x0)
```

```python
ax = plt.subplot(111)
ax.set_prop_cycle("color", [plt.cm.viridis_r(i) for i in np.linspace(0, 1, len(x_series))])
for x in x_series:
    plot_2masses(x, L, W)
```

It's convenient to turn the above plotting code into a function that we can reuse:

```python
def plot_series(x_series, L, W):
    """Plot all N masses/strings solution vectors in x_series (N, 9) array"""
    ax = plt.subplot(111)
    ax.set_prop_cycle("color", [plt.cm.viridis_r(i) for i in np.linspace(0, 1, len(x_series))])
    for x in x_series:
        plot_2masses(x, L, W)
    return ax
```

## Additional work
Try different masses, e.g. M1 = M2 = 10, or M1= 0 , M2 = 10.

### M1 = M2 = 10

```python
W_2 = np.array([10, 10])
def fLW_2(x):
    return f_2masses(x, L, W_2)
```

```python
x_series_2 = newton_raphson_intermediates(fLW_2, x0)
pretty_print(x_series_2[-1])
```

```python
plot_series(x_series_2, L, W_2)
```

### M1 = 0, M2 = 10
What do you expect to see?

```python

```

### Vary lengths...
Try to predict what the geometry will look like for different lengths and masses and then do the numerical calculation.

```python

```


```python
from sympy import *
from sympy.physics.units import *

e0 = var("e0")

E = Matrix([
    [2, 1, 0],
    [1, 0, 0],
    [0, 0, -1],
    ])
E *= e0

n = Matrix([1, 1, 1])
n /= sqrt(3)
nT = n.transpose()

En = E*n
es = nT*En

pprint("\ne*:")
pprint(es)

# e*:
# [e₀]

```


# Maximum Likelihood Estimate

Suppose we are given a problem where we can assume the _parametric class_ of distribution (e.g. Normal Distribution) that generates a set of data, and we want to determine the most likely parameters of this distribution using the given data. Since this class of distribution has a finite number of parameters (e.g. mean $\mu$ and standard deviation $\sigma$, in case of normal distribution) that need to be figured out in order to identify the particular member of the class, we will use the given data to do so.

The obtained parameter estimates will be called **Maximum Likelihood Estimates**.

Let us consider a Random Variable $X$ to be normally distributed with some mean $\mu$ and standard deviation $\sigma$. We need to estimate $\mu$ and $\sigma$ using our samples which accurately represent the actual $X$ and not just the samples that we have drawn out.

## Estimating Parameters

Let's have a look at the Probability Density Function (PDF) for the Normal Distribution and see what they mean.

$$
\begin{equation}
f(x; \mu, \sigma) = \frac{e^{-(x - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}}
\end{equation}
$$ (eq_normal_dist)

This equation is used to obtain the probability of our sample $x$ being from our random variable $X$, when the true parameters of the distribution are $\mu$ and $\sigma$. Normal distributions with different $\mu$ and $\sigma$ are shown below.

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
np.random.seed(10)

plt.style.use('seaborn')
plt.rcParams['figure.figsize'] = (12, 8)

def plot_normal(x_range, mu=0, sigma=1, **kwargs):
    '''
    https://emredjan.github.io/blog/2017/07/19/plotting-distributions/
    '''
    x = x_range
    y = norm.pdf(x, mu, sigma)
    plt.plot(x, y, **kwargs)

mus = np.linspace(-6, 6, 6)
sigmas = np.linspace(1, 3, 6)

assert len(mus) == len(sigmas)
x_range = np.linspace(-10, 10, 200)

for mu, sigma in zip(mus, sigmas):
    plot_normal(x_range, mu, sigma, label=f'$\mu$ = {mu:.2f}, $\sigma$ = {sigma:.2f}')
plt.legend();
```

Let us consider that our sample = 5. Then what is the probability that it comes from a normal distribution with $\mu = 4$ and $\sigma = 1$? To get this probability, we only need to plug in the values of $x, \mu$ and $\sigma$ in Equation {eq}`eq_normal_dist`. Scipy as a handy function [`norm.pdf()`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.norm.html) that we can use to obtain this easily.

```python
from scipy.stats import norm

norm.pdf(5, 4, 1)
```

    0.24197072451914337

What if our sample came from a different distribution with $\mu = 3$ and $\sigma = 2$?

```python
norm.pdf(5, 3, 2)
```

    0.12098536225957168

As we can see, the PDF equation {eq}`eq_normal_dist` shows us how likely our sample are from a distribution with certain parameters. Current results show that our sample is more likely to have come from the first distribution. But this with just a single sample. What if we had multiple samples and we wanted to estimate the parameters?

Let us assume we have multiple samples from $X$ which we assume to have come from some normal distribution. Also, all the samples are mutually independent of one another. In this case, the we can get the total probability of observing all samples by multiplying the probabilities of observing each sample individually.

E.g., The probability that both $7$ and $1$ are drawn from a normal distribution with $\mu = 4$ and $\sigma=2$ is equal to:

```python
norm.pdf(7, 4, 2) * norm.pdf(1, 4, 2)
```

    0.004193701896768355

## Likelihood of many samples

```python
x_data = np.random.randint(-9, high=9, size=5)
print(x_data)
```

    [ 0 -5  6 -9  8]

In maximum likelihood estimation (MLE), we specify a distribution of unknown parameters and then use our data to obtain the actual parameter values. In essence, MLE's aim is to find the set of parameters for the probability distribution that maximizes the likelihood of the data points. This can be formally expressed as:

$$
\begin{equation}
\hat{\mu}, \hat{\sigma} = \operatorname*{argmax}_{\mu, \sigma} \prod_{i=1}^n f(x_i)
\end{equation}
$$ (eq_likelihood)

However, it is difficult to optimize this product of probabilities because of long and messy calculations. Thus, we use log-likelihood which is the logarithm of the probability that the data point is observed. Formally Equation {eq}`eq_likelihood` can be re-written as,

$$
\begin{equation}
\hat{\mu}, \hat{\sigma} = \operatorname*{argmax}_{\mu, \sigma}\Sigma_{i=1}^n \ln f(x_i)
\end{equation}
$$ (eq_log_likelihood)

This is because logarithmic function is a monotonically increasing function. Thus, taking the log of another function does not change the point where the original function peaks. There are two main advantages of using log-likelihood:
1. The exponential terms in the probability density function are more manageable and easily optimizable.
1. The product of all likelihoods become a sum of individual likelihoods which allows these individual components to be maximized rather than working with the product of $n$ probability density functions.

Now, let us find the maximum likelihood estimates using the log likelihood function.

$$
\begin{align*}
\begin{split}
& \ln\left[ L(\mu, \sigma|x_1, ..., x_n)\right] \\
 &= \ln \left( \frac{e^{-(x_1 - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}} \times  \frac{e^{-(x_2 - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}} \times ... \times  \frac{e^{-(x_n - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}}  \right) \\
 &= \ln\left( \frac{e^{-(x_1 - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}} \right) + \ln\left( \frac{e^{-(x_2 - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}} \right) + ... \\
 &\quad + \ln\left( \frac{e^{-(x_n - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}} \right)
\end{split}
\end{align*}
$$ (eq_mle_all_terms)

Here,

$$
\begin{align*}
&\ln\left( \frac{e^{-(x_1 - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}} \right) \\
&= \ln\left( \frac{1} {\sigma\sqrt{2\pi}} \right) + \ln\left( e^{-(x_1 - \mu)^{2}/(2\sigma^{2}) } \right) \\
&= \ln\left[ (2\pi\sigma^2)^{\frac{-1}{2}} \right] - \frac{(x_1 - \mu)^2}{2\sigma^2}\ln(e) \\
&= -\frac{1}{2}\ln ( 2\pi\sigma^2) - \frac{(x_1-\mu)^2}{2\sigma^2} \\
&= -\frac{1}{2}\ln(2\pi) - \frac{1}{2}\ln(\sigma^2) - \frac{(x_1 - \mu)^2}{2\sigma^2} \\
\end{align*}
$$
$$
\begin{align*}
&= -\frac{1}{2}\ln(2\pi) - \ln(\sigma) - \frac{(x_1 - \mu)^2}{2\sigma^2} \\
\end{align*}
$$ (eq_mle_single_term)

Thus, Equation {eq}`eq_mle_all_terms` can be written as:

$$
\begin{align*}
\ln\left[ L(\mu, \sigma|x_1, ..., x_n)\right] &= \ln\left( \frac{e^{-(x_1 - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}} \right) + \ln\left( \frac{e^{-(x_2 - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}} \right) + ... \\
 &\quad + \ln\left( \frac{e^{-(x_n - \mu)^{2}/(2\sigma^{2}) }} {\sigma\sqrt{2\pi}} \right) \\
&= \left[ -\frac{1}{2}\ln(2\pi) - \ln(\sigma) - \frac{(x_1 - \mu)^2}{2\sigma^2} \right] \\
&\quad + \left[ -\frac{1}{2}\ln(2\pi) - \ln(\sigma) - \frac{(x_2- \mu)^2}{2\sigma^2} \right] \\
&\quad + ... + \left[ -\frac{1}{2}\ln(2\pi) - \ln(\sigma) - \frac{(x_n - \mu)^2}{2\sigma^2} \right] \\
\end{align*}
$$
$$
\begin{align*}
&= -\frac{n}{2}\ln(2\pi) - n\ln(\sigma) - \frac{(x_1-\mu)^2}{2\sigma^2} - \frac{(x_2-\mu)^2}{2\sigma^2} - ... - \frac{(x_n-\mu)^2}{2\sigma^2}
\end{align*}
$$ (eq_mle_simplified)

## Values of parameters

Now, we will use Equation {eq}`eq_mle_simplified` to find the values of $\mu$ and $\sigma$. For this purpose, we take the partial derivative of Equation {eq}`eq_mle_simplified` with respect to $\mu$ and $\sigma$.

$$
\begin{align*}
\frac{\partial}{\partial \mu}\ln\left[L(\mu, \sigma|x_1, x_2, ..., x_n) \right] &= 0 - 0 + \frac{x_1 - \mu}{\sigma^2} + \frac{x_2 - \mu}{\sigma^2} + ... + \frac{x_n - \mu}{\sigma^2} \\
\end{align*}
$$
$$
\begin{align*}
\frac{\partial}{\partial \mu}\ln\left[L(\mu, \sigma|x_1, x_2, ..., x_n) \right] &= \frac{1}{\sigma^2}\left[ (x_1 + x_2 + ... + x_n) - n\mu \right]
\end{align*}
$$ (eq_mu)

$$
\begin{align*}
\frac{\partial}{\partial \sigma}\ln\left[L(\mu, \sigma|x_1, x_2, ..., x_n) \right] &= 0 - \frac{n}{\sigma} + \frac{(x_1 - \mu)^2}{\sigma^3} + \frac{(x_2 - \mu)^2}{\sigma^3} + ... + \frac{(x_n - \mu)^2}{\sigma^3} \\
\end{align*}
$$
$$
\begin{align*}
\frac{\partial}{\partial \sigma}\ln\left[L(\mu, \sigma|x_1, x_2, ..., x_n) \right] &= -\frac{n}{\sigma} + \frac{1}{\sigma^3}\left[ (x_1 - \mu)^2 + (x_2 - \mu)^2 + ...+ (x_n - \mu)^2 \right]
\end{align*}
$$ (eq_sigma)

Now, to find the maximum likelihood estimate for $\mu$ and $\sigma$, we need to solve for the derivative with respect to $\mu = 0$ and $\sigma = 0$, because the slope is 0 at the peak of the curve.

Thus, using Equation {eq}`eq_mu` and setting $\frac{\partial}{\partial \mu}\ln\left[L(\mu, \sigma|x_1, x_2, ..., x_n) \right] = 0$, we get,

$$
\begin{align*}
0 &= \frac{1}{\sigma^2}\left[ (x_1 + x_2 + ... + x_n) - n\mu \right] \\
0 &= (x_1+x_2 + ... + x_n) - n\mu \\
\end{align*}
$$
$$
\begin{align*}
\mu &= \frac{(x_1+x_2+...+x_n)}{n}
\end{align*}
$$ (eq_mu_final)

Thus, the maximum likelihood estimate for $\mu$ is the mean of the samples.

Simialrly, using Equation {eq}`eq_sigma` and setting $\frac{\partial}{\partial \sigma}\ln\left[L(\mu, \sigma|x_1, x_2, ..., x_n) \right] = 0$, we get,

$$
\begin{align*}
0 &= -\frac{n}{\sigma} + \frac{1}{\sigma^3}\left[ (x_1 - \mu)^2 + (x_2 - \mu)^2 + ...+ (x_n - \mu)^2 \right] \\
0 &= -n + \frac{1}{\sigma^2}\left[ (x_1-\mu)^2 + (x_2-\mu)^2 + ...+ (x_n-\mu)^2 \right] \\
n\sigma^2 &= (x_1-\mu)^2 + (x_2-\mu)^2 + ...+ (x_n-\mu)^2 \\
\end{align*}
$$
$$
\begin{align*}
\sigma &= \sqrt{\frac{(x_1-\mu)^2 + (x_2-\mu)^2 + ...+ (x_n-\mu)^2}{n}} \\
\end{align*}
$$ (eq_sigma_final)

Thus, the maximum likelihood estimate for $\sigma$ is the standard deviation of the samples.

## An Example

Let us now consider 5 samples with values 0, -5,  6, -9 and 8. We want to know the normal distribution from which all of these samples were most likely to be drawn. In other words, we would like to maximize the value of $f(0, -5,  6, -9, 8)$ as given in Equation {eq}`eq_normal_dist`. Since we do not know the values of $\mu$ and $\sigma$ for the required distribution, we need to estimate them using  Equations {eq}`eq_mu_final` and {eq}`eq_sigma_final` respectively.

Using the formulae of $\mu$ and $\sigma$, we get,

```python
samples = np.array([0, -5, 6, -9, 8])
mu = np.mean(samples)
sigma = np.std(samples)
print(f'mu = {mu:.2f} and sigma = {sigma:.2f}')
```

    mu = 0.00 and sigma = 6.42

Let us plot the normal distribution with these values and also mark the given points.

```python
x_range = np.linspace(-20, 20, 200)

plot_normal(x_range, mu, sigma)
plt.axhline(y=0)
plt.vlines(samples, ymin=0, ymax=norm.pdf(samples, mu, sigma), linestyle=':')

plt.plot(samples, [0]*samples.shape[0], 'o', zorder=10, clip_on=False);
```

Thus, this is the most likely normal distribution from which the five sample points were drawn out.

## References

1. [What is Maximum Likelihood Estimation — Examples in Python | Medium.com](https://medium.com/@rrfd/what-is-maximum-likelihood-estimation-examples-in-python-791153818030)

1. [Maximum Likelihood Estimation: How it Works and Implementing in Python | towardsdatascience.com](https://towardsdatascience.com/maximum-likelihood-estimation-how-it-works-and-implementing-in-python-b0eb2efb360f)

1. [Maximum Likelihood For the Normal Distribution, step-by-step! | YouTube](https://youtu.be/Dn6b9fCIUpM)


## 4.7 优化理论

优化理论是科学研究的另一个重要方向，它的关键在于使用数学模型对现实问题建模，并在若干约束的条件下，求问题的最优解，主要分为单目标优化、多目标优化两种类型。不管是单目标优化还是多目标优化，其数学表达式的主要内容均包括目标函数及约束条件两个主要部分。

【**例1**】书写如下带约束的优化问题：

\begin{equation}
\begin{aligned}
&\min_{x}\,f(x) \\
&\text{s.t.}\,x\in\mathcal{X},\quad g_{j}\left(x\right)\leq 0,\,j=1,\ldots,r
\end{aligned}
\end{equation}

其中，min是minimize的缩写，s.t.是subject to的缩写。

```tex
\documentclass[12pt]{article}
\begin{document}

\begin{equation}
\begin{aligned}
&\min_{x}\,f(x) \\
&\text{s.t.}\,x\in\mathcal{X},\quad g_{j}\left(x\right)\leq 0,\,j=1,\ldots,r
\end{aligned}
\end{equation}

\end{document}
```

【**例2**】书写$x^{*}\in\operatorname{arg}\min_{x\in\mathcal{X}}~L(x,\lambda^{*})$。

```tex
\documentclass[12pt]{article}
\begin{document}

$$x^{*}\in\operatorname{arg}\min_{x\in\mathcal{X}}~L(x,\lambda^{*})$$

\end{document}
```

【**例3**】书写一个由有限个向量$\boldsymbol{x}_{1},\ldots,\boldsymbol{x}_{m}$构成的集合：

$$\operatorname{conv}\left(\{\boldsymbol{x}_{1},\cdots,\boldsymbol{x}_{m}\}\right)
=\left\{\sum_{i=1}^{m}\alpha_{i}\boldsymbol{x}_{i}
\bigm|\alpha_{i}\geq 0,\,i=1,\ldots,m,\,\sum_{i=1}^{m}\alpha_{i}=1\right\}$$

```tex
\documentclass[12pt]{article}
\usepackage{mathtools}
\begin{document}

$$\operatorname{conv}\left(\{\boldsymbol{x}_{1},\cdots,\boldsymbol{x}_{m}\}\right)
=\left\{\sum_{i=1}^{m}\alpha_{i}\boldsymbol{x}_{i}
\bigm|\alpha_{i}\geq 0,\,i=1,\ldots,m,\,\sum_{i=1}^{m}\alpha_{i}=1\right\}$$

\end{document}
```

【**例4**】书写

\begin{equation}
\begin{aligned}
f^{\star}(y)&=\sup_{x\in\mathbb{R}^{n}}~\left\{xy-f(x)\right\},\quad y\in\mathbb{R}^{n} \\
f^{\star\star}(x)&=\sup_{y\in\mathbb{R}^{n}}~\left\{xy-f^{\star}(y)\right\},\quad x\in\mathbb{R}^{n}
\end{aligned}
\end{equation}

其中，sup是supremum的缩写，表示上确界。

```tex
\documentclass[12pt]{article}
\usepackage{amssymb, amsfonts}
\begin{document}

\begin{equation}
\begin{aligned}
&f^{\star}(y)=\sup_{x\in\mathbb{R}^{n}}~\left\{xy-f(x)\right\},\quad y\in\mathbb{R}^{n} \\
&f^{\star\star}(x)=\sup_{y\in\mathbb{R}^{n}}~\left\{xy-f^{\star}(y)\right\},\quad x\in\mathbb{R}^{n}
\end{aligned}
\end{equation}

\end{document}
```

【**例5**】书写

\begin{equation}
f(x)=\inf_{z\in\mathbb{R}^{m}}~F(x,z),\quad x\in\mathbb{R}^{n}
\end{equation}

其中，inf是infimum的缩写，表示下确界。

```tex
\documentclass[12pt]{article}
\usepackage{amssymb, amsfonts}
\begin{document}

\begin{equation}
f(x)=\inf_{z\in\mathbb{R}^{m}}~F(x,z),\quad x\in\mathbb{R}^{n}
\end{equation}

\end{document}
```

【**例6**】书写

\begin{equation}
p(u)=\inf_{x\in\mathcal{X}}\sup_{z\in\mathcal{Z}}~\left\{\phi(x,z)-u'z\right\},\quad u\in\mathbb{R}^{m}
\end{equation}

```tex
\documentclass[12pt]{article}
\usepackage{amssymb, amsfonts}
\begin{document}

\begin{equation}
p(u)=\inf_{x\in\mathcal{X}}\sup_{z\in\mathcal{Z}}~\left\{\phi(x,z)-u'z\right\},\quad u\in\mathbb{R}^{m}
\end{equation}

\end{document}
```

### 练习题

> 打开LaTeX在线系统[https://www.overleaf.com](https://www.overleaf.com/project)或本地安装好的LaTeX编辑器，创建名为LaTeX_practice的项目，并同时新建一个以`.tex`为拓展名的源文件，完成以下几道练习题。

1. 书写

\begin{equation}
\begin{aligned}
q(\mu)=&\inf_{u\in\mathbb{R}^{m}}~\left\{p(u)+u'\mu\right\} \\
=&\inf_{u\in\mathbb{R}^{m}}\inf_{x\in\mathcal{X}}~\left\{p_{x}(u)+u'\mu\right\} \\
=&\inf_{x\in\mathcal{X}}\inf_{u\in\mathbb{R}^{m}}~\left\{p_{x}(u)+u'\mu\right\} \\
=&\inf_{x\in\mathcal{X}}~\left\{-p_{x}^{\star}(-\mu)\right\}
\end{aligned}
\end{equation}

```tex
\documentclass[12pt]{article}
\usepackage{amssymb, amsfonts}
\begin{document}

\begin{equation}
\begin{aligned}
% 在此处书写公式
\end{aligned}
\end{equation}

\end{document}
```

【回放】[**4.6 概率论与数理统计**](https://nbviewer.jupyter.org/github/xinychen/latex-cookbook/blob/main/chapter-4/section6.ipynb)

### License

This work is released under the MIT license.


```python
############### Gradient Descent ##########################

"""
This cell is for the function f(x,y)=x^2+2y^2 only

The next cell is for the method in general with symbolic math
"""

import numpy as np

x=[]

## funcion x^2+2y^2

x.append([2,1]) # punto inicial

grad = [2*x[0][0],4*x[0][1]] #gradiente [2x,4y]
k=0
print("Punto inicial:"+str(x[0]))

while np.linalg.norm(grad)>0.00001:

    a=grad[0]**2+2*grad[1]**2 # valor del coeficiente cuadratico en obtencion de t
    b=-(2*x[k][0]*grad[0]+4*x[k][1]*grad[1]) # valor coeficiente lineal en obtencion de t
    grad=[2*x[k][0],4*x[k][1]] #gradiente [2x,4y]

    t=-1*b/2/a #valor de t
    x.append([np.round(x[k][0]-t*grad[0],3), np.round(x[k][1]-t*grad[1],3)]) # calculo del proximo x
    print("Iteration "+str(k)+", Gradient: "+str(grad)+ ", step: "+str(t)+", point: "+str(x[k]))

    k+=1
    grad=[2*x[k][0],4*x[k][1]] #gradiente [2x,4y]

```

    Punto inicial:[2, 1]
    Iteration 0, Gradient: [4, 4], step: 0.3333333333333333, point: [2, 1]
    Iteration 1, Gradient: [1.334, -1.332], step: 0.33350012499990617, point: [0.667, -0.333]
    Iteration 2, Gradient: [0.444, 0.444], step: 0.3333333333333333, point: [0.222, 0.111]
    Iteration 3, Gradient: [0.148, -0.148], step: 0.3333333333333333, point: [0.074, -0.037]
    Iteration 4, Gradient: [0.05, 0.048], step: 0.3379290939786157, point: [0.025, 0.012]
    Iteration 5, Gradient: [0.016, -0.016], step: 0.3333333333333333, point: [0.008, -0.004]
    Iteration 6, Gradient: [0.006, 0.004], step: 0.3823529411764706, point: [0.003, 0.001]
    Iteration 7, Gradient: [0.002, -0.004], step: 0.27777777777777773, point: [0.001, -0.001]

```python
##upgrade##

############### Symbolic Gradient Descent ###################

from sympy import symbols,diff, Symbol, Function
import numpy as np
from sympy.solvers import solve

def normalize(v):
    norm = np.linalg.norm(v)
    if norm == 0:
       return v
    return v / norm

np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})

n = 2 # numero de variables
z = [Symbol('z'+str(i+1)) for i in range(n)] # definicion de variables z1,z2,...,zn

#f = z[0]**4+4*z[1]**4+10*z[0]**3- 2*z[1]**2 #definicion de funcion f(z)

f = z[0]**2 + 2*z[1]**2 #funcion simple

#f = (z[0]-2)**2 + (z[1]-1)**2 -z[2]*(z[0]+z[1]-1) con esta funcion no converge

gradient=[f.diff(zi) for zi in z] # calculo del gradiente f'(z)

x=[np.array([5,5])] #iteraciones del metodo del gradiente, valor inicial x[0]=(2,1)

t=0.3 #valor del paso fijo
maxiter=1000 #numero maximo de iteraciones
print("Punto inicial: "+str(x[0]))

#t=Symbol("t")

for k in range(maxiter):
    nablaf_xk=np.array([nf.subs([(z[i],x[k][i]) for i in range(n)]) for nf in gradient],dtype="float32") #evaluar x[k] en gradiente

    #t=solve(diff(f.subs([(z[i],x[k][i]-t*nablaf_xk[i]) for i in range(n)]), t)) por aoora solo calcula puntos criticos, por lo que se puede ir al maximo

    x.append(x[k]-t*nablaf_xk)

    if np.linalg.norm(nablaf_xk)<0.001: # termino del algoritmo
        print("Optimo: "+str(x[k+1]))
        break
    else:
        print("Iteration "+str(k+1)+", gradient: "+str(nablaf_xk)+", x: "+str(x[k+1]))

```

    Punto inicial: [5 5]
    Iteration 1, gradient: [10.000 20.000], x: [2.000 -1.000]
    Iteration 2, gradient: [4.000 -4.000], x: [0.800 0.200]
    Iteration 3, gradient: [1.600 0.800], x: [0.320 -0.040]
    Iteration 4, gradient: [0.640 -0.160], x: [0.128 0.008]
    Iteration 5, gradient: [0.256 0.032], x: [0.051 -0.002]
    Iteration 6, gradient: [0.102 -0.006], x: [0.020 0.000]
    Iteration 7, gradient: [0.041 0.001], x: [0.008 -0.000]
    Iteration 8, gradient: [0.016 -0.000], x: [0.003 0.000]
    Iteration 9, gradient: [0.007 0.000], x: [0.001 -0.000]
    Iteration 10, gradient: [0.003 -0.000], x: [0.001 0.000]
    Iteration 11, gradient: [0.001 0.000], x: [0.000 -0.000]
    Optimo: [0.000 0.000]

```python
import plotly.graph_objects as go

xd=np.linspace(-5,5,101)
yd=np.linspace(-5,5,101)
X,Y=np.meshgrid(xd,yd)
Z=np.array([[f.evalf(subs = dict(zip(z,[x,y]))) for x in xd] for y in yd]).astype(float)
fig = go.Figure()
fig.add_trace(go.Surface(z=Z, x=X, y=Y, opacity=0.3))

xp=np.round([p[0] for p in x])
yp=np.round([p[1] for p in x])
zp=np.array([f.evalf(subs = dict(zip(z,[p[0],p[1]]))) for p in x]).astype(float)

fig.add_trace(go.Scatter3d(x=xp, y=yp, z=zp,marker=dict(size=4,color="red"),
                                line=dict(color='darkblue',width=2)))

fig.update_layout(title='Gradient Descent', autosize=True,
                  width=1000, height=1000,
                  margin=dict(l=65, r=50, b=65, t=90))

fig.show()

```

<meta charset="utf-8" />

```python

```