# [first order differential eqations](https://people.math.wisc.edu/~angenent/519.2016s/notes/elementary-diffeqs.html)
> ## First Order Separable Equation
>> ## $
\frac{dy}{dx} = f(x,y) \\
y'(x) = F(x)G(y(x)), \quad or \quad \frac{dy}{dx} = F(x)G(y)
$
>>> ### not separable $ \frac{dy}{dx} = x + y \quad or \quad y' = x + y$

# The simplest case
> ## $ \frac{dx}{dt} = f(t), \quad x(t_0) = x_0$
>> ## $ x(t) = x_0 + \int_{t_0}^{t} f(s)ds $

# The general case
> ## $ \frac{dx}{dt} = f(t,x), \quad x(t_0) = x_0 $
>> ## $ x_0 - \delta \leq x \leq x_0 + \delta , \quad t_{-}  \leq t \leq t_{+} $

# [Proof by Picard iteration of Exisence Theorem](https://people.math.wisc.edu/~angenent/519.2016s/notes/picard.html#mjx-eqn-eqsimplest-ODE)
> ## $
x_0(t) = x_0 \\
\text{which gives us a new function } x_1(t)
$
>> ## $
\frac{dx_1}{dt} = f(t,x_0), \quad x_0(t_0) = x_0 \\
\frac{dx_2}{dt} = f(t,x_1(t)), \quad x_1(t0) = x_0 \\
\dots \\
\frac{dx_n}{dt} = f(t, x_{n-1}(t), \quad x_{n-1}(t_0) = x_0 \\
\therefore x(t) \overset{def}{=} \lim \limits_{k\to\infty} x_k(t) \\
$
> ## $$
x_k(t) = x_0 + \int_{t_0}^{t} f(s,x_{k-1}(s))ds \\
x_{k+1}(t) = x_0 + \int_{t_0}^{t} f(s,x_{k}(s))ds \\
$$
>
> ## $
\begin{cases}
k \to \infty, \Rightarrow x_k(t) \to x(t) \\
k \to \infty, \Rightarrow y_k(x) \to y(x)
\end{cases}
$
> ## $$
x(t) = x_0 + \int_{t_0}^{t} f(s,x(s))ds \\
y(x) = y_0 + \int_{x_0}^{x} f(t,x(t))dt \\
$$

```python
import sympy as sym
t, x = sym.symbols('t x')
f,y = sym.symbols('f y',cls=sym.Function)
f.diff(x)

# y' = x + y
y = sym.dsolve(f(x).diff(x) - x - f(x),f(x))
C1,_ = y.free_symbols
y = y.subs(C1,2).simplify()

# y'= x + y
k1 = 2 + sym.integrate(t+2,(t,0,x))
k2 = 2 + sym.integrate(t+k1.subs(x,t),(t,0,x))
k3 = 2 + sym.integrate(t+k2.subs(x,t),(t,0,x))
k4 = 2 + sym.integrate(t+k3.subs(x,t),(t,0,x))
k5 = 2 + sym.integrate(t+k4.subs(x,t),(t,0,x))
k5
```

$\displaystyle \frac{x^{6}}{720} + \frac{x^{5}}{40} + \frac{x^{4}}{8} + \frac{x^{3}}{2} + \frac{3 x^{2}}{2} + 2 x + 2$

# [Picards method](https://www.youtube.com/watch?v=ZpvUyOvoNHg)
> # $ y'(x) = f(x,y) $
>> # $$ \begin{align}
    & y(x_0) & = \quad & y_0 \quad \text{, given initial value}  \\
    & \frac{d\, y(x)}{dx} & =\quad  & f(x,y) \\
    & dy(x) & =\quad  & f(x,y)\,dx \\
    & \int_{y_0}^{y}dy(x) & =\quad  & \int_{x_0}^{x}f(t,y(x)) dx \\
    & y - y_0 & =\quad  & \int_{x_0}^{x}f(x,y)dx \\
    & \therefore y & =\quad  & y_0 + \int_{x_0}^{x}f(x,y)dx \\
\end{align}$$
>
> # first approximation method
>> # $$ y(x) = y_{0} + \int_{x_0}^{x} f(x,y(x)) dx $$
>> # $$ y(x) = y_{0} + \int_{x_0}^{x} f(t,y(t)) dt $$

# [ODE]( https://www.youtube.com/watch?v=C0Hl8DXDJ2M&list=PLW1_9UnhaSkEdN19b2KTQm1ShFGdHHbIJ&index=2)
# $
\begin{cases}
y = ce^x -x -1
        \begin{cases}
           y' & = \frac{d}{dx}(ce^x - x -1) \\
           & = ce^x -1 \\
           & = ce^x -1 -x + x \\
           & = y + x\\
        \end{cases} \\
\therefore y' = y + x \\
\end{cases}
$

## $ y'' =  a^2 y $
> ## $ y = c_{1}sinh(ax)+c_{2}cosh(ax)$
>> ## $ \because \text{Recall: } \begin{cases}
        \frac{d}{dx}sinh(x) = cosh(x) \\
        \frac{d}{dx}cosh(x)  = sinh(x)
        \end{cases} $
> ## $ y = c_1 sinh(ax) + c_2 cosh(ax)$
> ## $ y' = a c_1 cosh(ax) + ac_2sinh(ax)$
> ## $ y'' = a^2 c_1 sinh(ax) + a^2c_2cosh(ax)$

```python
import sympy as sym
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as pat

x,y,a,b,c,c1,c2 = sym.symbols(r'x y alpha beta c c_1 c_2')
f = sym.symbols('f',cls=sym.Function)
fx =  c1*sym.sinh(a*x) + c2*sym.cosh(a*x)
sym.diff(fx,x,x) == a**2*fx
```

    True

```python
g = c*sym.exp(x) - x
dgdx = sym.diff(dgdx,x)
dgdx == g + x
```

    True

```python
def h(t,x):
    return x * sym.integrate(sym.sin(t)/t,(t,0,x))

t = sym.symbols('t')
f = x*sym.integrate(sym.sin(t)/t,(t,0,x))
sym.diff(h(t,x),x) == sym.sin(x) + f/x
```

    True

```python

```

```python
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot()
x = np.linspace(-7,7,1000)
ax.plot(x,np.sinh(x))
ax.plot(x,np.cosh(x))
ax.axis('scaled')
ax.set_xlim(xmin=-7,xmax=7)
ax.set_ylim(ymin=-7,ymax=7)
```

```python
import sympy as sym
x,y = sym.symbols('x y')
f,g = sym.symbols('f g', cls=sym.Function)
f = f(x)

sym.dsolve(f.diff(x)-1-f**2,f.simplify())
sym.dsolve(f.diff(x) + (x+2)*f**2,f,hint='separable')
sym.dsolve(g(x).diff(x)-2*sym.sec(2*g(x)),g(x),hint='separable')
```

    [Eq(g(x), -asin(C1 + 4*x)/2 + pi/2), Eq(g(x), asin(C1 + 4*x)/2)]

```python
#sym.dsolve(2*x*f(x)*f(x).diff(x)-f(x)**2 + x**2,f(x),hint='separable_reduced')
sym.dsolve(2*x*f*f.diff(x)-f**2 + x**2,f,simplify=False)
```

$\displaystyle x + \frac{f^{2}{\left(x \right)}}{x} = C_{1}$

```python
sym.dsolve(f.diff(x) - (4*x**2+f**2)/x*f,f,simplify=False)
```

$\displaystyle \frac{1}{f^{2}{\left(x \right)}} = \left(C_{1} - \operatorname{Ei}{\left(4 x^{2} \right)}\right) e^{- 4 x^{2}}$

```python
import sympy as sym
x,h = sym.symbols('x h')
f = lambda x:x/(x-1)
f(x+h)
Df = (f(x+h) - f(x))/h
Df.simplify()
a = sym.limit(Df,h,0)
a.factor() == sym.diff(f(x),x).factor()
sym.limit(sym.sin(x)/x,x,0)
```

$\displaystyle 1$

```python
sym.Integral(sym.exp(-x**2),x).doit()
sym.Integral(sym.exp(-x**2),x)
```

$\displaystyle \int e^{- x^{2}}\, dx$

```python
sym.integrate(sym.exp(-x**2),(x,-sym.oo,sym.oo))
```

$\displaystyle \sqrt{\pi}$

# Matrices
$$
A = \begin{pmatrix}
1 & 2 \\
3 & 4 \\
\end{pmatrix}
$$

```python
A = [[1,2],[3,4]]
B = [[5,6],[7,8]]
A = sym.Matrix(A)
B = sym.Matrix(B)
```

```python
A + B
```

$\displaystyle \left[\begin{matrix}6 & 8\\10 & 12\end{matrix}\right]$

```python
A * B
```

$\displaystyle \left[\begin{matrix}19 & 22\\43 & 50\end{matrix}\right]$

```python
A.det()
A.T
```

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

# Chain rule

```python
t = sym.symbols('t')
x,y,z,w = sym.symbols('x y z w',cls=sym.Function)
x = x(t)
y = y(t)
z = z(t)
w = w(x,y,z)
w
```

$\displaystyle w{\left(x{\left(t \right)},y{\left(t \right)},z{\left(t \right)} \right)}$

```python
sym.diff(w,t)
```

$\displaystyle \frac{d}{d x{\left(t \right)}} w{\left(x{\left(t \right)},y{\left(t \right)},z{\left(t \right)} \right)} \frac{d}{d t} x{\left(t \right)} + \frac{d}{d y{\left(t \right)}} w{\left(x{\left(t \right)},y{\left(t \right)},z{\left(t \right)} \right)} \frac{d}{d t} y{\left(t \right)} + \frac{d}{d z{\left(t \right)}} w{\left(x{\left(t \right)},y{\left(t \right)},z{\left(t \right)} \right)} \frac{d}{d t} z{\left(t \right)}$

```python
w1 = x**2 + sym.exp(y) * sym.sin(z)
sym.diff(w1,t).subs([(x,sym.sin(t)),(y,sym.cos(t)),(z,t**2)]).doit()
```

$\displaystyle 2 t e^{\cos{\left(t \right)}} \cos{\left(t^{2} \right)} - e^{\cos{\left(t \right)}} \sin{\left(t \right)} \sin{\left(t^{2} \right)} + 2 \sin{\left(t \right)} \cos{\left(t \right)}$

# Vector CoordSys3D
> ### gradient

```python
C = sym.vector.CoordSys3D('')
f = C.x * sym.sin(C.y)
f
```

$\displaystyle \mathbf{{x}_{}} \sin{\left(\mathbf{{y}_{}} \right)}$

```python
sym.vector.gradient(f)
```

$\displaystyle (\sin{\left(\mathbf{{y}_{}} \right)})\mathbf{\hat{i}_{}} + (\mathbf{{x}_{}} \cos{\left(\mathbf{{y}_{}} \right)})\mathbf{\hat{j}_{}}$

```python
sym.vector.gradient(f).to_matrix(C)
```

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

```python
sym.vector.gradient(f).to_matrix(C).subs(C.y,sym.pi)
```

$\displaystyle \left[\begin{matrix}0\\- \mathbf{{x}_{}}\\0\end{matrix}\right]$

# Directional Derivatives
$$
D_uf = \Delta f\cdot u
$$

```python
u = 4*C.i - 3*C.j + 2*C.k
u = u.normalize()
u
```

$\displaystyle (\frac{4 \sqrt{29}}{29})\mathbf{\hat{i}_{}} + (- \frac{3 \sqrt{29}}{29})\mathbf{\hat{j}_{}} + (\frac{2 \sqrt{29}}{29})\mathbf{\hat{k}_{}}$

```python
sym.vector.gradient(f).dot(u)
```

$\displaystyle - \frac{3 \sqrt{29} \mathbf{{x}_{}} \cos{\left(\mathbf{{y}_{}} \right)}}{29} + \frac{4 \sqrt{29} \sin{\left(\mathbf{{y}_{}} \right)}}{29}$

# [Extreme Values and Saddle Points](https://www.youtube.com/watch?v=Teb28OFMVFc)
Extream values of f(x,y) can occur at
> Boundary points of the domain of f
> Critical Points($f_x = f_y = 0$)
> if
>> $ f_{xx} \lt 0 $ and $ f_{xx}f_{yy} - f{xy}^2 \gt 0 $ at (a,b) then local maximum
>> $ f_{xx} \gt 0 $ and $ f_{xx}f_{yy} - f{xy}^2 \gt 0 $ at (a,b) then local minimum
>> $ f_{xx}f_{yy} - f_{xy}^2 \lt 0$ at (a,b) then saddle point
>> $ f_{xx}f_{yy} - f_{xy}^2 = 0$ at (a,b) then inconclusive

```python
x,y = sym.symbols('x y')
f = x**3 + 3*x*y + y**3
f
```

$\displaystyle x^{3} + 3 x y + y^{3}$

```python
sym.solve([sym.diff(f,x),sym.diff(f,y)])
```

    [{x: -1, y: -1},
     {x: 0, y: 0},
     {x: -(1/2 - sqrt(3)*I/2)**2, y: 1/2 - sqrt(3)*I/2},
     {x: -(1/2 + sqrt(3)*I/2)**2, y: 1/2 + sqrt(3)*I/2}]

```python
fxx = sym.diff(f,x,x)
fyy = sym.diff(f,y,y)
fxy = sym.diff(f,x,y)
```

$\displaystyle 3$

# testing the first point (-1,-1)

```python
fxx.subs([(x,-1),(y,-1)])
```

$\displaystyle -6$

```python
(fxx * fyy - fxy**2).subs([(x,-1),(y,-1)])
# fxx < 0 and fxx * fyy - fxy**2 > 0 => local maximum
```

$\displaystyle 27$

```python

```


# 분류 최적화
Classification Optimization

```python
import matplotlib.pyplot as plt
import numpy as np
import numpy.random as nr
import scipy.optimize as so

```

$(x_1, x_2)$ 데이터 집합 두개 생성
Generating two data sets

```python
n = 1000

set_0 = nr.normal([2, 0], [1, 1], (n, 2))
set_1 = nr.normal([0, 2], [1, 1], (n, 2))

```

생성한 두 데이터 집합을 표시

Visualizing the two data sets

```python
def plot_two_sets(set_a, set_b):

    plt.plot(set_a[:, 0], set_a[:, 1], '.', label="y=0", alpha=0.5)
    plt.plot(set_b[:, 0], set_b[:, 1], '+', label="y=1", alpha=0.5)
    plt.grid(True)
    plt.axis('equal')
    plt.xlabel("$x_1$")
    plt.ylabel("$x_2$")

```

```python
plot_two_sets(set_0, set_1)

plt.legend(loc=0)
plt.show()
plt.close();

```

## 데이터 준비
Prepare data

모든 입력 값과 출력 값을 하나의 배열 안에 모음

Collect all input & output values into one `numpy.ndarray`

| 입력 input | 출력 output |
|:----------:|:-----------:|
| $(x_1, x_2)$ | 0 |
| $(x_1, x_2)$ | 1 |

```python
y0 = np.zeros((len(set_0), 1))
y1 = np.ones((len(set_1), 1))

data_0 = np.concatenate([set_0, y0], axis=1)
data_1 = np.concatenate([set_1, y1], axis=1)

data = np.concatenate([
        data_0,
        data_1
    ], axis=0)

```

행과 열의 갯수 확인

Check the number of rows and columns

```python
data.shape

```

처음 10개의 data
First 10 data points

```python
data[:10, :]

```

마지막 10개의 data
Last 10 data points

```python
data[-10:, :]

```

## 첫번째 (순진한) 시도
First (naive) attempt

### 모델
Model

$x_1$, $x_2$ 로부터 $y$ 값을 추정

Estimate $y$ from $x_1$ and $x_2$

$$
\hat y = w_1 x_1 +  x_2 + w_2
$$

```python
def wx(w:np.ndarray, x_y:np.ndarray) -> np.ndarray:
    w1 = w[0]
    w2 = w[1]

    x1 = x_y[:, 0]
    x2 = x_y[:, 1]

    return w1 * x1 + x2 + w2

```

이 $\hat y$ 값이 0.5 보다 크면 1, 아니면 0 인 것으로 가정.
Let's assume it was one if this $\hat y$ is larger than 0.5. Zero otherwise.

### 비용 함수
Cost function

$$
C = \frac{1}{n}\sum_{i=1}^{n} \left( \hat y_i - y_i \right)^2
$$

예상되는 문제점?

Can you expect any possible issues?

```python
def cost_function_first_attempt(w:np.ndarray, x_y:np.ndarray) -> float:
    n = len(x_y)
    y_hat = wx(w, x_y)
    y = x_y[:, -1]

    error = y_hat - y
    error_sqr = error ** 2
    result = error_sqr.sum() / n

    return result

```

최적화

Optimize

```python
result = so.fmin(cost_function_first_attempt, x0=nr.rand(2,), args=(data,), full_output=True)
weights, cost_value, n_iter, n_call, warning = result
result

```

$\hat y = 0.5$

$$
\begin{align}
    \hat y &= 0.5 \\
    w_1 x_1 +  x_2 + w_2 &= 0.5 \\
     x_2 &= -w_1 x_1 -w_2 + 0.5
\end{align}
$$

```python
def plot_decision_boundary(x_min, x_max, weights_array):
    x1_array = np.linspace(x_min, x_max)
    x2_array = - weights_array[0] * x1_array - weights_array[1] + 0.5

    plt.plot(x1_array, x2_array, label="$\hat y = 0.5$")

```

```python
plot_two_sets(set_0, set_1)
plot_decision_boundary(data[:, 0].min(), data[:, 0].max(), weights)

plt.legend(loc=0)
plt.show()
plt.close();

```

## 계단함수
Step function

0 또는 1로 바꾸어주는 함수

A function generating 0 or 1

$$
s(z)=
    \begin{cases}
        0, z < 0\\
        1, z >= 0 \\
    \end{cases}
$$

```python
def step(z):
    return np.heaviside(z, 0)

```

```python
z_array = np.linspace(-10, 10)
g_z_array = step(z_array)
plt.plot(z_array, g_z_array)
plt.grid(True)
plt.xlabel('$z$')
plt.ylabel("$g(z)$")
plt.show()

```

계단 함수를 사용하는 비용함수

Cost function using the sigmoid function

```python
def cost_function_step(w:np.ndarray, x_y:np.ndarray) -> float:
    n = len(x_y)
    y_hat = step(wx(w, x_y))
    y = x_y[:, -1]

    error = y_hat - y
    error_sqr = error ** 2
    result = error_sqr.sum() / n

    return result

```

최적화

Optimize

```python
result = so.fmin(cost_function_step, x0=nr.rand(2,), args=(data,), full_output=True)
weights, cost_value, n_iter, n_call, warning = result
result

```

```python
plot_two_sets(set_0, set_1)
plot_decision_boundary(data[:, 0].min(), data[:, 0].max(), weights)

plt.legend(loc=0)
plt.show()
plt.close();

```

## Sigmoid function

0 과 1 사이를 부드럽게 연결하는 함수

A function connecting 0 and 1 smoothly

ref : [](https://youtu.be/PIjno6paszY?t=650)

$$
z = w_1 x_1 +  x_2 + w_2
$$

$$
\hat y =  g(z)=\frac{1}{1+exp\left(-z\right)}
$$

$$
C = \frac{1}{n}\sum_{i=1}^{n} \left( \hat y_i - y_i \right)^2
$$

```python
def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))

```

```python
z_array = np.linspace(-10, 10)
g_z_array = sigmoid(z_array)
plt.plot(z_array, g_z_array)
plt.grid(True)
plt.xlabel('$z$')
plt.ylabel("$g(z)$")
plt.show()

```

시그모이드 함수를 사용하는 비용함수

Cost function using the sigmoid function

```python
def cost_function_sigmoid(w:np.ndarray, x_y:np.ndarray) -> float:
    n = len(x_y)
    y_hat = sigmoid(wx(w, x_y))
    y = x_y[:, -1]

    error = y_hat - y
    error_sqr = error ** 2
    result = error_sqr.sum() / n

    return result

```

최적화

Optimize

```python
result = so.fmin(cost_function_sigmoid, x0=nr.rand(2,), args=(data,), full_output=True)
weights, cost_value, n_iter, n_call, warning = result
result

```

```python
plot_two_sets(set_0, set_1)
plot_decision_boundary(data[:, 0].min(), data[:, 0].max(), weights)

plt.legend(loc=0)
plt.show()
plt.close();

```

## 교차 엔트로피 비용함수
Cross entropy cost function

국소 최소점을 피해 전역 최소점을 찾기 위해 사용
To find global and avoid local minimum.

ref : (14:23)
[](https://youtu.be/6vzchGYEJBc)

$$
C = \frac{1}{n}\sum_{i=1}^{n} \left[ -y_i log \left( \hat y_i \right) - \left(1 - y_i \right) log \left( 1 - \hat y_i \right)\right]
$$

```python
def cost_function_cross_entropy(w:np.ndarray, x_y:np.ndarray) -> float:
    n = len(x_y)
    y_hat = sigmoid(wx(w, x_y))
    y = x_y[:, -1]

    cost = -y * np.log2(y_hat) - (1 - y) * np.log2(1 - y_hat)

    return np.mean(cost)

```

최적화

Optimize

```python
result = so.fmin(cost_function_cross_entropy, x0=nr.rand(2,), args=(data,), full_output=True)
weights, cost_value, n_iter, n_call, warning = result
result

```

```python
plot_two_sets(set_0, set_1)
plot_decision_boundary(data[:, 0].min(), data[:, 0].max(), weights)

plt.legend(loc=0)
plt.show()
plt.close();

```

## Final Bell
마지막 종

```python
# stackoverfow.com/a/24634221
import os
os.system("printf '\a'");

```

```python

```


# Jastrow Factor Derivatives

This notebook calculates the expressions for the derivatives of the following Jastrow function:

$$J(\mathbf{X}) = \prod_{i < j} \exp\left[- \frac{1}{2}\left(\frac{\beta}{r}\right)^m  \right]$$

with $N$ particles in $D$ dimensions, $\mathbf{X}\in\mathbb{R}^{N\times D}$ and $r_{ij} = ||\mathbf{X_i} - \mathbf{X_j}||$.

```python
%reload_ext blackcellmagic
from sympy import *
init_printing()
```

```python
N, D, i, j, k, l, d = symbols("N D i j k l d", integer=True)
X = IndexedBase("X", shape=(N, D))

```

## Definition

```python
class r(Function):
    @classmethod
    def eval(cls, i, j):
        if i.is_Number and j.is_Number:
            return sqrt(Sum((X[i, d] - X[j, d]) ** 2, (d, 1, D)))

    def doit(self, **kwargs):
        i, j = self.args
        return sqrt(Sum((X[i, d] - X[j, d]) ** 2, (d, 1, D)))

    def _eval_derivative(self, x):
        return self.doit()._eval_derivative(x)

def J(X):
    beta = symbols(r"\beta", real=True, positive=True)
    n = symbols("n", integer=True, positive=True)
    exponent = Sum(-(beta / r(i, j)) ** n / 2, (i, 1, N - 1), (j, i + 1, N))
    return exp(exponent), (beta, n)

Jastrow, (beta, n) = J(X)
Jastrow

```

```python
log_Jastrow = expand_log(log(Jastrow), force=True)
log_Jastrow

```

## Derivative w.r.t. $\beta$

```python
dlogJ_dBeta = diff(log_Jastrow, beta, 1)
dlogJ_dBeta.simplify()

```

## Derivative w.r.t. $X_{kl}$

```python
dlogJ_dXkl = diff(log_Jastrow.doit(), X[k, l], 1)
dlogJ_dXkl

```

```python
# Manually simplify the above:
dlogJ_dXkl = Sum(
    n / 2
    * (beta / r(i, j)) ** n
    * (KroneckerDelta(k, i) - KroneckerDelta(k, j))
    * (X[i, l] - X[j, l])
    / (r(i, j).doit() ** 2),
    (i, 1, N - 1),
    (j, i + 1, N),
)
dlogJ_dXkl

```

## Second derivative w.r.t. $X_{kl}$

```python
d2J_dXkl2 = dlogJ_dXkl ** 2 + diff(dlogJ_dXkl, X[k, l], 1)
d2J_dXkl2

```

```python
# Manually simplify the above:
d2J_dXkl2 = dlogJ_dXkl ** 2 + Sum(
    n / 2
    * (beta / r(i, j)) ** n
    / (r(i, j) ** 2)
    * (KroneckerDelta(k, i) - KroneckerDelta(k, j)) ** 2
    * (1 - 2 * (X[i, l] - X[j, l]) ** 2 / (r(i, j) ** 2)   ),
    (i, 1, N - 1),
    (j, i + 1, N),
)
d2J_dXkl2

```

```python

```


```
import sympy as sy
from IPython.display import display
sy.init_printing(use_latex='mathjax')
```

```
s, lambda_ = sy.symbols('s lambda', real=True, positive=True)
beta = sy.symbols('beta', real=True)
```

The gamma distribution: $\textrm{Gamma}(s_{ij}|1, \frac{\lambda^2}{2})$

```
gamma = lambda_**2/2 * sy.exp(-lambda_**2 / 2 * s)
display(gamma)
```

$$\frac{\lambda^{2}}{2 e^{\frac{\lambda^{2} s}{2}}}$$

The normal distribution: $\mathcal{N}(0, s_{ij}^2)$

```
normal = 1/s*sy.exp(-beta**2/2/s**2)
display(normal)
```

$$\frac{1}{s} e^{- \frac{\beta^{2}}{2 s^{2}}}$$

```
sy.simplify(normal * gamma)
```

$$\frac{\lambda^{2}}{2 s e^{\frac{\beta^{2}}{2 s^{2}} + \frac{\lambda^{2} s}{2}}}$$

```
sy.integrate(normal * gamma, s)
```

$$\int \frac{\lambda^{2} e^{- \frac{\beta^{2}}{2 s^{2}}}}{2 s e^{\frac{\lambda^{2} s}{2}}}\, ds$$

```
sy.expand_log(2 * sy.log(normal * gamma))
```

$$- \frac{\beta^{2}}{s^{2}} - \lambda^{2} s + 4 \log{\left (\lambda \right )} - 2 \log{\left (s \right )} - 2 \log{\left (2 \right )}$$

Draw from Laplacian prior given in wei_lap.stan (Peltola)

```
%matplotlib inline
import scipy.stats as st
import matplotlib.pyplot as plt
import numpy as np
N = 1000000
r1 = st.norm(0, 1).rvs(N)
r2 = st.gamma(.5, .5).rvs(N)
r = st.expon(1).rvs(N)
```

```
_ = plt.hist(r1)
```

```
_ = plt.hist(r2)
```

```
_ = plt.hist(r)
```

```
_ = plt.hist(r1 * np.sqrt(r/r2), bins=1000)
plt.xlim(-1.5, 1.5)
```

```

```


# [NTDS'18] milestone 3: spectral graph theory
[ntds'18]: https://github.com/mdeff/ntds_2018

[Michaël Defferrard](http://deff.ch), [EPFL LTS2](https://lts2.epfl.ch)

---
## Students

* Team: 6
* Students: Gabor Csordas, Maëlle Le Clainche, Nicolas Fontbonne, Marie Sadler
* Dataset: Flights routes

---
## Rules

* Milestones have to be completed by teams. No collaboration between teams is allowed.
* Textual answers shall be short. Typically one to two sentences.
* Code has to be clean.
* You cannot import any other library than we imported.
* When submitting, the notebook is executed and the results are stored. I.e., if you open the notebook again it should show numerical results and plots. We won't be able to execute your notebooks.
* The notebook is re-executed from a blank state before submission. That is to be sure it is reproducible. You can click "Kernel" then "Restart & Run All" in Jupyter.

---
## Objective

The goal of this milestone is to get familiar with the graph Laplacian and its spectral decomposition.

---
## 0 Load your network

```python
%matplotlib inline
```

*If you get a `No module named 'sklearn'` error when running the below cell, install [scikit-learn](https://scikit-learn.org) with `conda install scikit-learn` (after activating the `ntds_2018` environment).*

```python
import numpy as np
from scipy import sparse
from scipy import linalg
import scipy.sparse.linalg
import matplotlib as mpl
mpl.style.use('seaborn')
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
# for ground truth checking
import pandas as pd
```

*Let's denote your graph as $\mathcal{G} = (\mathcal{V}, \mathcal{E}, A)$, where $\mathcal{V}$ is the set of nodes, $\mathcal{E}$ is the set of edges, $A \in \mathbb{R}^{N \times N}$ is the (weighted) adjacency matrix, and $N = |\mathcal{V}|$ is the number of nodes.*

*Import the adjacency matrix $A$ that you constructed in the first milestone.
(You're allowed to update it between milestones if you want to.)*

```python
adjacency =  np.load("adjacency.npy")
adjacency_unweighted = np.copy(adjacency)
adjacency_unweighted[adjacency_unweighted!=0]=1
degrees = np.sum(adjacency_unweighted, axis = 0)
n_nodes =  adjacency_unweighted.shape[0]
## We are removing those edges where the weight is smaller thane the threshold
threshold = 20
node_map = np.where(degrees >= threshold)[0]
adjacency_th = np.delete(adjacency_unweighted,np.where(degrees < threshold),0)
adjacency_th = np.delete(adjacency_th,np.where(degrees < threshold),1)
degrees_th = np.sum(adjacency_th, axis = 0)
n_nodes_th = adjacency_th.shape[0]

adjacency_csr = sparse.csr_matrix(adjacency_unweighted);
degree_matrix_csc = sparse.diags(degrees,format = "csc")
```

---
## 1 Graph Laplacian

---
### Question 1

*From the (weighted) adjacency matrix $A$, compute both the combinatorial (also called unnormalized) and the normalized graph Laplacian matrices.*

*Note: if your graph is weighted, use the weighted adjacency matrix. If not, use the binary adjacency matrix.*

*For efficient storage and computation, store these sparse matrices in a [compressed sparse row (CSR) format](https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_row_.28CSR.2C_CRS_or_Yale_format.29).*

```python
laplacian_combinatorial_csr =  sparse.csr_matrix(degree_matrix_csc - adjacency_csr);
inv_degree_matrix_csr = sparse.linalg.inv(degree_matrix_csc).tocsr()
sqrt_inv_degree_matrix_csr = sparse.csr_matrix.sqrt(inv_degree_matrix_csr)
laplacian_normalized_csr = sqrt_inv_degree_matrix_csr * laplacian_combinatorial_csr * sqrt_inv_degree_matrix_csr
```

*Use one of them as the graph Laplacian $L$ for the rest of the milestone.
We however encourage you to run the code with both to get a sense of the difference!*

```python
laplacian = laplacian_normalized_csr
```

---
### Question 2

*Compute the eigendecomposition of the Laplacian $L = U^\top \Lambda U$, where the columns $u_k \in \mathbb{R}^N$ of $U = [u_1, \dots, u_N] \in \mathbb{R}^{N \times N}$ are the eigenvectors and the diagonal elements $\lambda_k = \Lambda_{kk}$ are the corresponding eigenvalues.*

*Make sure that the eigenvalues are ordered, i.e., $0 = \lambda_1 \leq \lambda_2 \leq \dots \leq \lambda_N$.*

```python
# calculate eigenvalues and eigenvectors
[eigenvalues, eigenvectors] = sparse.linalg.eigsh(laplacian_normalized_csr, k = n_nodes-1, which = 'LM')

#This function will not return the first eigenvalue 0 because we know that the first eigenvalue is always 0
#So when we now look at eigenvalues we should not forget that there is one more 0.

# sort the resulting values and vectors
sortID = np.argsort(eigenvalues)
eigenvalues = eigenvalues[sortID]
eigenvalues[eigenvalues < 10**(-10)] = 0

eigenvectors = eigenvectors[:,sortID]
eigenvectors[eigenvectors < 10**(-10)] = 0
```

* *Justify your choice of eigensolver.*

**Answer**

The Laplacian is always a symmetric real valued matrix and therefore a Hermitian matrix as well. So we can use the solver for sparse Hermitian matrices.

---
### Question 3

* *We can write $L = S S^\top$. What is the matrix $S$? What does $S^\top x$, with $x \in \mathbb{R}^N$, compute?*

**Answer**

The matrix S is the incidence matrix having as rows the nodes and as columns the edges ($S \in \mathbb{R}^{NxM}$, N the number of nodes and M the number of edges). For an unweighted graph, $S_{i,j}$ = 1, for an edge $e_j$ between two vertexes $v_{i,k}$, -1 for $e_j$ between $v_{k,i}$ and 0 otherwise (the choice of sign is arbitrary, but it has to be once positive and once negative for a given edge). For a weighted graph, the entries are $\sqrt{w_{i,k}}$ and $-\sqrt{w_{i,k}}$, instead of +1 and -1, respectively.

$S^\top$ acts as a graph gradient, and $S^\top x$ for a weighted graph computes:

\begin{equation}
(S^\top x )[j]= \sqrt{w_{i,k}} (x[i] - x[k])
\end{equation}

which corresponds to the derivative of x along edge j

---
### Question 4

* *Show that $\lambda_k = \| S^\top u_k \|_2^2$, where $\| \cdot \|_2^2$ denotes the squared Euclidean norm (a.k.a. squared $L^2$ norm).*

**Answer**

\begin{equation}
\begin{split}
\lambda_k & = u_k^\top L u_k \text{, result of the eigendecomposition of $L$ with $u_k$ being a unit-vector}\\
 & = u_k^\top S S^\top  u_k \text{      , with $L = S S^\top$}\\
 & = (S^\top u_k)^\top S^\top  u_k \text{        , where the order of factors reverses when taking the transpose}\\
 & =\| S^\top u_k \|_2^2 \text{     , with $x^\top x$ being the squared Euclidean norm}\\
\end{split}
\end{equation}

* *What does the quantity $\| S^\top x \|_2^2$ tell us about $x$?*

**Answer**

This quantity tells us how "smooth" x is, i.e. a larger quantity means a higher variation among the x vector components.

---
### Question 5

* *What is the value of $u_0$, both for the combinatorial and normalized Laplacians?*

**Answer**

 * **Combinatorial Laplacian**

$u_0$ corresponds to the eigenvalue 0 and cannot be the vector 0. From the definition of the eigendecomposition we have:

\begin{equation}
L u_0 = \lambda_0 u_0 = 0
\end{equation}

Multiply by $u_0^\top$ gives:
\begin{equation}
u_0^\top L u_0 = u_0^\top \cdot 0 = 0
\end{equation}

Since $u_0^\top L u_0$ is defined as follows:
\begin{equation}
u_0^\top L u_0 = \frac{1}{2} \sum_{\substack{u,v} \in E} w_{u,v}(u_0[u]-u_0[v])^2
\end{equation}

We get:
\begin{equation}
\sum_{\substack{u,v} \in E} w_{u,v}(u_0[u]-u_0[v])^2 = 0
\end{equation}

For this equation to hold, we need to have $u_0[u] = u_0[v]$ for any edge (u,v) $\in E$. In the case where the graph is connected, we get that $u_0[i] = u_0[k]$ for every i,k $\in V$. From this we get that $u_0$ equals:
\begin{align}
    u_0 &= \alpha \begin{bmatrix}
           1 \\
           1 \\
           \vdots \\
           1
         \end{bmatrix}
  \end{align}

with $\alpha \in \mathbb{R}^*$, or to have a unit vector, $\alpha$ needs to equal $\alpha = \frac{1}{\sqrt{N}}$, where $N$ is the number of connected nodes. Thus, the value of $u_0$ is the unit vector $e$.

- **Normalized Laplacian**

Let's call $u_0'$ the eigenvector of the normalized Laplacian $L_{norm}$.

From the theory, we know that $u_0' = D^{\frac{1}{2}} u_0$, and hence $u_0' =  D^{\frac{1}{2}} e$ is the eigenvector of $L_{norm}$ of eigenvalue 0.

---
### Question 6

- *Look at the spectrum of the Laplacian by plotting the eigenvalues.
Comment on what you observe.*

```python
plt.plot(eigenvalues.real, "b." , markersize = 1)
plt.xlabel('Eigenvalue Index')
plt.ylabel('Eigenvalue')
```

**Answer**

As espected the eigenvalues are in the domain $[0,2]$. The first eigenvalues which equal to 0 represent the number of connected components.

It is interesting to observe that a lot of eigenvalues equal 1. In this notebook we will not further investigate into this property, but we will keep it in mind for the final report.

- *How many connected components are there in your graph? Answer using the eigenvalues only.*

```python
# in addition to the 0-multiplicity we add 1 which corresponds to the first eigenvalue 0 which is omitted in the computation
n_conn_comp = eigenvalues[eigenvalues == 0].shape[0] + 1
print("Number of connected components:",n_conn_comp)
```

There are **8** connected components.

- *Is there an upper bound on the eigenvalues, i.e., what is the largest possible eigenvalue? Answer for both the combinatorial and normalized Laplacians.*

**Answer**

For normalized Laplacians the upper bound on the eigenvalues is 2, where equality holds iff the graph is bipartite.

For our graph (normalized graph and the threshold graph), we observed that the upper bound of the eigenvalues of the combinatorial Laplacian was the same than the maximum degree. Defining the upper bound is an active field of research, and diverse theorems of tight bounds can be found in the literature.

---
## 3 Laplacian eigenmaps

*Laplacian eigenmaps* is a method to embed a graph $\mathcal{G}$ in a $d$-dimensional Euclidean space.
That is, it associates a vector $z_i \in \mathbb{R}^d$ to every node $v_i \in \mathcal{V}$.
The graph $\mathcal{G}$ is thus embedded as $Z \in \mathbb{R}^{N \times d}$.

---
### Question 7

* *What do we use Laplacian eigenmaps for? (Or more generally, graph embeddings.)*

**Answer**

We use Laplacian eigenmaps for **dimensionality reduction**. Often, graph data is intrinsically low dimensional, but lies in a very high-dimensional space. Thus, **mapping a network to a vector space and reducing the dimension, while preserving relevant graph properties**, can be useful for faster **computation, machine learning algorithms, statistics, or visualization**.

---
### Question 8

*Embed your graph in $d=2$ dimensions with Laplacian eigenmaps.
Try with and without re-normalizing the eigenvectors by the degrees, then keep the one your prefer.*

*Recompute the eigenvectors you need with a partial eigendecomposition method for sparse matrices.
When $k \ll N$ eigenvectors are needed, partial eigendecompositions are much more efficient than complete eigendecompositions.
A partial eigendecomposition scales as $\Omega(k |\mathcal{E}|$), while a complete eigendecomposition costs $\mathcal{O}(N^3)$ operations.*

```python
#Creating the matrix in the good format
adjacency_lc_csr = sparse.csr_matrix(adjacency_th);
degrees_lc = np.sum(adjacency_th, axis = 0)
degree_matrix_lc_csc = sparse.diags(degrees_lc,format = "csc")
n_nodes_lc =  degrees_lc.shape[0]

#Computation of the laplacian for our graph
laplacian_combinatorial_lc_csr =  sparse.csr_matrix(degree_matrix_lc_csc - adjacency_lc_csr);
inv_degree_matrix_lc_csr = sparse.linalg.inv(degree_matrix_lc_csc).tocsr()
sqrt_inv_degree_matrix_lc_csr = sparse.csr_matrix.sqrt(inv_degree_matrix_lc_csr)
laplacian_normalized_lc_csr = sqrt_inv_degree_matrix_lc_csr * laplacian_combinatorial_lc_csr * sqrt_inv_degree_matrix_lc_csr

```

```python
def graph_embedding(laplacian_normalized_csr, d):
    [eigenvalues, eigenvectors] = sparse.linalg.eigsh(laplacian_normalized_csr, k = d+1, which = 'SM')
    sortID = np.argsort(eigenvalues)
    eigenvalues = eigenvalues[sortID]
    eigenvectors = eigenvectors[:,sortID]
    proj  = eigenvectors[:,1:d+1]
    return proj
```

* *Plot the nodes embedded in 2D. Comment on what you see.*

```python
proj = graph_embedding(laplacian_normalized_lc_csr,  2)
plt.plot(proj[:,0],proj[:,1], '.')
```

**Answer**

With the embedding in two dimensions, airports that are connected by a flight now appear close to each other in the plot. We start to see clusters that naturally emerge form the geometry. Since the clusters represent airports that are well connected to each other, there might be another meaning to it such as financially, economically, and culturally similar countries.

---
### Question 9

What does the embedding $Z \in \mathbb{R}^{N \times d}$ preserve?

**Answer**

The embedding $Z \in \mathbb{R}^{N \times d}$ preserves relevant graphs properties such as the number of data points N, while reducing the dimension d.
The goal of the Laplacian eigenmap algorithm is to preserve local information optimally. All the point node that are close to each other, meaning nodes connected by an edge with a large weight, must remain close to each other in the embedding.

---
## 2 Spectral clustering

*Spectral clustering is a method to partition a graph into distinct clusters.
The method associates a feature vector $z_i \in \mathbb{R}^d$ to every node $v_i \in \mathcal{V}$, then runs [$k$-means](https://en.wikipedia.org/wiki/K-means_clustering) in the embedding space $\mathbb{R}^d$ to assign each node $v_i \in \mathcal{V}$ to a cluster $c_j \in \mathcal{C}$, where $k = |\mathcal{C}|$ is the number of desired clusters.*

For this part, we have chosen to use the unweighted adjacency matrix, because we feel that our graph will probably be clustered according to geographical features. Moreover we kept only the nodes with a degree larger than 20, because we want to see only the airports (=nodes) that are significant (an airport with less than 20 flights is not very significant). By applying this threshold, we also assure that our graph is connected.

```python
#Computation of the eigenvalues and eigenvectors for our graph
[eigenvalues_lc, eigenvectors_lc] = sparse.linalg.eigsh(laplacian_normalized_lc_csr, k = n_nodes_lc-1, which = 'LM')
#This function will not return the first 0 because it is assuming it is always there.
#So when we now look at eigenvalues we had not to forget that there is one more 0.
sortID = np.argsort(eigenvalues_lc)
eigenvalues_lc = eigenvalues_lc[sortID]
eigenvectors_lc = eigenvectors_lc[:,sortID]
```

```python
n_conn_comp = eigenvalues_lc[eigenvalues_lc ==  0].shape[0]+1
eigenvalues_lc[eigenvalues_lc < 10**(-10)] = 0
print("Number of connected components:",n_conn_comp)
```

```python
# Graph Spectrum
plt.plot(eigenvalues_lc, "b." , markersize = 1)
plt.title("Graph Spectrum")
plt.xlabel("Numbers of the eigenvalues")
plt.ylabel("Values of eigenvalues")
```

---
### Question 10

* *Choose $k$ and $d$. How did you get to those numbers?*

**Answer**
- k is chosen according to the graph of eigenvalues : the number of eighenvalues before the "gap" corresponds to k. k is the number of clusters.
- d is the dimension of the embedding space, it has to be chosen in order to preserve local infomation optimally in a certain sense. (after Belkin's book).

```python
#Plot eigenvalues repartitions
x = np.ones_like(range(len(eigenvalues_lc)))
plt.plot(eigenvalues_lc,x,"b.",markersize = 10)
plt.xlim([0,0.5])
plt.title("Repartition of eigenvalues (zoom on the part next to 0)")
plt.xlabel("Value of the eigenvalue")

##count how many values we have until the gap
nb_before_1rst=len(eigenvalues_lc[np.where(eigenvalues_lc< 0.2)])+1 # +1 correspond to the first 0 not in the list of eigenvalues
nb_before_2nd=len(eigenvalues_lc[np.where(eigenvalues_lc< 0.4)])+1  # +1 correspond to the first 0 not in the list of eigenvalues
print("number of eigenvalues before 1st gap :",nb_before_1rst)
print("number of eigenvalues before 2nd gap :",nb_before_2nd)
```

- As we see on the graph, we have two big gaps. The first corresponds to k=3 and the second to k=6. We will test these two possibilities
- We have decided to take d=k, because d is the number of eigenvectors to keep when applying the K-means. If d<k, the number of information may be to little to process a good clustering. However if d is large, the computational cost will be higher. Considering these facts, d=k seems a good compromise.

---
### Question 11

1. *Embed your graph in $\mathbb{R}^d$ as $Z \in \mathbb{R}^{N \times d}$.
   Try with and without re-normalizing the eigenvectors by the degrees, then keep the one your prefer.*
1. *If you want $k=2$ clusters, partition with the Fiedler vector. For $k > 2$ clusters, run $k$-means on $Z$. Don't implement $k$-means, use the `KMeans` class imported from scikit-learn.*

We choose to do the embedding with the eigenvectors obtained from the eigendecomposition of the normalized Laplacian.

```python
# For k=3 and d=3
k = 3; d = 3
H = eigenvectors_lc[:,:d];
clusters3 = KMeans(n_clusters=k, random_state=0).fit_predict(H)

print("----- For k=",k," and d=",d," -----")
print("Number of elements in clusters :")
for i in range(k):
    cnt = 0
    for j in clusters3:
        if j == i:
            cnt +=1
    print("Cluster ",i+1,":",cnt)

#For k=6 and d=6
k = 6; d = 6
H = eigenvectors_lc[:,:d];
clusters6 = KMeans(n_clusters=k, random_state=0).fit_predict(H)
print()
print("----- For k=",k," and d=",d," -----")
print("Number of elements in clusters :")
for i in range(k):
    cnt = 0
    for j in clusters6:
        if j == i:
            cnt +=1
    print("Cluster ",i+1,":",cnt)

```

```python
#For k=2, we have :
fiedler_vect = np.sign(eigenvectors_lc[:,0])
nb_neg=len(fiedler_vect[np.where(fiedler_vect==-1)])
nb_pos=len(fiedler_vect[np.where(fiedler_vect==1)])
print("----- For k=2 (this is just an exemple, it has no real sense for our graph) -----")
print("Number of elements in clusters :")
print("Cluster labeled +1 :",nb_pos)
print("Cluster labeled -1 :",nb_neg)
```

---
### Question 12

- *Use the computed cluster assignment to reorder the adjacency matrix $A$.
What do you expect? What do you observe?*

```python
# For k=3
new_order3 = np.array([],dtype = int)
for i in range(3):
    new_order3 = np.append(new_order3,np.where(clusters3 == i))
plt.spy(adjacency_th[:,new_order3][new_order3], markersize=1)
plt.title("Reordered ajacency matrix for k=3")

#For k=6
plt.figure()
new_order6 = np.array([],dtype = int)
for i in range(6):
    new_order6 = np.append(new_order6,np.where(clusters6 == i))
plt.spy(adjacency_th[:,new_order6][new_order6], markersize=1)
plt.title("Reordered ajacency matrix for k=6")
```

We were expecting a block diagonal matrix or something close to it, because the nodes of one cluster are supposed to be mostly connected with the nodes of this same cluster. Of course the clusters are not fully independent (in this case, our graph would have several distinct components).
- For k=3, we observe 3 blocks in the diagonal. However, those blocks seems to have "internal" blocks : that means that it may exist some "internal" clusters in clusters. This is fully visible in the case k=6.
- For k=6, the diagonal is composed of 6 blocks. Some blocks (like the 3rd - 167 nodes) are much bigger than others (like the 2nd - 11 nodes). The blocks are still connected with the other blocks even if they are mostly link with themselves.

---
### Question 13

*If you have ground truth clusters for your dataset, compare the cluster assignment from spectral clustering to the ground truth.
A simple quantitative measure is to compute the percentage of nodes that have been correctly categorized.
If you don't have a ground truth, qualitatively assess the quality of the clustering.*

*Ground truth clusters are the "real clusters".
For example, the genre of musical tracks in FMA, the category of Wikipedia articles, the spammer status of individuals, etc.
Look for the `labels` in the [dataset descriptions](https://github.com/mdeff/ntds_2018/tree/master/projects/README.md).*

Since there are no labels in our dataset, we have chosen to check our hypothesis of geographical clusters. In order to do that we import back our dataframe of airports and flights.

```python
# import of routes
routes = pd.read_csv('routes.dat', sep=',', encoding='utf-8', engine='python')
routes.columns = ['Airline','Airline ID','Source Airport','Source Airport ID','Destination Airport','Destination Airport ID','Codeshare','Stops','Equipment']
routes = routes.drop(columns=['Source Airport ID','Destination Airport ID'])

# import of source and destination airport
source_airports = routes[['Source Airport']]
source_airports = source_airports.rename(columns={'Source Airport':'Airport'})

dest_airports = routes[['Destination Airport']]
dest_airports = dest_airports.rename(columns={'Destination Airport':'Airport'})

# creation of a dataframe with all airport and airport_idx
# (we use airport_idx insteed of airportID because some airports have no airportID)
airports = pd.concat([source_airports,dest_airports])
airports = airports.drop_duplicates()
airports.reset_index(inplace=True)
airports = airports.drop(columns=['index'])
airports.reset_index(inplace=True)
airports = airports.set_index('Airport')
airports = airports.rename(columns={'index':'airport_idx'})
```

```python
#For clustering with k=3
print("--------------------------------- For k=3 ---------------------------------\n")

for i in range(3):
    print("Cluster",i+1," :\n",airports.index[node_map[np.where(clusters3 == i)]].values)
```

For k=3, we observe that our cluster are relative to the continents.
- **Cluster 1:** European Airports (ex : Geneva, Lyon, Dublin, Rome, Vienne etc) + some Indonesian Airports (Jawa Timur, Jakarta)
- **Cluster 2:** American Continent Airport (ex : Peru, America, Canada etc)
- **Cluster 3:** Asian Continent (ex : China, Japan)

We notice that some airports are badly clustered if we consider geographical clusters : for exemple ITM (Osaka Airport, Japan) is in Cluster 1 ("Europe"). Moreover the "mix" of geographic areas in cluster 1 really shows that some in-clusters exist. Choosing a higher k will probably show a result closer to the geographical "reality".

```python
#For clustering with k=6
print("--------------------------------- For k=6 ---------------------------------\n")

for i in range(6):
    print("Cluster",i+1," :\n",airports.index[node_map[np.where(clusters6 == i)]].values)
```

For k=6, we observe that our clusters are more relative to smaller areas (countries or region of countries).
- **Cluster 1:** North and Central America (Peru, Colombia, and a lot of cities of the USA)
- **Cluster 2:** South Ameria (Bresil, Argentine)
- **Cluster 3:** Europe (France, Switzerland, Italy, UK, etc)
- **Cluster 4:** Asia and Indonesia (Phillipines, Thailande, Chine, Japon, etc)
- **Cluster 5:** Africa (Cote d'Ivoire, Egypt, Arabie Saoudite, Iran, etc)
- **Cluster 6:** Russia (+ some countries linked to Russia like Kazakhstan)

This clustering is really qualitative and shows some meaningful relations between countries. The case of Russia is quite revealing : it is a separate cluster and includes a lot of countries from the former Soviet Union.

---
### Question 14

Plot the cluster assignment (one color per cluster) on the 2D embedding you computed above with Laplacian eigenmaps.

```python
colors = ['Blue', 'Red', 'Green']

for i in range(3):
    cluster = proj[np.where(clusters3 == i)[0], :].real
    plt.plot(cluster[:,0],cluster[:,1], '.', color=colors[i])
```

**Answer**

- **Blue :** Europe + Indonesia (Jawa Timur, Jakarta)
- **Red :** America
- **Green :** Asia

---
### Question 15

Why did we use the eigenvectors of the graph Laplacian as features? Could we use other features for clustering?

**Answer**

The eigenvectors give the embedding of the node in a lower dimensional space keeping similar nodes (in the sense of their egde weigth) close together. Similar nodes are close to each other considering the $L-$distance used for the $k$-mean clustering. Therefore, the eigenvectors are good choice for clustering nodes.

We could have used other features, such as the eigenvectors of the adjacency matrix.


# Exercício 1.0

## Problema

Em uma loja e CD´s existem apenas quatro tipos de preços que estão associados a cores. Assim os CD´s que ficam na loja não são marcados por preços e sim por cores.

Desenvolva uma aplicação que receba como entrada a cor do CD e mostre o preço. A loja está atualmente com a seguinte tabela de preços.

| Cor | Preço|
| :-: | :--: |
| Verde    | R\$ 10,00 |
| Azul     | R\$ 20,00 |
| Amarelo  | R\$ 30,00 |
| Vermelho | R\$ 40,00 |

Caso o usuário digite uma cor não existente, a aplicação deve mostrar a mensagem `"Cor Não Registrada"`

## Análise

Em uma loja e CD´s existem apenas quatro tipos de preços que estão associados a cores.

Desenvolva uma aplicação que receba como entrada a cor do CD e mostre o preço.

| Cor | Preço|
| :-: | :--: |
| Verde    | R\$ 10,00 |
| Azul     | R\$ 20,00 |
| Amarelo  | R\$ 30,00 |
| Vermelho | R\$ 40,00 |

Caso o usuário digite uma cor não existente, a aplicação deve mostrar a mensagem `"Cor Não Registrada"`

## Implicações / Proposições / Afirmações

* A aplicação deve receber do usuário a cor digitada
* A Aplicação deve mostrar o valor do CD com base na cor
* As cores existente são: Verde, Azul, Amarelo e Vermelho.
* Se o usuário digitar um cor inexistente, a aplicação deve mostrar a mensagem `"Cor Não Registrada"`
* Os valores por cor são:
    * Verde -> R\$ 10,00
    * Azul -> R\$ 20,00
    * Amarelo -> R\$ 30,00
    * Vermelho -> R\$ 40,00

## Resolução e Resposta

```python
# Resolução e Resposta Aqui

cor = input("Digite a cor no CD: ")

if cor == "Verde" :
    print('O CD custa R$ 10,00')
elif cor == "Azul" :
    print('O CD custa R$ 20,00')
elif cor == "Amarelo" :
    print('O CD custa R$ 30,00')
elif cor == "Vermelho" :
    print('O CD custa R$ 40,00')
else :
    print('Essa cor não está registrada')
```

    Digite a cor no CD: Preto
    Essa cor não está registrada

# Exercício 2.0

## Problema

Uma empresa decidiu dar uma gratificação de Natal a seus funcionários, baseada no número de horas extras e no número de horas que o funcionário faltou ao trabalho. O valor do prêmio é obtido pela consulta na tabela a seguir, em que:

\begin{equation}\label{eq:}
H = (horas\_extras) - 2/3 * (horas\_falta)
\end{equation}

| H | Gratificação |
| :-: | :--------: |
| Maior que 2400 | R\$ 500,00 |
| 1801 até 2400  | R\$ 400,00 |
| 1201 até 1800  | R\$ 300,00 |
| 600 até 1200   | R\$ 200,00 |
| Menor que 600  | R\$ 100,00 |

Faça uma aplicação que receba o número de horas extras e o número de horas-falta de um funcionário e imprima o valor de sua gratificação.

## Análise

Uma empresa decidiu dar uma gratificação a seus funcionários, baseada no número de horas extras e no número de horas que o funcionário faltou ao trabalho.

`𝐻 = (ℎ𝑜𝑟𝑎𝑠_𝑒𝑥𝑡𝑟𝑎𝑠) − 2/3 ∗ (ℎ𝑜𝑟𝑎𝑠_𝑓𝑎𝑙𝑡𝑎)`

| H | Gratificação |
| :-: | :--------: |
| Maior que 2400 | R\$ 500,00 |
| 1801 até 2400  | R\$ 400,00 |
| 1201 até 1800  | R\$ 300,00 |
| 600 até 1200   | R\$ 200,00 |
| Menor que 600  | R\$ 100,00 |

Faça uma aplicação que receba o número de horas extras e o número de horas-falta de um funcionário e imprima o valor de sua gratificação.

## Implicações / Proposições / Afirmações

* A aplicação deve receber 2 valores: Quantidade de Horas Extras e Quantidade de Horas Falta
* A Aplicação deve utiizar a fórmula `𝐻 = (ℎ𝑜𝑟𝑎𝑠_𝑒𝑥𝑡𝑟𝑎𝑠) − 2/3 ∗ (ℎ𝑜𝑟𝑎𝑠_𝑓𝑎𝑙𝑡𝑎)` para calcular a gratificação
* O Valor Obtido através da fórmula deve ser comparado com a seguinte tabela para verificarmos qual será o valor da gratificação:

| H | Gratificação |
| :-: | :--------: |
| Maior que 2400 | R\$ 500,00 |
| 1801 até 2400  | R\$ 400,00 |
| 1201 até 1800  | R\$ 300,00 |
| 600 até 1200   | R\$ 200,00 |
| Menor que 600  | R\$ 100,00 |

## Resolução e Resposta

```python
# Resolução e Resposta Aqui

h_extras = int(input('Inorme a quantidade de horas extras: '))
h_faltas = int(input('Inorme a quantidade de horas-falta: '))

h = h_extras - (2/3 * h_faltas)

if(h > 2400):
    print(f'O Valor da gratificação é de R$ 500,00')
elif(h > 1800 and h <= 2400):
    print(f'O Valor da gratificação é de R$ 400,00')
elif(h > 1200 and h <= 1800):
    print(f'O Valor da gratificação é de R$ 300,00')
elif(h > 600 and h <= 1200):
    print(f'O Valor da gratificação é de R$ 200,00')
else:
    print(f'O Valor da gratificação é de R$ 100,00')
```

    Inorme a quantidade de horas extras: 2400
    Inorme a quantidade de horas-falta: 0
    O Valor da gratificação é de R$ 400,00


Álgebra Lineal

1  Matrices

1.1  Propiedades de la matriz y algunas matrices elementales

1.2  Exploración de matrices

1.3  Suma y resta

1.4  Ejemplo:

1.4.1  Solución:

1.5  Multiplicación por escalar

1.6  Ejemplo:

1.6.1  Solución:

1.7  Transpuesta

1.8  Multiplicación

1.9  Ejercicio

1.10  Análisis de la multiplicación en las aplicaciones

1.11  Matrices Inversas

2  Resolución de sistemas de ecuaciones lineales

2.1  Método de eliminación de Gauss Jordan

2.1.1  Operaciones elementales de fila

2.1.2  Algoritmo

2.1.3  Ejemplos:

2.2  Ahora con Python

3  Determinantes

3.1  ¿Cómo se calcula?

3.2  ¿Qué pasa si la matriz es de mayor orden?

3.2.1  Algoritmo

3.3  Ejercicios:

3.4  ¿Cómo lo hago en Python?

3.4.1  Ejercicio

3.5  ¿Cómo puedo aplicar los determinantes?

3.5.1  Ejemplo:

3.6  ¿Se puede hallar la solución del sistema utilizando determinantes?

3.6.1  Ejemplo:

3.6.2  Problema

3.6.3  Solución

Álgebra Lineal - Fundamentos

Pensemos en la siguiente situación:

Cada uno de los vendedores entregarán tablas como las siguientes:

    Vendedor 1
    Lunes
    Martes
    Miércoles
    Jueves
    Viernes

     Producto1
    30
    50
    40
    20
    50

     Producto2
    35
    25
    45
    15
    20

     Producto3
    30
    10
    25
    25
    15

    Vendedor 2
    Lunes
    Martes
    Miércoles
    Jueves
    Viernes

     Producto1
    10
    70
    30
    60
    20

     Producto2
    35
    25
    75
    25
    45

     Producto3
    35
    15
    15
    15
    25

 ¿Cómo sintetiza la información entregada por los dos vendedores?

 ¿Obtendría una información similar?

 ¿Cómo expresaría la diferencia de ventas entre vendedores de acuerdo al día de la semana?

 ¿Cómo expresaría las ventas de la distribuidora en esa semana si quiere evaluar el desempeño diario de sus vendedores?

 Esperamos que tratando de responder las anteriores preguntas hayan surgido varias ideas, sin embargo, teniendo en cuenta que hablamos de productos diferentes en días distintos un ejercicio conveniente es presentar una tabla de las mismas dimensiones tanto para presentar el informe totalizado de ventas como para presentar el desempeño de cada vendedor y su diferencia. Es claro que la suma se hace componente a componente y la tabla se estructura como un elemento que se puede "sumar"  y  "restar". Este elemnto se abstrae con el concepto de matriz, una generalización de lo que conocemos como tablas con la posibilidad de dotarlas con operaciones algebraicas.

## Matrices

Una matriz es un arreglo bidimensional, en Python se representa como una lista de listas y es preferible usar *array* del paquete **numpy** para poder hacer todo tipo de operaciones matriciales.

Escribimos primero

*import numpy as np*,

con el fin de llamar todas las funciones de numpy con el prefijo np.*funcion*.

Lista en python:

```python
Lista=
```

```python
import numpy as np
```

Veamos algunos ejemplos, dos matrices 3x3 A y B (observe que cada lista se define con corchetes "[", "]", para nuestro caso, como es una arreglo bidimensional usamos dos niveles de corchetes para lograr la lista de listas):

```python
A = np.array([[1, 3, 2],
              [1, 0, 0],
              [1, 2, 2]])

B = np.array([[1, 0, 5],
              [7, 5, 0],
              [2, 1, 1]])
```

```python
A
```

    array([[1, 3, 2],
           [1, 0, 0],
           [1, 2, 2]])

```python
B
```

    array([[1, 0, 5],
           [7, 5, 0],
           [2, 1, 1]])

```python
## Una matriz 4x5
C=np.array([[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15],[16,17,18,19,20]])
```

```python
C
```

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

```python
C.dtype
```

    dtype('int64')

### Propiedades de la matriz y algunas matrices elementales

La cantidad de elementos y el tamaño son  algunas propiedades de cualquier matriz, en Python contamos con funciones para encontrar esos valores y además tenemos la oportunidad de crear algunas matrices elementales.

> Para las propiedades elementales escribimos el nombre de la matriz y luego la función:
> * **Dimensión:**     [*nombre de la matriz*].*shape*
> * **Cantidad de elementos:**     [*nombre de la matriz*].*size*

> Para las matrices elementales usamos el paqueter **numpy**:
> *  **Matriz de unos de mxn:** *np.ones([m,n])*
> *  **Matriz de ceros de mxn:** *np.zeros([m,n])*
> *  **Matriz idéntidad de nxn:** *np.identity(n)*

```python
C.shape
```

    (4, 5)

```python
A.shape
```

    (3, 3)

```python
## Una matriz de 3x5

V1=np.array([[30,50,40,20,50],[35,25,45,15,20],[30,10,25,25,15]])

```

```python
V1
```

    array([[30, 50, 40, 20, 50],
           [35, 25, 45, 15, 20],
           [30, 10, 25, 25, 15]])

```python
V1.shape
```

    (3, 5)

```python
V1.size
```

    15

```python
B.size
```

    9

```python
Matriz_de_unos=np.ones([4,5])
```

```python
Matriz_de_unos
```

    array([[1., 1., 1., 1., 1.],
           [1., 1., 1., 1., 1.],
           [1., 1., 1., 1., 1.],
           [1., 1., 1., 1., 1.]])

```python
Matriz_de_ceros=np.zeros([3,2])
Matriz_de_ceros
```

    array([[0., 0.],
           [0., 0.],
           [0., 0.]])

```python
np.identity(5)
```

    array([[1., 0., 0., 0., 0.],
           [0., 1., 0., 0., 0.],
           [0., 0., 1., 0., 0.],
           [0., 0., 0., 1., 0.],
           [0., 0., 0., 0., 1.]])

**Algunas definiciones**

*Matriz cuadrada:* Es una matriz con la misma cantidad de filas y columnas.

*Matriz identidad:* Es una matriz cuadrada que en la diagonal (elementos que están ubicados en la misma fila y columna)  tiene unos y fuera de la diagonal tiene ceros.

**Ejemplo**

$$\begin{pmatrix}1&0&0\\ 0&1&0\\ 0&0&1\end{pmatrix}$$

### Exploración de matrices

En general, una matriz de mxn tiene m filas y n columnas, y nos referimos a ella de manera unívoca cuando conocemos todos sus elementos y sus posiciones.

En otras palabras, una matriz es igual a otra si tiene la misma dimensión, los mismo elementos y ellos ocupan los mismos lugares:

$$\begin{pmatrix}1&2&3\\ 0&1&0\\ 0&4&1\end{pmatrix}\neq\begin{pmatrix}1&2&3\\ 0&1&0\\ 0&1&4\end{pmatrix}$$

**Notación de los elementos de una matriz**

Sea $A$ una matriz mxn, el elemento $a_{i j}$ cooresponde al elemento de la fila i-ésima y la columna j-ésima de la matriz. En Python se maneja la misma idea pero contando desde ***cero***.

```python
## Una matriz 3x3

A=np.array([[1,2,3],[0,1,0],[0,4,1]])
B=np.array([[1,2,3],[0,1,0],[0,1,4]])

```

```python
A
```

    array([[1, 2, 3],
           [0, 1, 0],
           [0, 4, 1]])

```python
B
```

    array([[1, 2, 3],
           [0, 1, 0],
           [0, 1, 4]])

```python
A==B
```

    array([[ True,  True,  True],
           [ True,  True,  True],
           [ True, False, False]])

```python
A[3,2] #Genera error porque no cuenta desde cero
```

```python
A[2,1]
```

    4

```python
B[2,1]
```

    1

**Muy importante tener en cuenta que las posiciones en Python están afectadas por iniciar siempre desde cero**

### Suma y resta

La suma/resta de matrices se hace componente a componente, esto nos indica que las matrices deben tener las mimas dimensiones, a esto se le conoce en ser compatibles para la suma/resta. Por ejemplo, si tenemos dos matrices de tamaño $2\times2$

$$A=\begin{bmatrix}a&b\\c&d\end{bmatrix} \\  B=\begin{bmatrix}e&f\\ g&h \end{bmatrix}\\
A\pm B= \begin{bmatrix}a\pm e & b \pm f\\ c \pm g & d\pm h\end{bmatrix}$$
Si $C$ fuera una matriz de un tamaño diferente a $2\times2$ la suma con $A$ o $B$ sería imposible de calcular.

### Ejemplo:
Verifique la suma y la resta de las matrices definidas en la celda de código anterior:

#### Solución:

```python
A
```

    array([[1, 2, 3],
           [0, 1, 0],
           [0, 4, 1]])

```python
B
```

    array([[1, 2, 3],
           [0, 1, 0],
           [0, 1, 4]])

```python
A+B
```

    array([[2, 4, 6],
           [0, 2, 0],
           [0, 5, 5]])

```python
A-B
```

    array([[ 0,  0,  0],
           [ 0,  0,  0],
           [ 0,  3, -3]])

### Multiplicación por escalar
Com osu nombre lo indica se está multiplicando una matriz por un número real cualquiera, el número real $r$ podrá ser positivo negativo o inluso cero, y la matriz $A$ podrá ser de cualquier tamaño, el resultado de $rA$ es simplemente multiplicar cada entrada de $A$ por $r$.

Si $A$ es una matriz de $n$ filas y $m$ columnas

$$A=\left[ \begin{array}{llll}
           a_{11}& a_{12}&\ldots& a_{1m}\\
           a_{21}& a_{22}&\ldots& a_{2m}\\
           \ \ \ \vdots& \ \ \ \vdots& \ \vdots& \ \ \ \vdots\\
           a_{n1}& a_{n2}&\ldots& a_{nm}
             \end{array}
   \right],\\
   rA=\left[ \begin{array}{llll}
           ra_{11}& ra_{12}&\ldots& ra_{1m}\\
           ra_{21}& ra_{22}&\ldots& ra_{2m}\\
           \ \ \ \vdots& \ \ \ \vdots& \ \vdots& \ \ \ \vdots\\
           ra_{n1}& ra_{n2}&\ldots& ra_{nm}
             \end{array}
   \right].$$

### Ejemplo:
Teniendo en cuenta las matrices definidas anteriormente,calcule $3A$, $-2A$ y $\pi B$.
#### Solución:

```python
3*A
```

    array([[ 3,  6,  9],
           [ 0,  3,  0],
           [ 0, 12,  3]])

```python
-2*A
```

    array([[-2, -4, -6],
           [ 0, -2,  0],
           [ 0, -8, -2]])

```python
np.pi*B ## Usamos un valor irracional muy conocido, el número PI
```

    array([[ 3.14159265,  6.28318531,  9.42477796],
           [ 0.        ,  3.14159265,  0.        ],
           [ 0.        ,  3.14159265, 12.56637061]])

### Transpuesta

La transpuesta es la operación sobre una matriz que cambia filas por columnas, para esta sección usaremos unas matrices rectangulares, observe que hacemos el arreglo bidimensional haciendo una lista de listas con la función **np.array**, cada lista interna representa una fila de la matriz:

```python
M=np.array([[1,2,3,4],[-5,-6,-7,-8],[9,10,11,12]])
```

```python
M
```

    array([[ 1,  2,  3,  4],
           [-5, -6, -7, -8],
           [ 9, 10, 11, 12]])

```python
np.transpose(M)
```

    array([[ 1, -5,  9],
           [ 2, -6, 10],
           [ 3, -7, 11],
           [ 4, -8, 12]])

### Multiplicación

A diferencia de las otras operaciones, la multiplicación no se hace componente a componente, se multiplican filas por columnas, y para que sean compatibles las matrices para el producto, la cantidad de columnas de la primera matriz debe coincidir con la cantidad de filas de la segunda, es decir

$$A_{nm}B_{ml}=C_{nl}$$

Debido a su definición tan estricta con el orden, se convierte en una de las primeras operaciones conocidas por nosotros que no es conmutativa.

Para que nos demos a una idea de la operación haremos un ejemplo:

$$A=\begin{bmatrix}1&2\\3&0\\5&-1\end{bmatrix},\ B=\begin{bmatrix}0&3&4&-1\\
1&2&-1&0\end{bmatrix}$$
Como la matriz $A$ es de tamaño $3\times2$ y la matriz B es $2\times4$ el resultado será una matriz $3\times4$
$$A\times B=\begin{bmatrix}1\cdot0+2\cdot1&1\cdot3+2\cdot2& 1\cdot4+2\cdot(-1)&1\cdot(-1)+2\cdot0\\ 3\cdot0+0\cdot1&3\cdot3+0\cdot2& 3\cdot4+0\cdot(-1)&3\cdot(-1)+0\cdot0\\ 5\cdot0+(-1)\cdot1&5\cdot3+(-1)\cdot2& 5\cdot4+(-1)\cdot(-1)&5\cdot(-1)+(-1)\cdot0\end{bmatrix}\\
=\begin{bmatrix} 2&7&2&-1\\0&9&12&-3\\-1&13&21&-5\end{bmatrix}$$

Notemos que el producto de $B$ por $A$ no se puede realizar ya que $4\neq3.$

En Python usamos el comando @ para hacer esta multiplicación.

```python
print(A) ## El comando print() permite visualizar un poco mejor la matriz.
print(M)

print("\n El resultado es:")
print(A@M)
```

    [[1 2 3]
     [0 1 0]
     [0 4 1]]
    [[ 1  2  3  4]
     [-5 -6 -7 -8]
     [ 9 10 11 12]]

     El resultado es:
    [[ 18  20  22  24]
     [ -5  -6  -7  -8]
     [-11 -14 -17 -20]]

```python
print(M.shape)
print(A.shape)
M@A
```

Observe lo que ocurre cuando multiplicamos matrices con dimensiones no apropiadas
### Ejercicio
Verifique las operaciones realizadas en la explicación  con código de Python.

### Análisis de la multiplicación en las aplicaciones

El vendedor 1 del ejemplo anterior tenía estas ventas semanales:

    Vendedor 1
    Lunes
    Martes
    Miércoles
    Jueves
    Viernes

     Producto1
    30
    50
    40
    20
    50

     Producto2
    35
    25
    45
    15
    20

     Producto3
    30
    10
    25
    25
    15

Suponga que vamos a darle una comisión según el producto a cada integrante del equipo de ventas, según la siguiente tabla:

||Producto 1|Producto 2|Producto 3|
|:---:|:---:|:---:|:---:|
|Comisión|\$20.000 |\$30.000|\$50.000|

```python
Comision=np.array([[20000,30000,50000]])
```

```python
V1
```

    array([[30, 50, 40, 20, 50],
           [35, 25, 45, 15, 20],
           [30, 10, 25, 25, 15]])

```python
Comision@V1
```

    array([[3150000, 2250000, 3400000, 2100000, 2350000]])

### Matrices Inversas
Si una matriz cuadrada $A$ de $n\times n$ se multiplica por derecha e izquierda por una matriz $B$ y se obtiene la identidad en ambos casos, es decir
$$AB=BA=I_n$$
La matriz $B$ se conoce como el inverso multiplicativo de la matriz $A$ o más comunmente como la matriz inversa de $A$.

No todas las matrices cuadradas tienen inversa, la forma de determinarla empleando Python es meduante la función  **np.linalg.inv**

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

```python
Ninv=np.linalg.inv(N)
Ninv
```

Comprobamos este resultado:

```python
Ninv @ N
```

Observe que la anterior es la matriz idéntica.

## Resolución de sistemas de ecuaciones lineales

El método más común para solucionar sistemas de ecuaciones es el conocido como Método de eliminación de Gauss-Jordan, el cual consiste básicamente en aplicar operaciones elementales a la matriz de coeficientes para hallar la solución del sistema, también se emplea para hallar inversas de martices.

### Método de eliminación de Gauss Jordan

Si se tiene el sistema de ecuaciones

$$\begin{cases}2x-4y=6\\
-4x+6y=-14
\end{cases}$$

Se puede verificar de manera sencilla que su solución es $x=5$ y $y=1$.

Si se remplaza la segunda ecuación por la suma de las dos veces la primera  más la segunda, obtendremos un nuevo sistema, el cual tiene como solución la misma de antes

$$\begin{cases}2x-4y=6\\
-y=-1
\end{cases}$$

Si se divide la segunda ecuación del primer sistema entre dos, obtenemos
$$\begin{cases}2x-4y=6\\
-2x+3y=-7
\end{cases}$$
sistema el cual también posee la misma solución.

Y si cambiamos el orden en las ecuaciones
$$\begin{cases}-4x+6y=-14\\2x-4y=6
\end{cases}$$
el sistema evidentemente tendrá las mismas soluciones.

Estas operaciones que acabamos de realizar sobre el sistema de ecuaciones son las conocidas como operaciones elementales, y éstas nunca van a cambiar las soluciones del sistema de ecuaciones. Éstas también pueden hacerse en columnas pero no es tan utilizado.

__________________
#### Operaciones elementales de fila
1. Cambio de filas.
2. Multiplicar una fila por una constante.
3. Sumar a una fila un múltiplo de ota.
_________________
Con esto en mente vale la pena trabajar con ellas para hallar la solución de un sistema de ecuaciones.

Como vimos anteriormente un sistema de ecuaciones se puede representar de manera matricial, por ejemplo

$$\begin{cases}2x-4y=6\\
-4x+6y=-14
\end{cases}$$
se puede ver como
$$\begin{bmatrix} 2&-4\\-4&6\end{bmatrix}\begin{bmatrix} x\\y\end{bmatrix}=\begin{bmatrix} 6\\-14\end{bmatrix}$$
La idea fundamental de escribir el sistema de esta manera es aplicar las operaciones elementales, para obtener lo siguiente
$$\begin{bmatrix} 1&0\\0&1\end{bmatrix}\begin{bmatrix} x\\y\end{bmatrix}=\begin{bmatrix} 5\\1\end{bmatrix}$$
ya que al realizar las operaciones de matrices obtendremos que $x=5$ y que $y$=1.

Realizaremos el procedimiento para la matriz que se tiene, con el fin de verificar la respuesta
$$
\left[
\begin{array}{cc|c}
2&-4&6\\
-4&6&-14
\end{array}
\right]
$$
El primer paso será convertir el 2 que está en la diagonal principal en un 1, esto lo podemos hacer dividiendo toda la fila entre 2,

$$
\left[
\begin{array}{cc|c}
2&-4&6\\
-4&6&-14
\end{array}
\right]\xrightarrow{f_1=\frac{1}{2}f_1}\left[
\begin{array}{cc|c}
1&-2&3\\
-4&6&-14
\end{array}
\right]
$$
Ya que tenemos el 1 en la diagonal principal debemos transformar en cero las demás entradas de la columna, en este caso al -4, esto se logra fácilmente sumando 4 veces la fila 1 a la fila 2
$$\left[
\begin{array}{cc|c}
1&-2&3\\
-4&6&-14
\end{array}
\right]\xrightarrow{f_2=f_2+4f_1}\left[
\begin{array}{cc|c}
1&-2&3\\
0&-2&-2
\end{array}
\right]
$$
Como ya se han anulado todos los elementos que no están en la diagonal principal, procedemos a volver 1 el siguiente elemento de la diagonal principal, en este caso al -2 que está en la segunda fila segunda columna.

$$\left[
\begin{array}{cc|c}
1&-2&3\\
0&-2&-2
\end{array}
\right]\xrightarrow{f_2=\frac{-1}{2}}\left[
\begin{array}{cc|c}
1&-2&3\\
0&1&1
\end{array}
\right]
$$
Y finalmente anulamos todos los elementos que no estén sobre la diagonal principal
$$\left[
\begin{array}{cc|c}
1&-2&3\\
0&1&1
\end{array}
\right]\xrightarrow{f_1=f_1+2f_2}\left[
\begin{array}{cc|c}
1&0&5\\
0&1&1
\end{array}
\right]
$$
De modo que $x=5$ mientras que $y=1$.

_____
#### Algoritmo
1. Si la entrada $a_{11}$ de la matriz no es un 1, debe transformase utilizando lasoperaciones elementales.
2. Volver cero todas las entradas de esa columnas que no estén sobre la diagonal principal.
3. Si la entrada $a_{22}$ de la matriz no es un 1, debe transformase utilizando lasoperaciones elementales.
4. Volver cero todas las entradas de esa columnas que no estén sobre la diagonal principal.
5. Repetir el proceso hasta agotar todas las columnas de la matriz.
_____

####  Ejemplos:
1. Solucione el siguiente sistema de ecuaciones utilizando Gauss-Jordan
$$\begin{cases}3x-15y=9\\
-6x+30y=0
\end{cases}$$

$$\left[
\begin{array}{cc|c}
3&-15&9\\
-6&30&0
\end{array}
\right]\xrightarrow{f_1=\frac{1}{3}f_1}\left[
\begin{array}{cc|c}
1&-5&3\\
-6&30&0
\end{array}
\right] \xrightarrow{f_2=f_2+6f_1}\left[
\begin{array}{cc|c}
1&-5&3\\
0&0&18
\end{array}
\right]
$$
Como es imposible transformar la entrada de la diagonal principal en uno, el proceso termina con un nuevo sistema de ecuaciones
$$\begin{cases}x-5y=3\\
0=18\end{cases}$$

como la segunda ecuación representa algo falso, lo que se puede decir del sistema es que no tiene soluciones.

2. Emplee Gauss-Jordan para solucionar el sistema de ecuaciones
$$\begin{cases} 3x+6y-9z=18\\
-6x+8y-10z=4\\
-2x+6y-8z=8\end{cases}$$

$$\left[
\begin{array}{ccc|c}
3&6&-9&18\\
-6&8&-10&4\\
-2&6&-8&8
\end{array}
\right] \xrightarrow{f_1=\frac{1}{3}f_1}\left[
\begin{array}{ccc|c}
1&2&-3&6\\
-6&8&-10&4\\
-2&6&-8&8
\end{array}
\right] \xrightarrow{f_2=f_2+6f_1}\left[
\begin{array}{ccc|c}
1&2&-3&6\\
0&20&-28&40\\
-2&6&-8&8
\end{array}
\right]\xrightarrow{f_3=f_3+2f_1}\left[
\begin{array}{ccc|c}
1&2&-3&6\\
0&20&-28&40\\
0&10&-14&20
\end{array}
\right] \xrightarrow{f_2=\frac{1}{20}f_2}\left[
\begin{array}{ccc|c}
1&2&-3&6\\
0&1&-\frac{7}{5}&2\\
0&10&-14&20
\end{array}
\right] \xrightarrow{f_3=f_3-10f_2}\left[
\begin{array}{ccc|c}
1&2&-3&6\\
0&1&-\frac{7}{5}&2\\
0&0&0&0
\end{array}
\right]
$$
Como los elementos de la última fila son todos nulos, podemos afirmar que el sistema tiene infinitas soluciones.

```python
135/5
```

    27.0

### Ahora con Python

El método que utilizamos para encontrar la solución de un sistema de ecuaciones lineales fue el método de eliminación de Gauss Jordan, si bien con la tabla definida en clase es muy sencillo definir un algoritmo para dicho proceso, es útil conocer funciones que nos permitan hacer ese cálculo.

Para esta sección usaremos otro paquete bien conocido en Python, el paquete **sympy**, iniciamos importandolo:

```python
!pip install sympy
```

    Requirement already satisfied: sympy in /home/isaac/anaconda3/lib/python3.7/site-packages (1.5.1)
    Requirement already satisfied: mpmath>=0.19 in /home/isaac/anaconda3/lib/python3.7/site-packages (from sympy) (1.1.0)

```python
from sympy import *
init_printing()
```

La forma de importar este paquete nos permite usar todas las funciones de **sympy** sin necesidad del prefijo. *init_printing()* es una función que nos facilitara la lectura de las matrices que usamos.

el paquete sympy es un paquete de matemática simbólica, es decir, buscará ser lo más fiel al lenguaje usado en clase.

Iniciamos definiendo una matriz con la función *Matrix*.

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

Con este paque se pueden redefinir las operaciones descritas en la sección anterior con numpy, tenemos por ejemplo:

```python
A=Matrix([[1,2,3],[0,1,2],[-1,1,3]])
B=Matrix([[1,-1,1],[0,1,1],[-1,10,31]])
A , B
```

Suma

```python
A+B
```

Resta

```python
A-B
```

Sumando como en el tablero, usando simbolos matriciales:

```python
3*A
```

```python
s=MatrixSymbol("+",3,3)
e=MatrixSymbol("=",3,3)
(A*s*B)*e*(A+B)
```

Producto

```python
A*B #En sympy ESTE SÍ es el producto de matrices
```

Transpuesta

```python
Transpose(A)##Escrita
```

```python
A.T ##Calculada
```

Inversa

```python
B**(-1)
```

No obstante, lo que queremos es hacer la reducción de Gauss Jordan para las matrices que pueden asociarse o bien a un problema de sistemas o a un problema de inversas. Para esto usamos la función *rref*:

Resolveremos el siguiente sistema:

$$ \begin{cases}x+y+z=1\\ 2x+2y+4z=0 \\ x+z=0 \end{cases}$$

Iniciamos definiendo la matriz aumentada del sistema:

```python
S=Matrix([[1,1,1,1],[2,2,4,0],[1,0,1,0]])
S
```

Ahora lo reducimos:

```python
S.rref()
```

La primera parte de la respuesta nos entrega la forma escalonada reducida de la matriz y la segunda indica el indice de los pivotes que se encontraron en dicha forma. De aquí es fácil notar que la solución del sistema es:

$$ \begin{cases} x=1\\ y=1 \\z=-1 \end{cases} $$

Otro ejemplo, para el sistema:

$$\begin{cases}x+z=3\\ 2x+3y+4z=7\\ -x-3y-3z=-4 \end{cases}$$

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

```python
S2.rref()
```

La solución aquí es infinita, tenemos:

$$\begin{cases} x=3-t\\ y=\frac{1}{3}-\frac{2}{3}t \\ z=t \end{cases} $$

para todo $ t\in \mathbb{R}$.

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

```python
S3.rref()
```

Si se convierte en ecuaciones al final tendré:

$0x+0y+0z=1$

Lo cual es absurdo, luego ese sistema no tiene solución.

**Otra forma de ver un sistema**

Sea el sistema:
$$\begin{cases}x+y-z=1\\x+2y-2z=1\\x+5y-z=5\end{cases}$$

La matriz aumentada es:

$\begin{pmatrix}1&1&-1&1\\1&2&-2&1\\1&5&-1&5\end{pmatrix}$

Expresión usando operaciones matriciales para el sistema:

$\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}\cdot \begin{pmatrix}x\\y\\z\end{pmatrix}=\begin{pmatrix} x+y-z\\x+2y-2z \\x+5y-z \end{pmatrix}=\begin{pmatrix}1\\1\\5\end{pmatrix}$

$\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}^{-1}\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}\cdot \begin{pmatrix}x\\y\\z\end{pmatrix}=\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}^{-1}\begin{pmatrix}1\\1\\5\end{pmatrix}$

$\begin{pmatrix}1&0&0\\0&1&0\\0&0&1\end{pmatrix}\cdot \begin{pmatrix}x\\y\\z\end{pmatrix}=\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}^{-1}\begin{pmatrix}1\\1\\5\end{pmatrix}$

$\begin{pmatrix}x\\y\\z\end{pmatrix}=\begin{pmatrix}1&1&-1\\1&2&-2\\1&5&-1\end{pmatrix}^{-1}\begin{pmatrix}1\\1\\5\end{pmatrix}$

```python
from sympy import *
A=Matrix([[1,1,-1],[1,2,-2],[1,5,-1]])
B=Matrix([[1],[1],[5]])
B
```

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

```python
Sol=A.inv()*B
Sol
```

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

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

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

```python
A1.inv()
```

```python
A.det()
```

$\displaystyle 4$

```python
A1.det()
```

$\displaystyle 0$

## Determinantes

Muchas veces necesitamos conocer de antemano si el problema al que nos enfrentamos tiene una solución única, para ello basta realizar un cálculo sencillo sobre el cual hablaremos enseguida.

**¿Qué es un determinante?**

El determinante es un número real asociado a cada matriz cuadrada, el cual nos permitirá, primero que nada, saber la cantidad de soluciones que tiene un sistema y en segundo lugar el valor de la solución cuando ésta sea única. El determinante de una matriz puede ser cualquier número real.

Es conveniente entender primero como calcular determinantes de matrices $2\times 2$ para luego calcular los deteminantes de mayor orden.

**Notación:**
Para indicar que se va a calcular el determinante de la matriz $A$ es común encontrar las notaciones $det(A)$ o $|A|$.

**Ejemplo:**
Halle el determinante de la matriz $\begin{pmatrix}1&3\\ \:4&2\end{pmatrix}$.

Podemos escribir $det\begin{pmatrix}1&3\\ \:4&2\end{pmatrix}=\begin{vmatrix}1&3\\ \:4&2\end{vmatrix}$

### ¿Cómo se calcula?
En realidad es una cuenta bastante simple, es el producto de la diagonal menos el producto de la antidiagonal, es decir
$$\begin{vmatrix}a_{11}&a_{12}\\ \:a_{21}&a_{22}\end{vmatrix}=a_{11}a_{22}-a_{12}a_{21}$$

**Ejemplos**
1. $$\begin{vmatrix}1&3\\ \:4&2\end{vmatrix}=(1)(2)-(3)(4)=-10$$
2. $$\begin{vmatrix}-4&3\\ \:-1&-6\end{vmatrix}=(-4)(-6)-(-1)(3)=27$$
3. $$\begin{vmatrix}2&4\\ \:-4&-8\end{vmatrix}=(2)(-8)-(4)(-4)=0$$

```python
M1=Matrix([[1,3],[4,2]])
M1.det()
```

$\displaystyle -10$

**Para una matriz de 3x3 el determinante se calcula usando Sarrus**

Sea $A=\begin{pmatrix}a_{11}&a_{12}&a_{13}\\
a_{21}&a_{22}&a_{23}\\
a_{31}&a_{32}&a_{33}\\
\end{pmatrix}$
Utiliza el siguiente arreglo auxiliar:

$\begin{bmatrix}{\color{green} a_{11}}&a_{12}&a_{13}&a_{11}&a_{12}\\
a_{21}&\color{green}  a_{22}&a_{23}&a_{21}&a_{22}\\
a_{31}&a_{32}& \color{green} a_{33}&a_{31}&a_{32}\\
\end{bmatrix} = \tiny  a_{11}a_{22}a_{33}+a_{12}a_{23}a_{31}+a_{13}a_{21}a_{32}-a_{13}a_{22}a_{31}-a_{11}a_{23}a_{32}-a_{12}a_{21}a_{33}$

### ¿Qué pasa si la matriz es de mayor orden?
Primero que nada se deben definir los **cofactores:** el cofactor asociado al elemento $a_{ij}$ de una matriz de tamaño $n\times n$ se define como
$$C_{ij}=(-1)^{i+j}M_{ij}$$
Siendo $M_{ij}$ la matriz obtenida al eliminar la fila $i$ y la columna $j$ de la matriz original.

**Ejemplo:**
Hallar el cofactor $C_{23}$ correspondiente a la matriz  $A=\begin{pmatrix}1&2&0\\ \:-1&1&1\\ \:2&-1&0\end{pmatrix}$.

Al eliminar la segunda fila y la tercera columna se obtiene la matriz

por lo cual
$$C_{23}=(-1)^{2+3}\begin{vmatrix}1&2\\ \:2&-1\end{vmatrix}=(-1)(-5)=5$$

#### Algoritmo
La idea será la siguiente:
1. Elegir una fila (o una columna) .
2. Hallar los cofactores correspondientes a cada uno de los elementos de la fila (o columna).
3. Multiplicar los valores de las entradas por sus respectivos cofactores .
4. Realizar la suma de los resultados obtenidos en el paso 3.

Vamos a hallar el determinante de la matriz anterior utilizando la primera fila, como también la tercera columna, esto con el fin de mostrar que no importa cual se elija, el resultado será el mismo y también para que siempre se elija la fila o columna con mayor cantidad de entradas nulas.

**Determinante seleccionando la primera fila:**

Notemos que como la entrada $A_{23}$ es cero, el sumando asociado a ésta no aporta nada para calcular el determinante.

**Determinante seleccionando la tercera columna:**

Siguiendo la observación anterior en la tercera columna se tienen 2 entradas cuyo valor es cero, por lo que su aporte al determinante es nulo, basta calcular únicamente el cofactor $C_{23}$ multiplicarlo por su respectiva entrada y obtendríamos el valor del determinante así

### Ejercicios:
1. Calcule:
    1. $\begin{vmatrix}0&1\\ \:5&2\end{vmatrix}$
    2. $\begin{vmatrix}4&4\\ \:-2&-2\end{vmatrix}$
    3. $\begin{vmatrix}-4&3\\ \:0&1\end{vmatrix}$
    4. $\begin{vmatrix}0&0&0\\ \:4&2&3\\ \:-1&3&9\end{vmatrix}$
    5. $\begin{vmatrix}0&0&0\\ \:4&2&3\\ \:-1&3&9\end{vmatrix}$
    6. Haga una función en Python que le permita calcular el cofactor indicado por el usuario en una matriz arbitraria.

### ¿Cómo lo hago en Python?
En realidad es bastante simple, ya que la librería *Sympy* tiene una función que premite hallar el determinante directamente.

`matriz.det()`

```python
from sympy import *
```

```python
init_printing(use_unicode=True)
```

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

```python
M.col(2)
```

```python
M.row(1)
```

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

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

#### Ejercicio
1. Utilice la función `.det()` para corroborar los resultados obtenidos anteriormente.

### ¿Cómo puedo aplicar los determinantes?

Como vimos anteriormente los sistemas de ecuaciones se pueden representar de manera matricial de la siguiente manera:
$$Ax=b$$
Si el determinante de $A$ fuese nulo, el sistema puede tener infinitas o ninguna solución, mientras que si es diferente de cero tendrá una única solución.

#### Ejemplo:
¿El siguiente sistema tiene una única solución?
$$\begin{cases} 3x+y=0\\ 2x-6y=5\end{cases}$$

Para responder a la pregunta formulada basta hallar el determinante de la matriz de coeficientes del sistema
$$\begin{vmatrix}3&1\\ \:2&-6\end{vmatrix}=-20\neq0$$
por lo tanto el sistema posee una única solución.

### ¿Se puede hallar la solución del sistema utilizando determinantes?
A éste proceso se le conoce como la Regla de Cramer y consiste en lo siguiente:
1. Se debe calcular el determinante de la matriz de coeficientes.
2. Se remplaza la columna correspondiente a la primera variable por los elementos que conforman al vector $b$, obteniendo una nueva matriz.
3. El proceso anterior se repite con todas las variables que tenga el sistema, es decir, se obtendrán tantas matrices como variables tiene el sistema.
4. Se calculan los determinantes de las matrices anteriores.
5. El valor de cada variable será el obtenido al dividir el determinante de la matriz en la cual se remplazó en vector $b$ por tal variable, entre el determinante de la matriz $A$.

#### Ejemplo:
Para tener una guía de lo que se está realizando es sano escribir como sub-índice la variable que se está remplazando por el vector $b$, por ejemplo
$$|A_x|=\begin{vmatrix}0&1\\ \:5&-6\end{vmatrix}=-5.$$
$$|A_y|=\begin{vmatrix}3&0\\ \:2&5\end{vmatrix}=15.$$
Por lo tanto
$$x=\frac{|A_x|}{|A|}=\frac{-5}{-20}=\frac{1}{4}.$$
$$y=\frac{|A_y|}{|A|}=\frac{15}{-20}=-\frac{3}{4}.$$

#### Problema
Un carpintero fabrica sillas, mesas y repisas.Se necesitan 15 minutos para lijar una silla, 5 para pintarla y 10 para barnizarla. Se necesitan 30 minutos para lijar una mesa, 12 para pintarla y 20 para barnizarla. Se necesitan 20 minutos para lijar una repisa, 10 para pintarla y 15 para barnizarla. La mesa de lijado está disponible 235  horas al mes, la mesa de pintura 95 horas al mes y la mesa de barnizado 160 horas. ¿Cuántas unidades de cada mueble deben fabricarse por mes de modo que las mesas de trabajo se ocupen todo el tiempo disponible?

#### Solución
Dado que las unidades de fabricación vienen dada en minutos y la catidad de tiempo total viene dada en horas, procederemos a convertir las horas en minutos, así

$$\text{Conversión de horas a minutos, debemos multiplicar por 60 }\ \ \begin{cases} 235h\rightarrow14100m\\95h\rightarrow5700m\\160h\rightarrow9600m\end{cases}$$

Por lo tanto el sistema se puede expresar como
$$\begin{cases}
15s+30m+20r=14100\\
5s+12m+10r=5700\\
10s+20m+15r=9600
\end{cases}$$

```python
A=Matrix([[15,30,20],[5,12,10],[10,20,15]])
A
```

```python
A.det()
```

En vista que el determinante de la matriz de coeficientes es diferente de cero, sabemos que el sistema tendrá una única solución, aplicando la regla de Cramer tenemos lo siguiente

```python
As=Matrix([[14100,30,20],[5700,12,10],[9600,20,15]])
As
```

```python
Am=Matrix([[15,14100,20],[5,5700,10],[10,9600,15]])
Am
```

```python
Ar=Matrix([[15,30,14100],[5,12,5700],[10,20,9600]])
Ar
```

Por lo tanto la solución del sistema de ecuaciones viene dada por:

```python
s=As.det()/A.det()
s
```

```python
m=Am.det()/A.det()
m
```

```python
r=Ar.det()/A.det()
r
```

Se deben fabricar mensualmete 180 sillas, 300 mesas y 120 repisas.

```python
A=Matrix([[3,1,0],[2,-6,5]])
```

```python
A
```

```python
A_p=Matrix([[3,1],[2,-6]])
A_p
```

```python
A_x=Matrix([[0,1],[5,-6]])
A_x
```

```python
A_y=Matrix([[3,0],[2,5]])
A_y
```

```python
x=A_x.det()/A_p.det()
x
```

```python
y=A_y.det()/A_p.det()
y
```

$$\begin{cases} 3x+y=0\\ 2x-6y=5\end{cases}$$

```python
3*(1/4)+(-3/4)
```

```python
2*(1/4)-6*(-3/4)
```

```python

```