Table of Contents

Partie 1: Régularisation de Tikhonov

Partie 2: Régularisation pour la parcimoine

Partie 3: Comparaison

# SD211 TP2: Régression logistique
*
Author: Pengfei Mi
*
*
Date: 12/05/2017
*

```python
import numpy as np
import matplotlib.pyplot as plt
from cervicalcancerutils import load_cervical_cancer
from scipy.optimize import check_grad
from time import time
from sklearn.metrics import classification_report
```

## Partie 1: Régularisation de Tikhonov

$\textbf{Question 1.1}\quad\text{Calculer le gradient et la matrice hessienne.}$

        Notons $\tilde{X} = (\tilde{\mathbf{x}}_1,...,\tilde{\mathbf{ x}}_n)^T$, où $\tilde{\mathbf{x}}_i = \begin{pmatrix}1\\
        \mathbf{x}_i\end{pmatrix}\in \mathbb{R}^{p+1}$, $\tilde{\mathbf{\omega}} = \begin{pmatrix}
        \omega_0\\\mathbf{\omega}\end{pmatrix}\in \mathbb{R}^{p+1}$, et la matrice
        $$A = diag(0,1,...,1) =
        \begin{pmatrix}
        0&0&\cdots&0\\
        0&1&&0\\
        \vdots&&\ddots&\vdots\\
        0&0&\cdots&1
        \end{pmatrix}
        $$

        On a:
        $$
        \begin{aligned}
        f_1(\omega_0, \omega) &= \frac{1}{n}\sum_{i=1}^{n}\text{log}\big(1+e^{-y_i(x_i^T\omega+\omega_0)}\big)+\frac{\rho}{2}\|\omega\|_2^2 \\
        & = \frac{1}{n}\sum_{i=1}^{n}\text{log}\big(1+e^{-y_i\tilde x_i^T \tilde \omega}\big)+\frac{\rho}{2}\tilde{\omega}^TA\tilde{\omega}
        \end{aligned}
        $$

        Ainsi on obtient le gradient:
        $$
        \begin{aligned}
        \nabla{f_1}(\omega_0, \omega) &= \frac{1}{n}\sum_{i=1}^{n}\frac{-e^{-y_i\tilde x_i^T \tilde \omega}y_i\tilde{\mathbf{x}}_i}{1+e^{-y_i\tilde x_i^T \tilde \omega}} + \rho A\tilde{\mathbf{\omega}} \\
        &= \frac{1}{n}\sum_{i=1}^{n}\frac{-y_i\tilde{\mathbf{x}}_i}{1+e^{y_i\tilde x_i^T \tilde \omega}} +
        \rho A\tilde{\mathbf{\omega}}
        \end{aligned}
        $$

        et la matrice hessienne:
        $$
        \begin{aligned}
        \mathbf{H} = \nabla^2f_1(\omega_0, \omega) &= \frac{1}{n}\sum_{i=1}^{n}\frac{e^{y_i\tilde x_i^T \tilde \omega}(y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})^2} + \rho A \\
        & = \frac{1}{n}\sum_{i=1}^{n}\frac{(y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} + \rho A
        \end{aligned}
        $$

        Soient $\omega \in \mathbb{R}^{p+1}$, on a:
        $$
        \begin{aligned}
        \omega^TH\omega &= \frac{1}{n}\sum_{i=1}^{n}\frac{\omega^T (y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T \omega}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} + \rho \omega^T A \omega \\
        &= \frac{1}{n}\sum_{i=1}^{n}\frac{(\omega^T y_i\tilde{\mathbf{x}}_i)(\omega^T y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} + \rho \omega^T A^2 \omega \\
        &= \frac{1}{n}\sum_{i=1}^{n}\frac{\|\omega^T y_i\tilde{\mathbf{x}}_i\|_2^2}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} + \rho \|A\omega\|_2^2 \geq 0
        \end{aligned}
        $$

Donc, la matrice hessienne est semi-définie positive, la fonction $f_1$ est convexe.

$\textbf{Question 1.2}\quad\text{Coder une fonction qui retourne la valeur de la fonction, son gradient et sa hessienne.}$

On insère une colonne de $1$ à gauche de $X$ pour simplifier le calcul.

```python
X, y = load_cervical_cancer("riskfactorscervicalcancer.csv")
print "Before the insertion:"
print X.shape, y.shape
n, p = X.shape
X = np.c_[np.ones(n), X]
print "After the insertion:"
print X.shape, y.shape
```

    Before the insertion:
    (210, 26) (210,)
    After the insertion:
    (210, 27) (210,)

```python
def objective(w_, X, y, rho, return_grad=True, return_H=True):
    """
    X: matrix of size n*(p+1)
    y: vector of size n
    w0: real number
    w: vector of size p
    """
    # Initialize elementary intermediate variables;
    n, p = X.shape
    w = w_[1:]
    y_x = np.array([y[i] * X[i, :] for i in range(n)])
    yx_w = np.array([np.sum(y_x[i, :]*w_) for i in range(n)])
    exp_yxw_1 = np.array([np.exp(yx_w[i]) for i in range(n)]) + 1
    exp_neg_yxw_1 = np.array([np.exp(-yx_w[i]) for i in range(n)]) + 1

    # Compute function value
    val = np.mean(np.log(exp_neg_yxw_1)) + np.sum(w**2)*rho/2.

    if return_grad == False:
        return val
    else:
        # Compute gradient
        grad = np.mean(-np.array([y_x[i]/exp_yxw_1[i] for i in range(n)]), axis=0) + rho*np.r_[0, w]

        if return_H == False:
            return val, grad
        else:
            # Compute the Hessian matrix
            H = np.mean(np.array([y_x[i].reshape(-1, 1).dot(y_x[i].reshape(1, -1) / (exp_yxw_1[i]*exp_neg_yxw_1[i])) for i in range(n)]), axis=0) + rho*np.diag(np.r_[0, np.ones(p-1)])
            return val, grad, H
```

```python
def funcMask(w_, X, y, rho):
    val, grad = objective(w_, X, y, rho, return_H=False)
    return val

def gradMask(w_, X, y, rho):
    val, grad = objective(w_, X, y, rho, return_H=False)
    return grad

rho = 1./n
t0 = time()
print "The difference of gradient is: %0.12f" % check_grad(funcMask, gradMask, np.zeros(p+1), X, y, rho)
print "Done in %0.3fs." % (time()-t0)
```

    The difference of gradient is: 0.000000039161
    Done in 0.073s.

```python
def gradMask(w_, X, y, rho):
    val, grad = objective(w_, X, y, rho, return_H=False)
    return grad.sum()

def hessianMask(w_, X, y, rho):
    val, grad, H = objective(w_, X, y, rho)
    return np.sum(H, axis=1)

t0 = time()
rho = 1./n
print "The difference of Hessian matrix is: %0.12f" % check_grad(gradMask, hessianMask, np.zeros(p+1), X, y, rho)
print "Done in %0.3fs." % (time()-t0)
```

    The difference of Hessian matrix is: 0.000000758053
    Done in 0.079s.

On a vérifié le calcul de gradient et de matrice hessienne.

$\textbf{Question 1.3}\quad\text{Coder la méthode de Newton.}$

        Selon la définition de méthode de Newton, on a:
        $$\omega^{k+1} = \omega^k - (\nabla^2f_1(\omega^k))^{-1}\nabla f_1(\omega^k)$$

```python
def minimize_Newton(func, w_, X, y, rho, tol=1e-10):
    n, p = X.shape
    val, grad, H = func(w_, X, y, rho)
    grad_norm = np.sqrt(np.sum(grad**2))
    norms = [grad_norm]
    cnt = 0
    while (grad_norm > tol):
        w_ = w_ - np.linalg.solve(H, np.identity(p)).dot(grad)
        val, grad, H = func(w_, X, y, rho)
        grad_norm = np.sqrt(np.sum(grad**2))
        norms.append(grad_norm)
        cnt = cnt + 1
    return val, w_, cnt, norms

t0 = time()
rho = 1./n
val, w, cnt, grad_norms  = minimize_Newton(objective, np.zeros(p+1), X, y, rho, tol=1e-10)
print "The value minimal of the objective function is: %0.12f" % val
print "Done in %0.3fs, number of iterations: %d" % (time()-t0, cnt)
print w
```

    The value minimal of the objective function is: 0.564490766022
    Done in 0.043s, number of iterations: 7
    [ 0.2013495  -0.17213872 -0.04798368 -0.14852973  0.17226181 -0.00720006
      0.20673756 -0.03692106  0.0768277   0.43086559  0.12956021  0.17260728
      0.31592059  0.17684201  0.10474188  0.10474188 -0.39105042  0.1639398
      0.66262584 -0.33199982  0.64063084 -0.39451384 -0.39515206  0.3884854
      0.02848865  0.3884854   0.27776305]

```python
plt.figure(1, figsize=(8,6))
plt.title("The norm of gradient, $\omega^0 = 0$")
plt.semilogy(range(0, len(grad_norms)), grad_norms)
plt.xlabel("Number of iteration")
plt.ylabel("Norm of gradient")
plt.xlim(0, len(grad_norms))
plt.show()
```

$\textbf{Question 1.4}\quad\text{Lancer avec comme condition initiale }(\omega_0^0,\omega^0) = 0.3e\text{, où }e_i=0\text{ pour tout }i.$

```python
t0 = time()
val, grad, H, cnt, grad_norms  = minimize_Newton(objective, 0.3*np.ones(p+1), X, y, rho, tol=1e-10)
print "The value minimal of the objective function is: %0.12f" % val
print "Done in %0.3fs, number of iterations: %d" % (time()-t0, cnt)
```

On a vu que avec cette condition initiale, la fonction objectif ne converge pas. C'est à cause de le point initiale est hors le domaine de convergence.

$\textbf{Question 1.5}\quad\text{Coder la méthode de recherche linéaire d'Armijo.}$

Notons $\omega^+(\gamma_k)=\omega^k - \gamma_k(\nabla^2 f_1(\omega^k))^{-1}\nabla f_1(\omega^k)$, soient $a \in (0,1)$, $b>0$ et $\beta \in (0,1)$, on cherche le premier entier $l$ non-négatif tel que:

    $$f_1(\omega^+(ba^l)) \leq f_1(\omega^k) + \beta\langle\nabla_{f_1}(\omega^k),\,\omega^+(ba^l)-\omega^k\rangle$$

Ici, on prend $\beta = 0.5$, ainsi que la recherche linéaire d'Armijo devient équicalente à la recherche linéaire de Taylor.

 On fixe $b_0 = 1$ et $b_k = 2\gamma_{k-1}$, c'est un choix classique.

 On fixe $a = 0.5$, c'est pour faire un compromis entre la précision de recherche et la vitesse de convergence.

```python
def minimize_Newton_Armijo(func, w_, X, y, rho, a, b, beta, tol=1e-10, max_iter=500):
    n, p = X.shape
    val, grad, H = func(w_, X, y, rho)
    grad_norm = np.sqrt(np.sum(grad**2))
    norms = [grad_norm]
    d = np.linalg.solve(H, np.identity(p)).dot(grad)
    gamma = b / 2.
    cnt = 0
    while (grad_norm > tol and cnt < max_iter):
        gamma = 2*gamma
        val_ = func(w_ - gamma*d, X, y, rho, return_grad=False)
        while (val_ > val - beta*gamma*np.sum(d*grad)):
            gamma = gamma*a
            val_ = func(w_ - gamma*d, X, y, rho, return_grad=False)
        w_ = w_ - gamma*d
        val, grad, H = func(w_, X, y, rho)
        d = np.linalg.solve(H, np.identity(p)).dot(grad)
        grad_norm = np.sqrt(np.sum(grad**2))
        norms.append(grad_norm)
        cnt = cnt + 1
    return val, w_, cnt, norms

t0 = time()
rho = 1./n
a = 0.5
b = 1
beta = 0.5
val_nls, w_nls, cnt_nls, grad_norms_nls  = minimize_Newton_Armijo(objective, 0.3*np.ones(p+1), X, y, rho, a, b, beta, tol=1e-10, max_iter=500)
print "The value minimal of the objective function is: %0.12f" % val_nls
t_nls = time()-t0
print "Done in %0.3fs, number of iterations: %d" % (t_nls, cnt_nls)
print w_nls
```

    The value minimal of the objective function is: 0.564490766022
    Done in 0.082s, number of iterations: 9
    [ 0.2013495  -0.17213872 -0.04798368 -0.14852973  0.17226181 -0.00720006
      0.20673756 -0.03692106  0.0768277   0.43086559  0.12956021  0.17260728
      0.31592059  0.17684201  0.10474188  0.10474188 -0.39105042  0.1639398
      0.66262584 -0.33199982  0.64063084 -0.39451384 -0.39515206  0.3884854
      0.02848865  0.3884854   0.27776305]

```python
plt.figure(2, figsize=(8,6))
plt.title("The norm of gradient by Newton with linear search")
plt.semilogy(range(0, len(grad_norms_nls)), grad_norms_nls)
plt.xlabel("Number of iteration")
plt.ylabel("Norm of gradient")
plt.xlim(0, len(grad_norms_nls))
plt.show()
```

## Partie 2: Régularisation pour la parcimoine

$\textbf{Question 2.1}\quad\text{Pourquoi ne peut-on pas utiliser la méthode de Newton pour résoudre ce problème?}$

Parce que la fonction objectif ici n'est pas différentiable, on ne peut pas utiliser le gradient et la matrice hessienne.

$\textbf{Question 2.2}\quad\text{Écrire la fonction objectif sous la forme }F_2 = f_2 + g_2\text{ où }f_2\text{ est dérivable et l’opérateur proximal de }g_2\text{ est simple.}$

        $$
        \begin{aligned}
        F_2(\omega_0,\omega) &= \frac{1}{n}\sum_{i=1}^{n}\text{log}\big(1+e^{-y_i(x_i^T\omega+\omega_0)}\big)+\rho\|\omega\|_1 \\
        &= f_2+g_2
        \end{aligned}
        $$
        où $f_2 = \frac{1}{n}\sum_{i=1}^{n}\text{log}\big(1+e^{-y_i(x_i^T\omega+\omega_0)}\big)$ est dérivable, $g_2 = \rho\|\omega\|_1$ de laquelle l'opérateur proximal est simple.

        On a le gradient de $f_2$:
        $$
        \begin{aligned}
        \nabla{f_2}(\omega_0, \omega) &= \frac{1}{n}\sum_{i=1}^{n}\frac{-e^{-y_i\tilde x_i^T \tilde \omega}y_i\tilde{\mathbf{x}}_i}{1+e^{-y_i\tilde x_i^T \tilde \omega}} \\
        &= \frac{1}{n}\sum_{i=1}^{n}\frac{-y_i\tilde{\mathbf{x}}_i}{1+e^{y_i\tilde x_i^T \tilde \omega}}
        \end{aligned}
        $$

        et l'opérateur proximal de $g_2$:
        $$
        \begin{aligned}
        \text{prox}_{g_2}(x) &= \text{arg}\,\underset{y \in \mathbb{R}^p}{\text{min}}\, \big(g_2(y) + \frac{1}{2}\|y-x\|^2 \big) \\
        &= \text{arg}\,\underset{y \in \mathbb{R}^p}{\text{min}}\, \big(\rho\|y\|_1 + \frac{1}{2}\|y-x\|^2 \big) \\
        &= \text{arg}\,\underset{y \in \mathbb{R}^p}{\text{min}}\, \sum_{i=1}^{p}\big(\rho |y_i| + \frac{1}{2}(y_i-x_i)^2\big)
        \end{aligned}
        $$

    pour $1 \leq i \leq n$, on obtient la solution:
    $$
    y_i^* = \left\{
    \begin{align}
    x_i - \rho, &\text{ si } x_i > \rho \\
    x_i + \rho, &\text{ si } x_i < -\rho \\
    0, &\text{ si } -\rho \leq x_i \leq \rho
    \end{align}
    \right.
    $$

        $$
        \begin{aligned}
        \mathbf{H_2} = \nabla^2f_2(\omega_0, \omega) &= \frac{1}{n}\sum_{i=1}^{n}\frac{e^{y_i\tilde x_i^T \tilde \omega}(y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})^2} \\
        & = \frac{1}{n}\sum_{i=1}^{n}\frac{(y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})}
        \end{aligned}
        $$

    Soient $\omega \in \mathbb{R}^{p+1}$, on a:
        $$
        \begin{aligned}
        \omega^TH_2\omega &= \frac{1}{n}\sum_{i=1}^{n}\frac{\omega^T (y_i\tilde{\mathbf{x}}_i)(y_i\tilde{\mathbf{x}}_i)^T \omega}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} \\
        &= \frac{1}{n}\sum_{i=1}^{n}\frac{(\omega^T y_i\tilde{\mathbf{x}}_i)(\omega^T y_i\tilde{\mathbf{x}}_i)^T}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} \\
        &= \frac{1}{n}\sum_{i=1}^{n}\frac{\|\omega^T y_i\tilde{\mathbf{x}}_i\|_2^2}{(1+e^{y_i\tilde x_i^T \tilde \omega})(1+e^{-y_i\tilde x_i^T \tilde \omega})} \geq 0
        \end{aligned}
        $$

Donc, la matrice hessienne de $f_2$ est semi-définie positive, la fonction $f_2$ est convexe.

    $$
        \begin{aligned}
        g_2(\omega_0, \omega) &= \rho\|\omega\|_1 \\
        &= \rho \sum_{i=1}^{n}|\omega_i|
        \end{aligned}
    $$

La fonction de valeur absolue est convexe pour chaque élément de $\omega$, pour $\rho \geq 0$, $g_2$ est aussi convexe.

Donc $F_2 = f_2 + g_2$ est convexe pour $\rho \geq 0$.

$\textbf{Question 2.3}\quad\text{Coder le gradient proximal avec recherche linéaire.}$

On rajoute la recherche linéaire de Taylor.

On prend $a = 0.5$, $b_0 = 1b$ et $b = 2\gamma_{k-1}$. On cherche le premier entier $l$ non-négatif tel que:

    $$f_2(\omega^+(ba^l)) \leq f_2(\omega^k) + \langle\nabla_{f_2}(\omega^k),\,\omega^+(ba^l)-\omega^k\rangle + \frac{1}{2ba^l}\|\omega^k - \omega^+(ba^l)\|^2$$

    On peut utiliser un seuillage pour la valeur de fonction objectif évaluée dans une itération comme test d'arrêt.

```python
def objective_proximal(w_, X, y, rho):
    """
    X: matrix of size n*(p+1)
    y: vector of size n
    w0: real number
    w: vector of size p
    """
    # Initialize elementary intermediate variables;
    n, p = X.shape
    w = w_[1:]
    y_x = np.array([y[i] * X[i, :] for i in range(n)])
    yx_w = np.array([np.sum(y_x[i, :]*w_) for i in range(n)])
    exp_neg_yxw_1 = np.array([np.exp(-yx_w[i]) for i in range(n)]) + 1

    # Compute function value
    val = np.mean(np.log(exp_neg_yxw_1)) + rho*np.sum(np.fabs(w))
    return val

def f(w_, X, y, return_grad=True):
    """
    X: matrix of size n*(p+1)
    y: vector of size n
    w0: real number
    w: vector of size p
    """
    # Initialize elementary intermediate variables;
    n, p = X.shape
    w = w_[1:]
    y_x = np.array([y[i] * X[i, :] for i in range(n)])
    yx_w = np.array([np.sum(y_x[i, :]*w_) for i in range(n)])
    exp_yxw_1 = np.array([np.exp(yx_w[i]) for i in range(n)]) + 1
    exp_neg_yxw_1 = np.array([np.exp(-yx_w[i]) for i in range(n)]) + 1

    # Compute function value
    val = np.mean(np.log(exp_neg_yxw_1))

    if return_grad == False:
        return val
    else:
        # Compute gradient
        grad = np.mean(-np.array([y_x[i]/exp_yxw_1[i] for i in range(n)]), axis=0)
        return val, grad

def Soft_Threshold(w, rho):
    w_ = np.zeros_like(w)
    w_[w > rho] = w[w > rho] - rho
    w_[w < -rho] = w[w < -rho] + rho
    w_[0] = w[0]
    return w_

def minimize_prox_grad_Taylor(func, f, w_, X, y, rho, a, b, tol=1e-10, max_iter=500):
    n, p = X.shape
    val = func(w_, X, y, rho)
    val_f, grad_f = f(w_, X, y)
    gamma = b / 2.
    delta_val = tol*2
    cnt = 0
    while (delta_val > tol and cnt < max_iter):
        gamma = 2*gamma
        w_new = Soft_Threshold(w_ - gamma*grad_f, gamma*rho)
        val_f_ = f(w_new, X, y, return_grad=False)
        #while (val_f_ > val_f + beta*np.sum(grad_f*(w_new - w_))):
        while (val_f_ > val_f + np.sum(grad_f*(w_new-w_)) + np.sum((w_new-w_)**2)/gamma):
            #print val_
            gamma = gamma*a
            w_new = Soft_Threshold(w_ - gamma*grad_f, gamma*rho)
            val_f_ = f(w_new, X, y, return_grad=False)
        w_ = w_new
        val_f, grad_f = f(w_, X, y)
        val_ = func(w_, X, y, rho)
        delta_val = val - val_
        val = val_
        cnt = cnt + 1
    return func(w_, X, y, rho), w_, cnt

t0 = time()
rho = 0.1
a = 0.5
b = 1
val_pgls, w_pgls, cnt_pgls  = minimize_prox_grad_Taylor(objective_proximal, f, 0.3*np.ones(p+1), X, y, rho, a, b, tol=1e-8, max_iter=500)
print "The value minimal of the objective function is: %0.12f" % val_pgls
t_pgls = time()-t0
print "Done in %0.3fs, number of iterations: %d" % (t_pgls, cnt_pgls)
print w_pgls
```

    The value minimal of the objective function is: 0.692565193505
    Done in 0.072s, number of iterations: 8
    [-0.05716261  0.          0.          0.          0.          0.          0.
      0.          0.          0.          0.          0.          0.          0.004657
      0.          0.          0.          0.          0.          0.
      0.00538332  0.          0.          0.01859929  0.          0.01859929
      0.        ]

```python
def objective_proximal(w_, X, y, rho, return_grad=True):
    """
    X: matrix of size n*(p+1)
    y: vector of size n
    w0: real number
    w: vector of size p
    """
    # Initialize elementary intermediate variables;
    n, p = X.shape
    w = w_[1:]
    y_x = np.array([y[i] * X[i, :] for i in range(n)])
    yx_w = np.array([np.sum(y_x[i, :]*w_) for i in range(n)])
    exp_yxw_1 = np.array([np.exp(yx_w[i]) for i in range(n)]) + 1
    exp_neg_yxw_1 = np.array([np.exp(-yx_w[i]) for i in range(n)]) + 1

    # Compute function value
    val = np.mean(np.log(exp_neg_yxw_1)) + rho*np.sum(np.fabs(w))

    if return_grad == False:
        return val
    else:
        # Compute gradient
        grad = np.mean(-np.array([y_x[i]/exp_yxw_1[i] for i in range(n)]), axis=0)
        return val, grad

def Soft_Threshold(w, rho):
    w_ = np.zeros_like(w)
    w_[w > rho] = w[w > rho] - rho
    w_[w < -rho] = w[w < -rho] + rho
    w_[0] = w[0]
    return w_

def minimize_prox_grad_Armijo(func, w_, X, y, rho, a, b, beta, tol=1e-10, max_iter=500):
    val, grad = func(w_, X, y, rho)
    gamma = b / 2.
    delta_val = tol*2
    cnt = 0
    while (delta_val > tol and cnt < max_iter):
        gamma = 2*gamma
        w_new = Soft_Threshold(w_ - gamma*grad, rho)
        val_ = func(w_new, X, y, rho, return_grad=False)
        while (val_ > val + beta*np.sum(grad*(w_new - w_))):
            gamma = gamma*a
            w_new = Soft_Threshold(w_ - gamma*grad, rho)
            val_ = func(w_new, X, y, rho, return_grad=False)
        w_ = w_new
        val_, grad = func(w_, X, y, rho)
        delta_val = val - val_
        val = val_
        cnt = cnt + 1
    return val, w_, cnt

t0 = time()
rho = 0.1
a = 0.7
b = 1
beta = 0.5
val_pgls, w_pgls, cnt_pgls  = minimize_prox_grad_Armijo(objective_proximal, 0.3*np.ones(p+1), X, y, rho, a, b, beta, tol=1e-6, max_iter=500)
print "The value minimal of the objective function is: %0.12f" % val_pgls
t_pgls = time()-t0
print "Done in %0.3fs, number of iterations: %d" % (t_pgls, cnt_pgls)
print w_pgls
```

    The value minimal of the objective function is: 0.692577339566
    Done in 0.178s, number of iterations: 21
    [-0.0567545   0.          0.          0.          0.          0.          0.
      0.          0.          0.          0.          0.          0.          0.
      0.          0.          0.          0.          0.          0.
      0.01092454  0.          0.          0.0130465   0.          0.0130465   0.        ]

## Partie 3: Comparaison

$\textbf{Question 3.1}\quad\text{Comparer les propriétés des deux problèmes d’optimisation.}$

1. Toutes les deux fonctions objectifs sont convexes, laquelle de régularisation de Tikhonov est différentible, l'autre n'est pas différentiable.

2. Selon les deux $\omega$ qu'on obtient, la régularisation de Tiknonov utilise tous les variables explicatives, la régularisation pour la parcimoine en utilise une partie.

$\textbf{Question 3.2}\quad\text{Comparer les solutions obtenues avec les deux types de régularisation.}$

```python
y_pred_nls = np.sign(X.dot(w_nls))
y_pred_pgls = np.sign(X.dot(w_pgls))
print "The chance level is: %f" % max(np.mean(y == 1), 1-np.mean(y == 1))
print "The score by Newton method with line search is: %f" % np.mean(y == y_pred_nls)
print "The score by proximal gradient method with line search is: %f" % np.mean(y == y_pred_pgls)

print "-"*60
print "Classification report for Newton method"
print classification_report(y, y_pred_nls)
print "-"*60
print "Classification report for proximal gradient method"
print classification_report(y, y_pred_pgls)
```

    The chance level is: 0.514286
    The score by Newton method with line search is: 0.685714
    The score by proximal gradient method with line search is: 0.561905
    ------------------------------------------------------------
    Classification report for Newton method
                 precision    recall  f1-score   support

           -1.0       0.65      0.84      0.73       108
            1.0       0.76      0.52      0.62       102

    avg / total       0.70      0.69      0.68       210

    ------------------------------------------------------------
    Classification report for proximal gradient method
                 precision    recall  f1-score   support

           -1.0       0.54      1.00      0.70       108
            1.0       1.00      0.10      0.18       102

    avg / total       0.76      0.56      0.45       210

En comparant les scores et les rapports de classification:

1. Le score obtenu par la méthode de Newton est meilleur que celui de la méthode de gradient proximal.

2. Selon le f1-score, la méthode de Newton est aussi meilleur.

3. Dans la méthode de gradient proximal, la «precision» pour class 1 est 1.0, de plus, la «recall» est 0.1. On peut conclure que cette méthode avandage la class 1.


```python
# ============================================================
# Notebook setup
# ============================================================

%load_ext autoreload
%autoreload 2

# Control figure size
interactive_figures = False
if interactive_figures:
    # Normal behavior
    %matplotlib widget
    figsize=(9, 3)
else:
    # PDF export behavior
    figsize=(14, 5)

from util import cmapss
import numpy as np
from matplotlib import pyplot as plt
import pandas as pd
from tensorflow import keras
from tensorflow.keras import layers, callbacks
from tensorflow_probability import distributions
from sklearn.metrics import r2_score

# Load data
data_folder = 'data'
data = cmapss.load_data(data_folder)

# Identify input columns
dt_in = list(data.columns[3:-1])
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

# RUL Prediction as Regression

## RUL Prediction as Regression

**Let's start from _the most straightforward formulation_ of a RUL problem**

* We will predict the RUL using a regression approach
* ...And trigger maintenance when the estimated RUL becomes too low, i.e.:

$$
f(x, \lambda) < \theta
$$

* $f$ is the regressor, with parameter vector $\lambda$
* The threshold $\theta$ must account for possible estimation errors

**We will focus on the hardest of the four datasets (to reduce training times):**

```python
data_by_src = cmapss.split_by_field(data, field='src')
dt = data_by_src['train_FD004']
```

## Training and Test Data

**We now need to define our training and test data**

In a practical setting:

* Some run-to-failure experiments will form the training set
* Others run-to-failure experiments will be used for testing

I.e. we split _whole experiments_ rather than individual examples!

**Each run-to-failure experiment in our data is associated to a machine**

Let's check how many we have:

```python
print(f'Number of machines: {len(dt.machine.unique())}')
```

    Number of machines: 249

* This is actually a very large number
* In most practical setting, _much fewer_ experiments will be available

## Training and Test Data

**Let's use 75% of the machine for training, the rest for testing**

First, we partition the machine indexes:

```python
tr_ratio = 0.75
np.random.seed(42)
machines = dt.machine.unique()
np.random.shuffle(machines)

sep = int(tr_ratio * len(machines))
tr_mcn = machines[:sep]
ts_mcn = machines[sep:]
```

Then, we partition the dataset itself (via a helper function):

```python
tr, ts = cmapss.partition_by_machine(dt, tr_mcn)
```

## Training and Test Data

**Let's have a look at the training data**

```python
tr
```

      src
      machine
      cycle
      p1
      p2
      p3
      s1
      s2
      s3
      s4
      ...
      s13
      s14
      s15
      s16
      s17
      s18
      s19
      s20
      s21
      rul

      0
      train_FD004
      461
      1
      42.0049
      0.8400
      100.0
      445.00
      549.68
      1343.43
      1112.93
      ...
      2387.99
      8074.83
      9.3335
      0.02
      330
      2212
      100.00
      10.62
      6.3670
      320

      1
      train_FD004
      461
      2
      20.0020
      0.7002
      100.0
      491.19
      606.07
      1477.61
      1237.50
      ...
      2387.73
      8046.13
      9.1913
      0.02
      361
      2324
      100.00
      24.37
      14.6552
      319

      2
      train_FD004
      461
      3
      42.0038
      0.8409
      100.0
      445.00
      548.95
      1343.12
      1117.05
      ...
      2387.97
      8066.62
      9.4007
      0.02
      329
      2212
      100.00
      10.48
      6.4213
      318

      3
      train_FD004
      461
      4
      42.0000
      0.8400
      100.0
      445.00
      548.70
      1341.24
      1118.03
      ...
      2388.02
      8076.05
      9.3369
      0.02
      328
      2212
      100.00
      10.54
      6.4176
      317

      4
      train_FD004
      461
      5
      25.0063
      0.6207
      60.0
      462.54
      536.10
      1255.23
      1033.59
      ...
      2028.08
      7865.80
      10.8366
      0.02
      305
      1915
      84.93
      14.03
      8.6754
      316

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      60989
      train_FD004
      708
      180
      35.0019
      0.8409
      100.0
      449.44
      556.28
      1377.65
      1148.96
      ...
      2387.77
      8048.91
      9.4169
      0.02
      337
      2223
      100.00
      14.66
      8.7446
      4

      60990
      train_FD004
      708
      181
      0.0023
      0.0000
      100.0
      518.67
      643.95
      1602.98
      1429.57
      ...
      2388.27
      8122.44
      8.5242
      0.03
      396
      2388
      100.00
      38.40
      23.1079
      3

      60991
      train_FD004
      708
      182
      25.0030
      0.6200
      60.0
      462.54
      536.88
      1268.01
      1067.09
      ...
      2027.98
      7865.18
      10.9790
      0.02
      309
      1915
      84.93
      14.24
      8.4254
      2

      60992
      train_FD004
      708
      183
      41.9984
      0.8414
      100.0
      445.00
      550.64
      1363.76
      1145.72
      ...
      2387.48
      8069.84
      9.4607
      0.02
      333
      2212
      100.00
      10.37
      6.2727
      1

      60993
      train_FD004
      708
      184
      0.0013
      0.0001
      100.0
      518.67
      643.50
      1602.12
      1430.34
      ...
      2388.33
      8120.43
      8.4998
      0.03
      395
      2388
      100.00
      38.48
      23.0767
      0

45385 rows × 28 columns

## Training and Test Data

**...And at the test data**

```python
ts
```

      src
      machine
      cycle
      p1
      p2
      p3
      s1
      s2
      s3
      s4
      ...
      s13
      s14
      s15
      s16
      s17
      s18
      s19
      s20
      s21
      rul

      321
      train_FD004
      462
      1
      41.9998
      0.8400
      100.0
      445.00
      548.99
      1341.82
      1113.16
      ...
      2387.98
      8082.37
      9.3300
      0.02
      331
      2212
      100.00
      10.77
      6.2894
      298

      322
      train_FD004
      462
      2
      9.9999
      0.2500
      100.0
      489.05
      604.23
      1498.00
      1299.54
      ...
      2388.07
      8125.46
      8.6088
      0.03
      368
      2319
      100.00
      28.61
      17.3135
      297

      323
      train_FD004
      462
      3
      42.0079
      0.8403
      100.0
      445.00
      549.11
      1351.47
      1126.43
      ...
      2387.93
      8082.11
      9.2965
      0.02
      330
      2212
      100.00
      10.70
      6.4288
      296

      324
      train_FD004
      462
      4
      42.0077
      0.8400
      100.0
      445.00
      548.77
      1345.81
      1116.64
      ...
      2387.88
      8079.41
      9.3200
      0.02
      330
      2212
      100.00
      10.50
      6.2818
      295

      325
      train_FD004
      462
      5
      24.9999
      0.6200
      60.0
      462.54
      537.00
      1259.55
      1043.95
      ...
      2028.13
      7867.08
      10.8841
      0.02
      307
      1915
      84.93
      14.26
      8.5789
      294

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      61244
      train_FD004
      709
      251
      9.9998
      0.2500
      100.0
      489.05
      605.33
      1516.36
      1315.28
      ...
      2388.73
      8185.69
      8.4541
      0.03
      372
      2319
      100.00
      29.11
      17.5234
      4

      61245
      train_FD004
      709
      252
      0.0028
      0.0015
      100.0
      518.67
      643.42
      1598.92
      1426.77
      ...
      2388.46
      8185.47
      8.2221
      0.03
      396
      2388
      100.00
      39.38
      23.7151
      3

      61246
      train_FD004
      709
      253
      0.0029
      0.0000
      100.0
      518.67
      643.68
      1607.72
      1430.56
      ...
      2388.48
      8193.94
      8.2525
      0.03
      395
      2388
      100.00
      39.78
      23.8270
      2

      61247
      train_FD004
      709
      254
      35.0046
      0.8400
      100.0
      449.44
      555.77
      1381.29
      1148.18
      ...
      2388.83
      8125.64
      9.0515
      0.02
      337
      2223
      100.00
      15.26
      9.0774
      1

      61248
      train_FD004
      709
      255
      42.0030
      0.8400
      100.0
      445.00
      549.85
      1369.75
      1147.45
      ...
      2388.66
      8144.33
      9.1207
      0.02
      333
      2212
      100.00
      10.66
      6.4341
      0

15864 rows × 28 columns

## Standardization/Normalization

**Now, we need to make the range of each columns more uniform**

We will _standardize_ all parameters and sensor inputs:

```python
trmean = tr[dt_in].mean()
trstd = tr[dt_in].std().replace(to_replace=0, value=1) # handle static fields

ts_s = ts.copy()
ts_s[dt_in] = (ts_s[dt_in] - trmean) / trstd
tr_s = tr.copy()
tr_s[dt_in] = (tr_s[dt_in] - trmean) / trstd
```

We will _normalize_ the RUL values (i.e. our regression target)

```python
trmaxrul = tr['rul'].max()

ts_s['rul'] = ts['rul'] / trmaxrul
tr_s['rul'] = tr['rul'] / trmaxrul
```

## Standardization/Normalization

**Let's check the results**

```python
tr_s.describe()
```

      machine
      cycle
      p1
      p2
      p3
      s1
      s2
      s3
      s4
      s5
      ...
      s13
      s14
      s15
      s16
      s17
      s18
      s19
      s20
      s21
      rul

      count
      45385.000000
      45385.000000
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      ...
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      4.538500e+04
      45385.000000

      mean
      582.490955
      133.323896
      2.894775e-16
      1.302570e-16
      1.178889e-16
      4.664830e-15
      2.522791e-15
      1.727041e-15
      -6.633794e-16
      1.496703e-16
      ...
      1.135835e-15
      -6.017186e-15
      -1.409030e-16
      -1.058182e-15
      -6.532030e-16
      1.583594e-16
      -3.093135e-15
      2.572263e-16
      -1.477916e-16
      0.244140

      std
      71.283034
      89.568561
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      ...
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      1.000000e+00
      0.165256

      min
      461.000000
      1.000000
      -1.623164e+00
      -1.838222e+00
      -2.381839e+00
      -1.055641e+00
      -1.176507e+00
      -1.646830e+00
      -1.486984e+00
      -1.138606e+00
      ...
      -2.387561e+00
      -2.583315e+00
      -1.480340e+00
      -6.950269e-01
      -1.644539e+00
      -2.151433e+00
      -2.381839e+00
      -1.077489e+00
      -1.079308e+00
      0.000000

      25%
      521.000000
      62.000000
      -9.461510e-01
      -1.031405e+00
      4.198344e-01
      -1.055641e+00
      -8.055879e-01
      -6.341243e-01
      -6.912917e-01
      -1.138606e+00
      ...
      4.181950e-01
      -5.977256e-02
      -8.501637e-01
      -6.950269e-01
      -6.379310e-01
      -1.126840e-01
      4.198344e-01
      -9.979498e-01
      -9.988926e-01
      0.112546

      50%
      585.000000
      123.000000
      6.868497e-02
      4.154560e-01
      4.198344e-01
      -3.917563e-01
      -6.336530e-01
      -4.718540e-01
      -5.429267e-01
      -2.714913e-01
      ...
      4.194408e-01
      1.862163e-01
      -4.025983e-02
      -6.950269e-01
      -4.941298e-01
      -3.717480e-02
      4.198344e-01
      -5.972306e-01
      -5.971843e-01
      0.225092

      75%
      639.000000
      189.000000
      1.218855e+00
      8.661917e-01
      4.198344e-01
      6.926385e-01
      7.407549e-01
      7.495521e-01
      8.439630e-01
      6.867534e-01
      ...
      4.202973e-01
      7.086220e-01
      1.055441e-01
      1.438762e+00
      7.641306e-01
      6.561373e-01
      4.198344e-01
      7.740747e-01
      7.744926e-01
      0.346863

      max
      708.000000
      543.000000
      1.219524e+00
      8.726308e-01
      4.198344e-01
      1.732749e+00
      1.741030e+00
      1.837978e+00
      2.000975e+00
      1.818973e+00
      ...
      4.383623e-01
      2.264997e+00
      2.365107e+00
      1.438762e+00
      1.806689e+00
      1.095464e+00
      4.198344e-01
      1.915822e+00
      1.907272e+00
      1.000000

8 rows × 27 columns

## Regression Model

**We can now define a regression model**

We will use a feed-forward neural network (MLP):

```python
def build_regressor(hidden):
    input_shape = (len(dt_in), )
    model_in = keras.Input(shape=input_shape, dtype='float32')
    x = model_in
    for h in hidden:
        x = layers.Dense(h, activation='relu')(x)
    model_out = layers.Dense(1, activation='linear')(x)
    model = keras.Model(model_in, model_out)
    return model
```

* The `hidden` argument is a list of sizes for the hidden layers
* ...E.g. `hidden = [64, 32]`
* By setting `hidden=[]` we obtain a simple Linear Regression approach

## Training

**We can now train our model**

```python
nn = build_regressor(hidden=[])
nn.compile(optimizer='Adam', loss='mse')
cb = [callbacks.EarlyStopping(patience=10, restore_best_weights=True)]
history = nn.fit(tr_s[dt_in], tr_s['rul'], validation_split=0.2,
                 callbacks=cb, batch_size=32, epochs=20, verbose=1)
```

    2021-12-04 11:33:12.332619: I tensorflow/core/platform/cpu_feature_guard.cc:151] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 FMA
    To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.

    Epoch 1/20
    1135/1135 [==============================] - 1s 702us/step - loss: 0.3791 - val_loss: 0.0936
    Epoch 2/20
    1135/1135 [==============================] - 1s 636us/step - loss: 0.0813 - val_loss: 0.0637
    Epoch 3/20
    1135/1135 [==============================] - 1s 612us/step - loss: 0.0561 - val_loss: 0.0418
    Epoch 4/20
    1135/1135 [==============================] - 1s 617us/step - loss: 0.0383 - val_loss: 0.0286
    Epoch 5/20
    1135/1135 [==============================] - 1s 620us/step - loss: 0.0281 - val_loss: 0.0218
    Epoch 6/20
    1135/1135 [==============================] - 1s 622us/step - loss: 0.0226 - val_loss: 0.0179
    Epoch 7/20
    1135/1135 [==============================] - 1s 610us/step - loss: 0.0193 - val_loss: 0.0155
    Epoch 8/20
    1135/1135 [==============================] - 1s 610us/step - loss: 0.0173 - val_loss: 0.0136
    Epoch 9/20
    1135/1135 [==============================] - 1s 637us/step - loss: 0.0160 - val_loss: 0.0128
    Epoch 10/20
    1135/1135 [==============================] - 1s 617us/step - loss: 0.0154 - val_loss: 0.0123
    Epoch 11/20
    1135/1135 [==============================] - 1s 611us/step - loss: 0.0149 - val_loss: 0.0114
    Epoch 12/20
    1135/1135 [==============================] - 1s 607us/step - loss: 0.0147 - val_loss: 0.0114
    Epoch 13/20
    1135/1135 [==============================] - 1s 609us/step - loss: 0.0146 - val_loss: 0.0118
    Epoch 14/20
    1135/1135 [==============================] - 1s 618us/step - loss: 0.0144 - val_loss: 0.0112
    Epoch 15/20
    1135/1135 [==============================] - 1s 611us/step - loss: 0.0144 - val_loss: 0.0116
    Epoch 16/20
    1135/1135 [==============================] - 1s 611us/step - loss: 0.0143 - val_loss: 0.0108
    Epoch 17/20
    1135/1135 [==============================] - 1s 608us/step - loss: 0.0143 - val_loss: 0.0110
    Epoch 18/20
    1135/1135 [==============================] - 1s 609us/step - loss: 0.0143 - val_loss: 0.0113
    Epoch 19/20
    1135/1135 [==============================] - 1s 620us/step - loss: 0.0143 - val_loss: 0.0107
    Epoch 20/20
    1135/1135 [==============================] - 1s 661us/step - loss: 0.0143 - val_loss: 0.0109

## Training

**Here's the loss evolution over time and its final value**

```python
cmapss.plot_training_history(history, figsize=figsize)
trl, vll = history.history["loss"][-1], np.min(history.history["val_loss"])
print(f'Final loss: {trl:.4f} (training), {vll:.4f} (validation)')
```

## Training

**Let's try with a more complex model**

```python
nn = build_regressor(hidden=[32, 32])
nn.compile(optimizer='Adam', loss='mse')
history = nn.fit(tr_s[dt_in], tr_s['rul'], validation_split=0.2,
                 callbacks=cb, batch_size=32, epochs=20, verbose=1)
```

    Epoch 1/20
    1135/1135 [==============================] - 1s 844us/step - loss: 0.0228 - val_loss: 0.0165
    Epoch 2/20
    1135/1135 [==============================] - 1s 731us/step - loss: 0.0152 - val_loss: 0.0120
    Epoch 3/20
    1135/1135 [==============================] - 1s 732us/step - loss: 0.0146 - val_loss: 0.0113
    Epoch 4/20
    1135/1135 [==============================] - 1s 732us/step - loss: 0.0144 - val_loss: 0.0118
    Epoch 5/20
    1135/1135 [==============================] - 1s 732us/step - loss: 0.0142 - val_loss: 0.0112
    Epoch 6/20
    1135/1135 [==============================] - 1s 740us/step - loss: 0.0141 - val_loss: 0.0107
    Epoch 7/20
    1135/1135 [==============================] - 1s 748us/step - loss: 0.0139 - val_loss: 0.0105
    Epoch 8/20
    1135/1135 [==============================] - 1s 791us/step - loss: 0.0139 - val_loss: 0.0109
    Epoch 9/20
    1135/1135 [==============================] - 1s 733us/step - loss: 0.0137 - val_loss: 0.0110
    Epoch 10/20
    1135/1135 [==============================] - 1s 733us/step - loss: 0.0137 - val_loss: 0.0104
    Epoch 11/20
    1135/1135 [==============================] - 1s 788us/step - loss: 0.0137 - val_loss: 0.0126
    Epoch 12/20
    1135/1135 [==============================] - 1s 763us/step - loss: 0.0136 - val_loss: 0.0110
    Epoch 13/20
    1135/1135 [==============================] - 1s 781us/step - loss: 0.0135 - val_loss: 0.0110
    Epoch 14/20
    1135/1135 [==============================] - 1s 739us/step - loss: 0.0135 - val_loss: 0.0107
    Epoch 15/20
    1135/1135 [==============================] - 1s 739us/step - loss: 0.0135 - val_loss: 0.0114
    Epoch 16/20
    1135/1135 [==============================] - 1s 787us/step - loss: 0.0135 - val_loss: 0.0111
    Epoch 17/20
    1135/1135 [==============================] - 1s 734us/step - loss: 0.0135 - val_loss: 0.0107
    Epoch 18/20
    1135/1135 [==============================] - 1s 746us/step - loss: 0.0134 - val_loss: 0.0106
    Epoch 19/20
    1135/1135 [==============================] - 1s 742us/step - loss: 0.0134 - val_loss: 0.0103
    Epoch 20/20
    1135/1135 [==============================] - 1s 730us/step - loss: 0.0133 - val_loss: 0.0108

## Training

**Let's check the loss behavior and compare it to Linear Regression**

```python
cmapss.plot_training_history(history, figsize=figsize)
trl, vll = history.history["loss"][-1], np.min(history.history["val_loss"])
print(f'Final loss: {trl:.4f} (training), {vll:.4f} (validation)')
```

* There is some improvement w.r.t. pure Linear Regression

## Predictions

**We can now obtain the predictions and evaluate their quality**

```python
tr_pred = nn.predict(tr_s[dt_in]).ravel() * trmaxrul
cmapss.plot_pred_scatter(tr_pred, tr['rul'], figsize=figsize)
print(f'R2 score: {r2_score(tr["rul"], tr_pred)}')
```

## Predictions

**The results so far are not comforting**

...But it's worth seeing what is going on over time:

```python
stop = 1095
cmapss.plot_rul(tr_pred[:stop], tr['rul'][:stop], figsize=figsize)
```

## Predictions

**The situation is similar on the test set:**

```python
ts_pred = nn.predict(ts_s[dt_in]).ravel() * trmaxrul
cmapss.plot_rul(ts_pred[:stop], ts['rul'][:stop], figsize=figsize)
```

## Quality Evaluation

**Let's try to recap the situation**

* Our accuracy is quite poor
* ...Especially for large RUL values

**Possible reasons:**

* Large RUL value are somewhat scarce on the dataset
* Fault effects become noticeable only after a while

**But perhaps we don't care! Our goal is _not a high accuracy_**

* We just need to _stop at the right time_
* ...And our model may still be good enough for that

> **For a proper evaluation, we need a _cost model_**

## Cost Model

**We will assume that:**

* Whenever a turbine operates for a time step, we gain a profit of 1 unit
* A failure costs $C$ units (i.e. the equivalent of $C$ operation days)
* We never trigger maintenance before $s$ time steps

**Let $x_k$ be the times series for machine $k$, and $I_k$ its set of time steps**

With our RUL based policy:

* The time step when we trigger maintenance is given by:
$$
\min \{i \in I_k \mid f(x_{ki}) < \theta\}
$$
* A failure occurs if:
$$
f(x_{ki}) \geq \theta \quad \forall i \in I_k
$$

## Cost Model

**The whole cost formula _for a single machine_ will be:**

$$
\mathit{cost}(f, {x_k}, \theta) = \mathit{op\_profit}(f(x_k), \theta) + \mathit{fail\_cost}(f(x_k), \theta)
$$

Where:

$$
\mathit{op\_profit}(f(x_k), \theta) = -\max(0, \min \{i \in I_k \mid f(x_{ki}) < \theta\}-s) \\
\mathit{fail\_cost}(f(x_k), \theta) = \left\{
\begin{align}
& C \text{ if } f(x_{ki}) \geq \theta \quad \forall i \in I_k \\
& 0 \text{ otherwise}
\end{align}
\right.
$$

* $s$ units of machine operation are guaranteed
* ...So we gain over the default policy only if we stop after that
* Profit is modeled as a negative cost

**For the total cost, we need to sum over all machines**

## Cost Model

**Normally, we would proceed as follows**

* $s$ is determined by the fixed maintenance schedule
* $C$ must be determined by discussing with the customer

In our example, we will derive both from data

**First, we collect all failure times**

```python
failtimes = dt.groupby('machine')['cycle'].max()
failtimes.head()
```

    machine
    461    321
    462    299
    463    307
    464    274
    465    193
    Name: cycle, dtype: int64

## Cost Model

**Then, we define $s$ and $C$ based on statistics**

```python
print(failtimes.describe())
safe_interval = failtimes.min()
maintenance_cost = failtimes.max()
```

    count    249.00000
    mean     245.97992
    std       73.11080
    min      128.00000
    25%      190.00000
    50%      234.00000
    75%      290.00000
    max      543.00000
    Name: cycle, dtype: float64

* For the safe interval $s$, we choose the minimum failure time
* For the maintenance cost $C$ we choose the largest failure time

## Threshold Choice

**We can then choose the threshold $\theta$ as usual**

```python
cmodel = cmapss.RULCostModel(maintenance_cost=maintenance_cost, safe_interval=safe_interval)
th_range = np.arange(0, 100)
tr_thr = cmapss.opt_threshold_and_plot(tr['machine'].values, tr_pred, th_range, cmodel, figsize=figsize)
print(f'Optimal threshold for the training set: {tr_thr}')
```

## Evaluation

**Let's see how we fare in terms of cost**

```python
tr_c, tr_f, tr_sl = cmodel.cost(tr['machine'].values, tr_pred, tr_thr, return_margin=True)
ts_c, ts_f, ts_sl = cmodel.cost(ts['machine'].values, ts_pred, tr_thr, return_margin=True)
print(f'Cost: {tr_c} (training), {ts_c} (test)')
```

    Cost: -18605 (training), -6980 (test)

We can also evaluate the margin for improvement:

```python
print(f'Avg. fails: {tr_f/len(tr_mcn)} (training), {ts_f/len(ts_mcn)} (test)')
print(f'Avg. slack: {tr_sl/len(tr_mcn):.2f} (training), {ts_sl/len(ts_mcn):.2f} (test)')
```

    Avg. fails: 0.0 (training), 0.0 (test)
    Avg. slack: 16.13 (training), 13.02 (test)

* Slack = distance between when we stop and the failure
* The results are actually quite good!
* ...And we also generalize fairly well

# Sequence Input in Neural Models

## Sequence Input in Neural Models

**Feeding more time steps to our NN may improve the results**

* Intuitively, sequences provide information about the _trend_
* This may allow a better RUL estimate w.r.t. using only the current state
* E.g. we may gauge how quickly the component is deteriorating

**We will try to build a model capable to processing such input**

But first, we need to apply a sliding window:

```python
wlen = 3
tr_sw, tr_sw_m, tr_sw_r = cmapss.sliding_window_by_machine(tr_s, wlen, dt_in)
ts_sw, ts_sw_m, ts_sw_r = cmapss.sliding_window_by_machine(ts_s, wlen, dt_in)
```

* This must be done on a _per machine_ basis
* Windows _should not mix data_ belonging to different machines!

## Sliding Window for Multivariate Data

**The `sliding_window_by_machine` relies internally on:**

```python
def sliding_window_2D(data, wlen, stride=1):
    # Get shifted _tables_
    m = len(data)
    lt = [data.iloc[i:m-wlen+i+1:stride, :].values for i in range(wlen)]
    # Reshape to _add a new axis_
    s = lt[0].shape
    for i in range(wlen):
        lt[i] = lt[i].reshape(s[0], 1, s[1])
    # Concatenate
    wdata = np.concatenate(lt, axis=1)
    return wdata
```

* It's similar to our code for the univariate case
* The output is a tensor with shape `(n_windows, w_len, n_dims)`

## Sliding Window for Multivariate Data

**Let's look in deeper detail at the returned data structures**

`tr_sw` contain the actual sliding window data:

```python
tr_sw[0]
```

    array([[ 1.21931469,  0.86619169,  0.41983436, -1.05564063, -0.79621447,
            -0.70080293, -0.74549387, -1.1386061 , -1.08249848, -0.99389823,
            -0.11421637, -0.6315044 , -0.67586863, -0.36411574, -0.98910425,
             0.41889575,  0.08700467,  0.05991388, -0.69502688, -0.63793104,
            -0.11268403,  0.41983436, -1.03117521, -1.03187757],
           [-0.26962527,  0.41609996,  0.41983436,  0.6926385 ,  0.71397375,
             0.56288953,  0.29808726,  0.36365649,  0.3710279 ,  0.33249075,
             0.65388538,  0.56210134, -0.20641916,  0.32893584,  0.33156802,
             0.41687122, -0.24758681, -0.12925879, -0.69502688,  0.47652818,
             0.65613725,  0.41983436,  0.35321893,  0.35869109],
           [ 1.21924025,  0.86908928,  0.41983436, -1.05564063, -0.8157647 ,
            -0.70372248, -0.7109787 , -1.1386061 , -1.08433606, -0.98831315,
            -0.11380415, -0.64524209, -0.67586863, -0.37335643, -0.99026013,
             0.41874002, -0.00870947,  0.14931194, -0.69502688, -0.67388133,
            -0.11268403,  0.41983436, -1.04527086, -1.02276728]])

* 3 times steps per example
* 24 dimensions per time step

## Sliding Window for Multivariate Data

**Let's look in deeper detail at the returned data structures**

`tr_sw_m` contains the corresponding machine values

```python
tr_sw_m
```

    array([461, 461, 461, ..., 708, 708, 708])

* The structure is a plain numpy array

`tr_sw_r` contains the RUL values

```python
tr_sw_r
```

    array([0.58671587, 0.58487085, 0.58302583, ..., 0.00369004, 0.00184502,
           0.        ])

* Again, the structure is a plain numpy array

## 1D Convolutions in Keras

**The chosen format is ideal for _1D convolutions_ in keras**

We can define a simple 1D convolutional NN as follows:

```python
def build_cnn_regressor(wlen):
    input_shape = (wlen, len(dt_in))
    model_in = keras.Input(shape=input_shape, dtype='float32')
    model_out = layers.Conv1D(32, kernel_size=3, activation='relu')(model_in)
    model_out = layers.Flatten()(model_out)
    model_out = layers.Dense(32, activation='relu')(model_out)
    model_out = layers.Dense(1, activation='linear')(model_out)
    model = keras.Model(model_in, model_out)
    return model
```

* For simplicity, we consider a fixed architecture
* Each convolution in this case will consider 3 time steps
* We need to "flatten" the input before the fully connected layers
  - Flattening = getting rid of the temporal structure

## CNN Training

**Let's train our CNN**

```python
nn2 = build_cnn_regressor(wlen)
nn2.compile(optimizer='Adam', loss='mse')
history2 = nn2.fit(tr_sw, tr_sw_r, validation_split=0.2,
                 callbacks=cb,
                 batch_size=32, epochs=20, verbose=1)
```

    Epoch 1/20
    1126/1126 [==============================] - 1s 866us/step - loss: 0.0229 - val_loss: 0.0148
    Epoch 2/20
    1126/1126 [==============================] - 1s 785us/step - loss: 0.0164 - val_loss: 0.0129
    Epoch 3/20
    1126/1126 [==============================] - 1s 759us/step - loss: 0.0154 - val_loss: 0.0123
    Epoch 4/20
    1126/1126 [==============================] - 1s 758us/step - loss: 0.0149 - val_loss: 0.0110
    Epoch 5/20
    1126/1126 [==============================] - 1s 783us/step - loss: 0.0146 - val_loss: 0.0110
    Epoch 6/20
    1126/1126 [==============================] - 1s 768us/step - loss: 0.0142 - val_loss: 0.0118
    Epoch 7/20
    1126/1126 [==============================] - 1s 777us/step - loss: 0.0140 - val_loss: 0.0114
    Epoch 8/20
    1126/1126 [==============================] - 1s 822us/step - loss: 0.0138 - val_loss: 0.0103
    Epoch 9/20
    1126/1126 [==============================] - 1s 762us/step - loss: 0.0135 - val_loss: 0.0110
    Epoch 10/20
    1126/1126 [==============================] - 1s 766us/step - loss: 0.0135 - val_loss: 0.0115
    Epoch 11/20
    1126/1126 [==============================] - 1s 790us/step - loss: 0.0134 - val_loss: 0.0107
    Epoch 12/20
    1126/1126 [==============================] - 1s 806us/step - loss: 0.0133 - val_loss: 0.0101
    Epoch 13/20
    1126/1126 [==============================] - 1s 799us/step - loss: 0.0132 - val_loss: 0.0103
    Epoch 14/20
    1126/1126 [==============================] - 1s 786us/step - loss: 0.0132 - val_loss: 0.0099
    Epoch 15/20
    1126/1126 [==============================] - 1s 793us/step - loss: 0.0132 - val_loss: 0.0101
    Epoch 16/20
    1126/1126 [==============================] - 1s 758us/step - loss: 0.0131 - val_loss: 0.0106
    Epoch 17/20
    1126/1126 [==============================] - 1s 733us/step - loss: 0.0131 - val_loss: 0.0108
    Epoch 18/20
    1126/1126 [==============================] - 1s 734us/step - loss: 0.0130 - val_loss: 0.0108
    Epoch 19/20
    1126/1126 [==============================] - 1s 729us/step - loss: 0.0130 - val_loss: 0.0106
    Epoch 20/20
    1126/1126 [==============================] - 1s 749us/step - loss: 0.0129 - val_loss: 0.0101

## CNN Training

**Let's check the loss behavior and compare it to the MLP model**

```python
cmapss.plot_training_history(history2, figsize=figsize)
trl2, vll2 = history2.history["loss"][-1], np.min(history2.history["val_loss"])
print(f'Final loss: {trl2:.4f} (training), {vll2:.4f} (validation)')
```

* Not a lot of improvement, apparently, but some!

## Threshold Optimization

**Now we can proceed by choosing a threshold**

```python
tr_pred2 = nn2.predict(tr_sw).ravel() * trmaxrul
ts_pred2 = nn2.predict(ts_sw).ravel() * trmaxrul
tr_thr2 = cmapss.opt_threshold_and_plot(tr_sw_m, tr_pred2, th_range, cmodel, figsize=figsize)
print(f'Optimal threshold for the training set: {tr_thr2}')
```

## Evaluation

**Let's see how the CNN fares in terms of cost**

```python
tr_c2, tr_f2, tr_sl2 = cmodel.cost(tr_sw_m, tr_pred2, tr_thr2, return_margin=True)
ts_c2, ts_f2, ts_sl2 = cmodel.cost(ts_sw_m, ts_pred2, tr_thr2, return_margin=True)
print(f'Cost: {tr_c2} (training), {ts_c2} (test)')
print(f'Avg. fails: {tr_f2/len(tr_mcn)} (training), {ts_f2/len(ts_mcn)} (test)')
print(f'Avg. slack: {tr_sl2/len(tr_mcn):.2f} (training), {ts_sl2/len(ts_mcn):.2f} (test)')
```

    Cost: -17752 (training), -6622 (test)
    Avg. fails: 0.0 (training), 0.0 (test)
    Avg. slack: 18.73 (training), 16.86 (test)

Which is (more or less) on par with our MLP approach, for which we had:

```python
print(f'Cost: {tr_c} (training), {ts_c} (test)')
print(f'Avg. fails: {tr_f/len(tr_mcn)} (training), {ts_f/len(ts_mcn)} (test)')
print(f'Avg. slack: {tr_sl/len(tr_mcn):.2f} (training), {ts_sl/len(ts_mcn):.2f} (test)')
```

    Cost: -18605 (training), -6980 (test)
    Avg. fails: 0.0 (training), 0.0 (test)
    Avg. slack: 16.13 (training), 13.02 (test)

## Considerations and Take-Home Messages

**A first approach for RUL-based maintenance policies**

* Build a RUL predictor
* Trigger maintenance when the prediction is below a threshold
* In practice: a simple two-stage sequential optimization process

**Don't evaluate the performance in term of accuracy: it's a trap!**

* We don't care about accuracy when the RUL is high
* We only care about when we stop
* We need a _cost model_

**Sequence input may help**

* But don't automatically jump for that!
* First _think_: would considering multiple step provide more useful information?


Métodos Numéricos

Capítulo 5: Diferenciación e integración numérica

2021/02

MEDELLÍN - COLOMBIA

 Text provided under a Creative Commons Attribution license, CC-BY. All code is made available under the FSF-approved MIT license.(c) Carlos Alberto Alvarez Henao

***

***Docente:*** Carlos Alberto Álvarez Henao, I.C. D.Sc.

***e-mail:*** carlosalvarezh@gmail.com

***skype:*** carlos.alberto.alvarez.henao

***Linkedin:*** https://www.linkedin.com/in/carlosalvarez5/

***github:*** https://github.com/carlosalvarezh/Metodos_Numericos

***Herramienta:*** [Jupyter](http://jupyter.org/)

***Kernel:*** Python 3.8

***

Tabla de Contenidos

1  Diferenciación Numérica

1.1  Introducción

1.2  Series de Taylor

1.3  Esquemas de diferencias finitas para la primera derivada

1.3.1  Esquema de primer orden hacia adelante (forward)

1.3.2  Esquema de primer orden hacia atrás (backward)

1.3.3  Esquema de segundo orden (central)

1.3.4  Resumen esquemas diferencias finitas para la primera derivada

1.4  Esquemas de diferencias finitas para la segunda derivada

1.5  Implementación computacional de algunos esquemas de diferencias finitas

2  Integración Numérica

2.1  Introducción

2.2  Fórmulas de integración de Newton - Cotes

2.3  Regla trapezoidal

2.3.1  Regla trapezoidal de aplicación simple

2.3.2  Regla trapezoidal de aplicación múltiple

2.3.3  Implementación computacional

2.3.4  Error en la aplicación de la regla trapezoidal

2.4  Reglas de Simpson

2.4.1  Regla de Simpson1/3 de aplicación simple

2.4.2  Error en la regla de Simpson 1/3 de aplicación simple

2.4.3  Regla de simpson1/3 de aplicación múltiple

2.4.4  Implementación computacional regla de Simpson1/3 de aplicación múltiple

2.4.5  Regla de Simpson 3/8 de aplicación simple

2.4.6  Regla de Simpson3/8 de aplicación múltiple

2.4.7  Implementación computacional de la regla de Simpson3/8 de aplicación múltiple

2.5  Cuadratura de Gauss

2.5.1  Introducción

2.5.2  Determinación de los coeficientes

2.5.3  Cambios de los límites de integración

2.5.4  Fórmulas de punto superior

2.5.5  Ejemplo Cuadratura de Gauss

## Diferenciación Numérica

### Introducción

La [diferenciación numérica](https://en.wikipedia.org/wiki/Numerical_differentiation) se emplea para determinar (estimar) el valor de la derivada de una función en un punto específico. No confundir con la derivada de una función, pues lo que se obtendrá es un valor puntual y no una función. En este capítulo nos centraremos únicamente en ecuiaciones unidimensionales.

[Volver a la Tabla de Contenido](#TOC)

### Series de Taylor

De la [serie de Taylor](https://en.wikipedia.org/wiki/Taylor_series)

\begin{equation*}
f(x_{i \pm 1}) = f(x_i) \pm f'(x_i)h + \frac{f''(x_i)h^2}{2!} \pm \frac{f'''(x_i)h^3}{3!} + \ldots
\label{eq:Ec5_1} \tag{5.1}
\end{equation*}

con $h=\Delta x = x_{i+1}-x_i$ siendo el tamaño de paso.

Dada que la serie contiene infinitos términos, partir de la ecuación ($5.1$) se pueden obtener infinitos esquemas numéricos para determinar cada una de las infinitas derivadas de dicho polinomio. En este curso usaremos la técnica de [Diferencias Finitas](https://en.wikipedia.org/wiki/Finite_difference) para desarrollarlas.

[Volver a la Tabla de Contenido](#TOC)

### Esquemas de diferencias finitas para la primera derivada

#### Esquema de primer orden hacia adelante (forward)

De la ecuación [(5.1)](#Ec5_1) tomando los valores positivos, que involucran únicamente términos hacia adelante, se trunca la serie hasta la primera derivada y se realiza un despeje algebraico para llegar a:

\begin{equation*}
f'(x_i) = \frac{f(x_{i+1})-f(x_i)}{h} + \mathcal{O}(h)
\label{eq:Ec5_2} \tag{5.2}
\end{equation*}

se puede observar que el término $\mathcal{O}(h)$ indica que el error es de orden lineal, es decir, si se reduce el tamaño de paso, $h$, a la mitad, el error se reducirá a la mitad. Si se reduc el tamaño de paso a una cuarta parte, el error se reducirá, linealmente, una cuarta parte.

[Volver a la Tabla de Contenido](#TOC)

#### Esquema de primer orden hacia atrás (backward)

De la ecuación [(5.1)](#Ec5_1) tomando los valores negativos, que involucran únicamente términos hacia atrás (backward), se trunca la serie hasta la primera derivada y se realiza un despeje algebraico para llegar a:

\begin{equation*}
f'(x_i) = \frac{f(x_{i})-f(x_{i-1})}{h} + \mathcal{O}(h)
\label{eq:Ec5_3} \tag{5.3}
\end{equation*}

se observa que se llega a una expresión similar a la de la ecuación [(5.2)](#Ec5_2), pero de esta vez, se tiene en cuenta es el valor anterior al punto $x_i$. También se observa que el error es de orden lineal, por lo que se mantiene un esquema de primer orden.

[Volver a la Tabla de Contenido](#TOC)

#### Esquema de segundo orden (central)

Una forma de aumentar el orden de estos esquemas, es realizar el truncamiento de la *serie de Taylor* hasta la segunda derivada, hacia adelante y hacia atras, y realizar su resta aritmética.

\begin{equation*}
\begin{split}
f(x_{i+1}) & = f(x_i) + f'(x_i)h + \frac{f''(x_i)h^2}{2!} \\
- \\
f(x_{i-1}) & = f(x_i) - f'(x_i)h + \frac{f''(x_i)h^2}{2!} \\
\hline \\
f(x_{i+1}) - f(x_{i-1}) & = 2 f'(x_i)h
\end{split}
\label{eq:Ec5_4} \tag{5.4}
\end{equation*}

de la anterior ecuación, despejando el término que corresponde a la primera derivada queda:

\begin{equation*}
\begin{split}
f'(x_i) = \frac{f(x_{i+1}) - f(x_{i-1})}{2h} + \mathcal{O}(h^2)
\end{split}
\label{eq:Ec5_5} \tag{5.5}
\end{equation*}

se llega al esquema de diferencias finitas central para la primera derivada, que es de orden dos, es decir, si se disminuye el tamaño de paso, $h$, a la mitad, el error se disminuye una cuarta partes. En principio, esta es una mejor aproximación que los dos esquemas anteriores. La selección del esquema dependerá de la disponibilidad de puntos y del fenómeno físico a tratar.

[Volver a la Tabla de Contenido](#TOC)

#### Resumen esquemas diferencias finitas para la primera derivada

Como la serie de Taylor es infinita, se podrían determinar infinitos esquemas de diferentes ordenes para la primera derivada. En la siguiente tabla se presentan algunos esquemas de diferencias finitas para la primera derivada de diferentes órdenes. Se deja al estudiante la consulta de otros esquemas.

|***Esquema***|***Función***|***Error***|
|:-----:|:-----:|:---:|
|***Forward***|$$f´(x_0)=\frac{f(x_0+h)-f(x_0)}{h}$$|$$\mathcal{O}(h)$$|
|       |$$f´(x_0)=\frac{-3f(x_0)+4f(x_0+h)-f(x_0+2h)}{2h}$$|$$\mathcal{O}(h^2)$$|
|***Central***|$$f´(x_0)=\frac{f(x_0+h)-f(x_0-h)}{2h}$$|$$\mathcal{O}(h^2)$$|
|       |$$f´(x_0)=\frac{f(x_0-2h)-8f(x_0-h)+8f(x_0+h)-f(x_0+2h)}{12h}$$|$$\mathcal{O}(h^4)$$|
|***Backward***|$$f´(x_0)=\frac{f(x_0)-f(x_0-h)}{h}$$|$$\mathcal{O}(h)$$|
|        |$$f´(x_0)=\frac{f(x_0-2h)-4f(x_0-h)+3f(x_0)}{2h}$$|$$\mathcal{O}(h^2)$$|

[Volver a la Tabla de Contenido](#TOC)

### Esquemas de diferencias finitas para la segunda derivada

Siguiendo con la misma forma de abordar el problema para la primera derivada, si se amplian los términos en la serie de Taylor hasta la tercera derivada tanto hacia adelante como hacia atrás, y se suman, se llega a:

\begin{equation*}
\begin{split}
f(x_{i+1}) & = f(x_i) + f'(x_i)h + \frac{f''(x_i)h^2}{2!} + \frac{f'''(x_i)h^3}{3!}\\
+ \\
f(x_{i-1}) & = f(x_i) - f'(x_i)h + \frac{f''(x_i)h^2}{2!} - \frac{f'''(x_i)h^3}{3!}\\
\hline \\
f(x_{i+1}) + f(x_{i-1}) & = 2 f(x_i) + 2f''(x_i)\frac{h^2}{2!} + \mathcal{O}(h^3)
\end{split}
\label{eq:Ec5_6} \tag{5.6}
\end{equation*}

Despejando para el término de la segunda derivada, se llega a:

\begin{equation*}
\begin{split}
f''(x_i) = \frac{f(x_{i+1}) - 2f(x_i) + f(x_{i-1})}{h^2} + \mathcal{O}(h^3)
\end{split}
\label{eq:Ec5_7} \tag{5.7}
\end{equation*}

Que corresponde a un esquema de diferencias finitas de segundo orden para la segunda derivada. A este esquema también se le llama "*molécula de tres puntos*"

Igual que para la primera derivada, se pueden determinar infinitos esquemas de diferentes órdenes para la segunda derivada, y derivadas superiores. A continuación se muestra un cuadro resumen de algunos esquemas de diferencias finitas para la segunda derivada. Se deja al estudiante la revisión de esquemas de mayor orden para la segunda derivada y derivadas superiores.

|***Esquema***|***Función***|***Error***|
|:-----:|:-----:|:---:|
|***Forward***|$$f''(x_0)=\frac{f(x_0)-2f(x_0+h)+f(x_0+2h)}{h^2}$$|$$\mathcal{O}(h)$$|
|       |$$f''(x_0)=\frac{2f(x_0)-5f(x_0+h)+4f(x_0+2h)-f(x_0+3h)}{h^2}$$|$$\mathcal{O}(h^2)$$|
|***Central***|$$f''(x_0)=\frac{f(x_0-h)-2f(x_0)+f(x_0+h)}{h^2}$$|$$\mathcal{O}(h^2)$$|
|       |$$f''(x_0)=\frac{-f(x_0-2h)+16f(x_0-h)-30f(x_0)+16f(x_0+h)-f(x_0+2h)}{12h^2}$$|$$\mathcal{O}(h^4)$$|
|***Backward***|$$f''(x_0)=\frac{f(x_0-2h)-2f(x_0-h)+f(x_0)}{h}$$|$$\mathcal{O}(h^2)$$|
|        |$$f''(x_0)=\frac{-f(x_0-3h)+4f(x_0-2h)-5f(x_0-h)+2f(x_0)}{h^2}$$|$$\mathcal{O}(h^2)$$|

[Volver a la Tabla de Contenido](#TOC)

### Implementación computacional de algunos esquemas de diferencias finitas

A manera de ejemplo, se implementarán algunos esquemas simples de diferencias finitas para la primera derivada. Se deja como actividad a los estudiantes la implementación de otros esquemas para las diferentes derivadas.

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

sym.init_printing()
```

```python
#Esquemas de diferencias finitas para la primera derivada

def df1df(x0, h):
    # Esquema de diferencias finitas para la primera derivada hacia adelante (forward)
    return (f(x0 + h) - f(x0)) / h

def df1db(x0, h):
    # Esquema de diferencias finitas para la primera derivada hacia atrás (backward)
    return (f(x0) - f(x0 - h) ) / h

def df1dc(x0,h):
    # Esquema de diferencias finitas para la primera derivada central (central)
    return (f(x0 + h) - f(x0 - h) ) / (2 * h)

```

```python
#funcion a determinar el valor de la derivada
def f(x):
    return 2*x**3 - 3*x**2 + 5*x+0.8
```

```python
#cálculo y evaluación de la primera derivada empleando cálculo simbólico

def df1de(x0):

    x = sym.Symbol('x')
    df = sym.diff(f(x), x)
    #print(df)
    df1 = df.evalf(subs={x:x0})
    return df1
```

```python
h  = 0.1
x0 = 0.8

print("1st derivative \t Value \t\t Error(%)")
print('---------------------------------------')

pde = df1de(x0)

pdf = df1df(x0, h)
epdf = abs((pde - pdf) / pde * 100)
print("forward \t {0:6.4f} \t {1:6.2f}".format(pdf,epdf))

pdb = df1db(x0, h)
epdb = abs((pde - pdb) / pde * 100)
print("backward \t {0:6.4f} \t {1:6.2f}".format(pdb,epdb))

pdc = df1dc(x0,h)
epdc = abs((pde - pdc) / pde * 100)
print("central \t {0:6.4f} \t {1:6.2f}".format(pdc, epdc))

print("exacta \t\t {0:6.4f} \t {1}".format(pde, '   -'))
```

[Volver a la Tabla de Contenido](#TOC)

## Integración Numérica

### Introducción

La [integración numérica](https://en.wikipedia.org/wiki/Numerical_integration) aborda una amplia gama de algoritmos para determinar el valor numérico (aproximado) de una integral definida. En este curso nos centraremos principalmente en los métodos de cuadratura, tanto de interpolación como [gaussiana](https://en.wikipedia.org/wiki/Gaussian_quadrature), como dos ejemplos de dichos algoritmos.

El problema a tratar en este capítulo es la solución aproximada de la función

\begin{equation*}
\begin{split}
I = \int_a^b f(x) dx
\end{split}
\label{eq:Ec5_8} \tag{5.8}
\end{equation*}

[Volver a la Tabla de Contenido](#TOC)

### Fórmulas de integración de *Newton - Cotes*

La idea básica en la integración numérica es cambiar una función difícil de integrar, $f(x)$, dada por la ecuación [(5.8)](#Ec5_8), por una función más simple, $p_n(x)$,

\begin{equation*}
\begin{split}
\widetilde{I} \approx \int_{a=x_0}^{b=x_n} p_{n}(x) dx
\end{split}
\label{eq:Ec5_9} \tag{5.9}
\end{equation*}

Cabe resaltar que en integración numérica no se conocerá la función a integrar, solo se dispondrá de una serie de $n+1$ puntos $(x_i, y_i), i = 0, 1, 2, \ldots, n$, y a partir de ellos se construye un polinomio interpolante de grado $n$, $p_n$, entre los valores de los límites de integración $a = x_0$ y $b=x_n$. $p_n(x)$ es un polinomio de interpolación de la forma

\begin{equation*}
\begin{split}
p_n(x)=a_0+a_1x+a_2x^2+\ldots+a_{n-1}x^{n-1}+a_nx^n
\end{split}
\label{eq:Ec5_10} \tag{5.10}
\end{equation*}

Las fórmulas de integración de [*Newton - Cotes*](https://en.wikipedia.org/wiki/Newton%E2%80%93Cotes_formulas), también llamadas de [cuadratura](https://en.wikipedia.org/wiki/Quadrature_(mathematics)), son un grupo de fórmulas de integración numérica de tipo interpolación, evaluando la función en puntos equidistantes, para determinar un valor aproximado de la integral. Si no se tienen puntos espaciados, otros métodos deben ser usados, como por ejemplo cuadratura gaussiana, que se verá al final del capítulo.

La forma general de las fórmulas de Newton - Cotes está dada por la función:

\begin{equation*}
\begin{split}
p_n(x)=\sum \limits_{i=0}^n f(x_i)L_{in}(x)
\end{split}
\label{eq:Ec5_11} \tag{5.11}
\end{equation*}

donde

\begin{equation*}
\begin{split}
L_{in}(x)=\frac{(x-x_0)\ldots(x-x_{i-1})(x-x_{i+1})\ldots(x-x_n)}{(x_i-x_0)\ldots(x_i-x_{i-1})(x_i-x_{i+1})\ldots(x_i-x_n)}
\end{split}
\label{eq:Ec5_12} \tag{5.12}
\end{equation*}

es el polinomio de Lagrange, de donde se deduce que:

\begin{equation*}
\begin{split}
\int_a^b p(x)dx=(b-a)\sum \limits_{i=0}^n f(x_i) \frac{1}{(b-a)} \int_a^b L_{in}(x)dx
\end{split}
\label{eq:Ec5_13} \tag{5.13}
\end{equation*}

entonces,

\begin{equation*}
\begin{split}
\int_a^b f(x)dx \approx \int_a^b p(x)dx=(b-a)\sum \limits_{i=0}^n w_if(x_i)
\end{split}
\label{eq:Ec5_14} \tag{5.14}
\end{equation*}

donde los pesos, $w_i$ de la función son representados por

\begin{equation*}
\begin{split}
w_i=\frac{1}{(b-a)} \int_a^b L_{in}(x)dx
\end{split}
\label{eq:Ec5_15} \tag{5.15}
\end{equation*}

A partir de esta idea se obtienen los diferentes esquemas de integración numérica de *Newton - Cotes*

[Volver a la Tabla de Contenido](#TOC)

### Regla trapezoidal

#### Regla trapezoidal de aplicación simple

La [regla trapezoidal](https://en.wikipedia.org/wiki/Trapezoidal_rule) emplea una aproximación de la función mediante una línea recta

 Fuente: wikipedia.com

y corresponde al caso en el que el polinomio en la ecuación [(5.11)](#Ec5_11) es de primer orden

\begin{equation*}
\begin{split}
I=\int_{a}^{b}f(x)dx \approx \int_a^b \left[ f(a) + \frac{f(b)-f(a)}{b-a}(x-a)\right]dx
= (b-a)\frac{f(a)+f(b)}{2}
\end{split}
\label{eq:Ec5_16} \tag{5.16}
\end{equation*}

Geométricamente, es equivalente a aproximar el área del trapezoide bajo la línea recta que conecta $f(a)$ y $f(b)$. La integral se representa como:

$$I ≈ \text{ancho} \times \text{altura promedio}$$

El error en la regla trapezoidal simple se puede determinar como:

\begin{equation*}
\begin{split}
E_t=-\frac{1}{12}f''(\xi)(b-a)^3
\end{split}
\label{eq:Ec5_17} \tag{5.17}
\end{equation*}

[Volver a la Tabla de Contenido](#TOC)

#### Regla trapezoidal de aplicación múltiple

Una manera de mejorar la exactitud de la regla trapezoidal es dividir el intervalo de integración de $a$ a $b$ en un número $n$ de segmentos y aplicar el método a cada uno de ellos. Las ecuaciones resultantes son llamadas fórmulas de integración de múltiple aplicación o compuestas.

 Fuente: wikipedia.com

Hay $n+1$ puntos base igualmente espaciados $(x_0, x_1, x_2, \ldots, x_n)$. En consecuencia hay $n$ segmentos de igual anchura: $h = (b–a) / n$. Si $a$ y $b$ son designados como $x_0$ y $x_n$ respectivamente, la integral total se representará como:

\begin{equation*}
\begin{split}
I=\int_{x_0}^{x_1}f(x)dx+\int_{x_1}^{x_2}f(x)dx+\int_{x_2}^{x_3}f(x)dx+\ldots+\int_{x_{n-2}}^{x_{n-1}}f(x)dx+\int_{x_{n-1}}^{x_n}f(x)dx
\end{split}
\label{eq:Ec5_18} \tag{5.18}
\end{equation*}

Al sustituir la regla trapezoidal simple en cada integrando, se tiene

\begin{equation*}
\begin{split}
I\approx \left(f(x_0)+f(x_1)\right)\frac{h}{2}+\left(f(x_1)+f(x_2)\right)\frac{h}{2}+\left(f(x_2)+f(x_3)\right)\frac{h}{2}+\ldots\left(f(x_{n-2})+f(x_{n-1})\right)\frac{h}{2}+\left(f(x_{n-1})+f(x_n)\right)\frac{h}{2}
\end{split}
\label{eq:Ec5_19} \tag{5.19}
\end{equation*}

ahora, agrupando términos

\begin{equation*}
\begin{split}
I\approx \frac{h}{2}\left[ f(x_0) + 2\sum_{i=1}^{n-1}f(x_i)+f(x_n) \right]
\end{split}
\label{eq:Ec5_20} \tag{5.20}
\end{equation*}

donde $h=(b-a)/n$

[Volver a la Tabla de Contenido](#TOC)

#### Implementación computacional

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

```python
def trapezoidal(x):
    n = len(x)
    h = (x[-1] - x[0]) / n

    suma = 0
    for i in range(1, n-1):
        suma += funcion(x[i])

    return h * (funcion(x[0]) + 2 * suma + funcion(x[-1])) / 2
```

```python
def funcion(x):
    return 4 / (1 + x**2)
```

```python
a = 0
b = 1
n = 2
x = np.linspace(a, b, n+1)
I = trapezoidal(x)
I
```

[Volver a la Tabla de Contenido](#TOC)

#### Error en la aplicación de la regla trapezoidal

Recordando que estos esquemas provienen de la serie truncada de Taylor, el error se puede obtener determinando el primer término truncado en el esquema, que para la regla trapezoidal de aplicación simple corresponde a:

\begin{equation*}
\begin{split}
E_t=-\frac{1}{12}f''(\xi)(b-a)^3
\end{split}
\label{eq:Ec5_21} \tag{5.21}
\end{equation*}

donde $f''(\xi)$ es la segunda derivada en el punto $\xi$ en el intervalo $[a,b]$, y $\xi$ es un valor que maximiza la evaluación de esta segunda derivada.

Generalizando este concepto a la aplicación múltiple de la regla trapezoidal, se pueden sumar cada uno de los errores en cada segmento para dar:

\begin{equation*}
\begin{split}
E_t=-\frac{(b-a)^3}{12n^3}\sum\limits_{i=1}^n f''(\xi_i)
\end{split}
\label{eq:Ec5_22} \tag{5.22}
\end{equation*}

el anterior resultado se puede simplificar estimando la media, o valor promedio, de la segunda derivada para todo el intervalo

\begin{equation*}
\begin{split}
\bar{f''} \approx \frac{\sum \limits_{i=1}^n f''(\xi_i)}{n}
\end{split}
\label{eq:Ec5_23} \tag{5.23}
\end{equation*}

de esta ecuación se tiene que $\sum f''(\xi_i)\approx nf''$, y reemplazando en la ecuación [(5.23)](#Ec5_23)

\begin{equation*}
\begin{split}
E_t \approx \frac{(b-a)^3}{12n^2}\bar{f''}
\end{split}
\label{eq:Ec5_24} \tag{5.24}
\end{equation*}

De este resultado se observa que si se duplica el número de segmentos, el error de truncamiento se disminuirá a una cuarta parte.

[Volver a la Tabla de Contenido](#TOC)

### Reglas de Simpson

Las [reglas de Simpson](https://en.wikipedia.org/wiki/Simpson%27s_rule) son esquemas de integración numérica en honor al matemático [*Thomas Simpson*](https://en.wikipedia.org/wiki/Thomas_Simpson), utilizado para obtener la aproximación de la integral empleando interpolación polinomial sustituyendo a $f(x)$.

[Volver a la Tabla de Contenido](#TOC)

#### Regla de Simpson1/3 de aplicación simple

La primera regla corresponde a una interpolación polinomial de segundo orden sustituida en la ecuación [(5.8)](#Ec5_8)

 Fuente: wikipedia.com

\begin{equation*}
\begin{split}
I=\int_a^b f(x)dx \approx \int_a^b p_2(x)dx
\end{split}
\label{eq:Ec5_25} \tag{5.25}
\end{equation*}

del esquema de interpolación de Lagrange para un polinomio de segundo grado, visto en el capitulo anterior, y remplazando en la integral arriba, se llega a

\begin{equation*}
\begin{split}
I\approx\int_{x0}^{x2} \left[\frac{(x-x_1)(x-x_2)}{(x_0-x_1)(x_0-x_2)}f(x_0)+\frac{(x-x_0)(x-x_2)}{(x_1-x_0)(x_1-x_2)}f(x_1)+\frac{(x-x_0)(x-x_1)}{(x_2-x_0)(x_2-x_1)}f(x_2)\right]dx
\end{split}
\label{eq:Ec5_26} \tag{5.26}
\end{equation*}

realizando la integración de forma analítica y un manejo algebraico, resulta

\begin{equation*}
\begin{split}
I\approx\frac{h}{3} \left[ f(x_0)+4f(x_1)+f(x_2)\right]
\end{split}
\label{eq:Ec5_27} \tag{5.27}
\end{equation*}

donde $h=(b-a)/2$ y los $x_{i+1} = x_i + h$

A continuación, vamos a comparar graficamente las funciones "exacta" (con muchos puntos) y una aproximada empleando alguna técnica de interpolación para $n=3$ puntos (Polinomio interpolante de orden $2$).

```python
from scipy.interpolate import barycentric_interpolate

# usaremos uno de los tantos métodos de interpolación dispobibles en las bibliotecas de Python

n = 3                                  # puntos a interpolar para un polinomio de grado 2

xp = np.linspace(a,b,n)                # generación de n puntos igualmente espaciados para la interpolación
fp = funcion(xp)                       # evaluación de la función en los n puntos generados
x = np.linspace(a, b, 100)             # generación de 100 puntos igualmente espaciados
y = barycentric_interpolate(xp, fp, x) # interpolación numérica empleando el método del Baricentro

fig = plt.figure(figsize=(9, 6), dpi= 80, facecolor='w', edgecolor='k')
ax = fig.add_subplot(111)

l, = plt.plot(x, y)
plt.plot(x, funcion(x), '-', c='red')
plt.plot(xp, fp, 'o', c=l.get_color())
plt.annotate('Función "Real"', xy=(.63, 1.5), xytext=(0.8, 1.25),arrowprops=dict(facecolor='black', shrink=0.05),)
plt.annotate('Función interpolada', xy=(.72, 1.75), xytext=(0.4, 2),arrowprops=dict(facecolor='black', shrink=0.05),)
plt.grid(True)                          # muestra la malla de fondo
plt.show()                              # muestra la gráfica
```

Se observa que hay una gran diferencia entre las áreas que se estarían abarcando en la función llamada "*real*" (que se emplearon $100$ puntos para su generación) y la función *interpolada* (con únicamente $3$ puntos para su generación) que será la empleada en la integración numérica (aproximada) mediante la regla de *Simpson $1/3$*.

Conscientes de esto, procederemos entonces a realizar el cálculo del área bajo la curva del $p_3(x)$ empleando el método de *Simpson $1/3$*

Creemos un programa en *Python* para que nos sirva para cualquier función $f(x)$ que queramos integrar en cualquier intervalo $[a,b]$ empleando la regla de integración de *Simpson $1/3$*:

```python
# se ingresan los valores del intervalo [a,b]
a = float(input('Ingrese el valor del límite inferior: '))
b = float(input('Ingrese el valor del límite superior: '))
```

```python
# cuerpo del programa por la regla de Simpson 1/3
h = (b-a)/2   # cálculo del valor de h

x0 = a        # valor del primer punto para la fórmula de S1/3
x1 = x0 + h   # Valor del punto intermedio en la fórmula de S1/3
x2 = b        # valor del tercer punto para la fórmula de S1/3

fx0 = funcion(x0)   # evaluación de la función en el punto x0
fx1 = funcion(x1)   # evaluación de la función en el punto x1
fx2 = funcion(x2)   # evaluación de la función en el punto x2

int_S13 = h / 3 * (fx0 + 4*fx1 + fx2)

#erel = np.abs(exacta - int_S13) / exacta * 100

print('el valor aproximado de la integral por la regla de Simpson1/3 es: ', int_S13, '\n')
#print('el error relativo entre el valor real y el calculado es: ', erel,'%')
```

[Volver a la Tabla de Contenido](#TOC)

#### Error en la regla de Simpson 1/3 de aplicación simple

El problema de calcular el error de esta forma es que realmente no conocemos el valor exacto. Para poder calcular el error al usar la regla de *Simpson 1/3*:

\begin{equation*}
\begin{split}
-\frac{h^5}{90}f^{(4)}(\xi)
\end{split}
\label{eq:Ec5_28} \tag{5.28}
\end{equation*}

será necesario derivar cuatro veces la función original: $f(x)=e^{x^2}$. Para esto, vamos a usar nuevamente el cálculo simbólico (siempre deben verificar que la respuesta obtenida es la correcta!!!):

```python
from sympy import *
x = symbols('x')
```

Derivamos cuatro veces la función $f(x)$ con respecto a $x$:

```python
deriv4 = diff(4 / (1 + x**2),x,4)
deriv4
```

y evaluamos esta función de la cuarta derivada en un punto $0 \leq \xi \leq 1$. Como la función $f{^{(4)}}(x)$ es creciente en el intervalo $[0,1]$ (compruébelo gráficamente y/o por las técnicas vistas en cálculo diferencial), entonces, el valor que hace máxima la cuarta derivada en el intervalo dado es:

```python
x0 = 1.0
evald4 = deriv4.evalf(subs={x: x0})
print('El valor de la cuarta derivada de f en x0={0:6.2f} es {1:6.4f}: '.format(x0, evald4))
```

Calculamos el error en la regla de *Simpson$1/3$*

```python
errorS13 = abs(h**5*evald4/90)
print('El error al usar la regla de Simpson 1/3  es: {0:6.6f}'.format(errorS13))
```

Entonces, podemos expresar el valor de la integral de la función $f(x)=e^{x^2}$ en el intervalo $[0,1]$ usando la *Regla de Simpson $1/3$* como:

$$\color{blue}{\int_0^1 \frac{4}{1 + x^2}dx} = \color{green}{3,133333} \color{red}{+ 0.004167}$$

Si lo fuéramos a hacer "a mano" $\ldots$ aplicando la fórmula directamente, con los siguientes datos:

$h = \frac{(1.0 - 0.0)}{2.0} = 0.5$

$x_0 = 0.0$

$x_1 = 0.5$

$x_2 = 1.0$

$f(x) = \frac{4}{1 + x^2}$

sustituyendo estos valores en la fórmula dada:

$\int_0^1\frac{4}{1 + x^2}dx \approx \frac{0.5}{3} \left[f(0)+4f(0.5)+f(1)\right]$

$\int_0^1\frac{4}{1 + x^2}dx \approx \frac{0.5}{3} \left[ \frac{4}{1 + 0^2} + 4\frac{4}{1 + 0.5^2} + \frac{4}{1 + 1^2} \right] \approx 3.133333$

[Volver a la Tabla de Contenido](#TOC)

#### Regla de simpson1/3 de aplicación múltiple

Al igual que en la regla Trapezoidal, las reglas de Simpson también cuentan con un esquema de aplicación múltiple (llamada también compuesta). Supongamos que se divide el intervalo $[a,b]$ se divide en $n$ sub intervalos, con $n$ par, quedando la integral

\begin{equation*}
\begin{split}
I=\int_{x_0}^{x_2}f(x)dx+\int_{x_2}^{x_4}f(x)dx+\ldots+\int_{x_{n-2}}^{x_n}f(x)dx
\end{split}
\label{eq:Ec5_29} \tag{5.29}
\end{equation*}

y sustituyendo en cada una de ellas la regla de Simpson1/3, se llega a

\begin{equation*}
\begin{split}
I \approx 2h\frac{f(x_0)+4f(x_1)+f(x_2)}{6}+2h\frac{f(x_2)+4f(x_3)+f(x_4)}{6}+\ldots+2h\frac{f(x_{n-2})+4f(x_{n-1})+f(x_n)}{6}
\end{split}
\label{eq:Ec5_30} \tag{5.30}
\end{equation*}

entonces la regla de Simpson compuesta (o de aplicación múltiple) se escribe como:

\begin{equation*}
\begin{split}
I=\int_a^bf(x)dx\approx \frac{h}{3}\left[f(x_0) + 2 \sum \limits_{j=1}^{n/2-1} f(x_{2j}) + 4 \sum \limits_{j=1}^{n/2} f(x_{2j-1})+f(x_n)\right]
\end{split}
\label{eq:Ec5_31} \tag{5.31}
\end{equation*}

donde $x_j=a+jh$ para $j=0,1,2, \ldots, n-1, n$ con $h=(b-a)/n$, $x_0=a$ y $x_n=b$.

[Volver a la Tabla de Contenido](#TOC)

#### Implementación computacional regla de Simpson1/3 de aplicación múltiple

```python

```

[Volver a la Tabla de Contenido](#TOC)

#### Regla de Simpson 3/8 de aplicación simple

Resulta cuando se sustituye la función $f(x)$ por una interpolación de tercer orden:

\begin{equation*}
\begin{split}
I=\int_{a}^{b}f(x)dx = \frac{3h}{8}\left[ f(x_0)+3f(x_1)+3f(x_2)+f(x_3) \right]
\end{split}
\label{eq:Ec5_32} \tag{5.32}
\end{equation*}

Realizando un procedimiento similar al usado para la regla de *Simpson $1/3$*, pero esta vez empleando $n=4$ puntos:

```python
# usaremos uno de los tantos métodos de interpolación dispobibles en las bibliotecas de Python

n = 4                                  # puntos a interpolar para un polinomio de grado 2

xp = np.linspace(0,1,n)                # generación de n puntos igualmente espaciados para la interpolación
fp = funcion(xp)                       # evaluación de la función en los n puntos generados
x = np.linspace(0, 1, 100)             # generación de 100 puntos igualmente espaciados
y = barycentric_interpolate(xp, fp, x) # interpolación numérica empleando el método del Baricentro

fig = plt.figure(figsize=(9, 6), dpi= 80, facecolor='w', edgecolor='k')
ax = fig.add_subplot(111)

l, = plt.plot(x, y)
plt.plot(x, funcion(x), '-', c='red')
plt.plot(xp, fp, 'o', c=l.get_color())
plt.annotate('"Real"', xy=(.63, 1.5), xytext=(0.8, 1.25),arrowprops=dict(facecolor='black', shrink=0.05),)
plt.annotate('Interpolación', xy=(.72, 1.75), xytext=(0.4, 2),arrowprops=dict(facecolor='black', shrink=0.05),)
plt.grid(True)                          # muestra la malla de fondo
plt.show()                              # muestra la gráfica
```

```python
# cuerpo del programa por la regla de Simpson 3/8
h = (b - a) / 3   # cálculo del valor de h

int_S38 = 3 * h / 8 * (funcion(a) + 3*funcion(a + h) + 3*funcion(a + 2*h) + funcion(a + 3*h) + 3 * funcion(a+4*h)/8)

erel = np.abs(np.pi - int_S38) / np.pi * 100

print('el valor aproximado de la integral utilizando la regla de Simpson 3/8 es: ', int_S38, '\n')
print('el error relativo entre el valor real y el calculado es: ', erel,'%')
```

Para poder calcular el error al usar la regla de *Simpson 3/8*:

$$\color{red}{-\frac{3h^5}{80}f^{(4)}(\xi)}$$

será necesario derivar cuatro veces la función original. Para esto, vamos a usar nuevamente el cálculo simbólico (siempre deben verificar que la respuesta obtenida es la correcta!!!):

```python
errorS38 = 3*h**5*evald4/80
print('El error al usar la regla de Simpson 3/8 es: ',errorS38)
```

Entonces, podemos expresar el valor de la integral de la función $f(x)=e^{x^2}$ en el intervalo $[0,1]$ usando la *Regla de Simpson $3/8$* como:

$$\color{blue}{\int_0^1\frac{4}{1 + x^2}dx} = \color{green}{3.138462} \color{red}{- 0.001852}$$

Aplicando la fórmula directamente, con los siguientes datos:

$h = \frac{(1.0 - 0.0)}{3.0} = 0.33$

$x_0 = 0.0$, $x_1 = 0.33$, $x_2 = 0.66$, $x_3 = 1.00$

$f(x) = \frac{4}{1 + x^2}$

sustituyendo estos valores en la fórmula dada:

$\int_0^1\frac{4}{1 + x^2}dx \approx \frac{3\times0.3333}{8} \left[ \frac{4}{1 + 0^2} + 3\frac{4}{1 + 0.3333^2} +3\frac{4}{1 + 0.6666^2} + \frac{4}{1 + 1^2} \right] \approx 3.138462$

Esta sería la respuesta si solo nos conformamos con lo que podemos hacer usando word...

[Volver a la Tabla de Contenido](#TOC)

#### Regla de Simpson3/8 de aplicación múltiple

Dividiendo el intervalo $[a,b]$ en $n$ sub intervalos de longitud $h=(b-a)/n$, con $n$ múltiplo de 3, quedando la integral

\begin{equation*}
\begin{split}
I=\int_{x_0}^{x_3}f(x)dx+\int_{x_3}^{x_6}f(x)dx+\ldots+\int_{x_{n-3}}^{x_n}f(x)dx
\end{split}
\label{eq:Ec5_33} \tag{5.33}
\end{equation*}

sustituyendo en cada una de ellas la regla de Simpson3/8, se llega a

\begin{equation*}
\begin{split}
I=\int_a^bf(x)dx\approx \frac{3h}{8}\left[f(x_0) + 3 \sum \limits_{i=0}^{n/3-1} f(x_{3i+1}) + 3 \sum \limits_{i=0}^{n/3-1}f(x_{3i+2})+2 \sum \limits_{i=0}^{n/3-2} f(x_{3i+3})+f(x_n)\right]
\end{split}
\label{eq:Ec5_34} \tag{5.34}
\end{equation*}

donde en cada sumatoria se deben tomar los valores de $i$ cumpliendo que $i=i+3$.

[Volver a la Tabla de Contenido](#TOC)

#### Implementación computacional de la regla de Simpson3/8 de aplicación múltiple

```python
#
```

[Volver a la Tabla de Contenido](#TOC)

### Cuadratura de Gauss

#### Introducción

Retomando la idea inicial de los esquemas de [cuadratura](#Quadrature), el valor de la integral definida se estima de la siguiente manera:

\begin{equation*}
\begin{split}
I=\int_a^b f(x)dx \approx \sum \limits_{i=0}^n c_if(x_i)
\end{split}
\label{eq:Ec5_35} \tag{5.35}
\end{equation*}

Hasta ahora hemos visto los métodos de la regla trapezoidal y las reglas de Simpson más empleadas. En estos esquemas, la idea central es la distribución uniforme de los puntos que siguen la regla $x_i=x_0+ih$, con $i=0,1,2, \ldots, n$ y la evaluación de la función en estos puntos.

Supongamos ahora que la restricción de la uniformidad en el espaciamiento de esos puntos fijos no es más considerada y se tiene la libertad de evaluar el área bajo una recta que conecte a dos puntos cualesquiera sobre la curva. Al ubicar estos puntos en forma “inteligente”, se puede definir una línea recta que equilibre los errores negativos y positivos

 Fuente: Chapra, S., Canale, R. Métodos Numéricos para ingenieros, 5a Ed. Mc. Graw Hill. 2007

De la figura de la derecha, se disponen de los puntos $x_0$ y $x_1$ para evaluar la función $f(x)$. Expresando la integral bajo la curva de forma aproximada dada en la la ecuación ([5.35](#Ec5_35)), y empleando los límites de integración en el intervalo $[-1,1]$ por simplicidad (después se generalizará el concepto a un intervalo $[a,b]$), se tiene

\begin{equation*}
\begin{split}
I=\int_{-1}^1 f(x)dx \approx c_0f(x_0)+c_1f(x_1)
\end{split}
\label{eq:Ec5_36} \tag{5.36}
\end{equation*}

[Volver a la Tabla de Contenido](#TOC)

#### Determinación de los coeficientes

se tiene una ecuación con cuatro incógnitas ($c_0, c_1, x_0$ y $x_1$) que se deben determinar. Para ello, supongamos que disponemos de un polinomio de hasta grado 3, $f_3(x)$, de donde podemos construir cuatro ecuaciones con cuatro incógnitas de la siguiente manera:

- $f_3(x)=1$:

\begin{equation*}
\begin{split}
\int_{-1}^1 1dx = c_0 \times 1 + c_1 \times 1 = c_0 + c_1 = 2
\end{split}
\label{eq:Ec5_37} \tag{5.37}
\end{equation*}

- $f_3(x)=x$:

\begin{equation*}
\begin{split}
\int_{-1}^1 xdx = c_0x_0 + c_1x_1 = 0
\end{split}
\label{eq:Ec5_38} \tag{5.38}
\end{equation*}

- $f_3(x)=x^2$:

\begin{equation*}
\begin{split}
\int_{-1}^1 x^2dx = c_0x^2_0 + c_1x^2_1 = \frac{2}{3}
\end{split}
\label{eq:Ec5_39} \tag{5.39}
\end{equation*}

y por último

- $f_3(x)=x^3$:

\begin{equation*}
\begin{split}
\int_{-1}^1 x^3dx = c_0x^3_0 + c_1x^3_1 = 0
\end{split}
\label{eq:Ec5_40} \tag{5.40}
\end{equation*}

resolviendo simultáneamente las dos primeras ecuaciones para $c_0$ y $c_1$ en térm,inos de $x_0$ y $x_1$, se llega a

\begin{equation*}
\begin{split}
c_0=\frac{2x_1}{x_1-x_0}, \quad c_1=-\frac{2x_0}{x_1-x_0}\end{split}
\label{eq:Ec5_41} \tag{5.41}
\end{equation*}

reemplazamos estos dos valores en las siguientes dos ecuaciones

\begin{equation*}
\begin{split}
\frac{2}{3}=\frac{2x_0^2x_1}{x_1-x_0}-\frac{2x_0x_1^2}{x_1-x_0}
\end{split}
\label{eq:Ec5_42} \tag{5.42}
\end{equation*}

\begin{equation*}
\begin{split}
0=\frac{2x_0^3x_1}{x_1-x_0}-\frac{2x_0x_1^3}{x_1-x_0}
\end{split}
\label{eq:Ec5_43} \tag{5.43}
\end{equation*}

de la ecuación ([5.43](#Ec5_43)) se tiene

\begin{equation*}
\begin{split}
x_0^3x_1&=x_0x_1^3 \\
x_0^2 &= x_1^2
\end{split}
\label{eq:Ec5_44} \tag{5.44}
\end{equation*}

de aquí se tiene que $|x_0|=|x_1|$ (para considerar las raíces negativas recuerde que $\sqrt{a^2}= \pm a = |a|$), y como se asumió que $x_0<x_1$, entonces $x_0<0$ y $x_1>0$ (trabajando en el intervalo $[-1,1]$), llegándose finalmente a que $x_0=-x_1$. Reemplazando este resultado en la ecuación ([5.42](#Ec5_42))

\begin{equation*}
\begin{split}
\frac{2}{3}=2\frac{x_1^3+x_1^3}{2x_1}
\end{split}
\label{eq:Ec5_45} \tag{5.45}
\end{equation*}

despejando, $x_1^2=1/3$, y por último se llega a que

\begin{equation*}
\begin{split}
x_0=-\frac{\sqrt{3}}{3}, \quad x_1=\frac{\sqrt{3}}{3}
\end{split}
\label{eq:Ec5_46} \tag{5.46}
\end{equation*}

reemplazando estos resultados en la ecuación ([5.41](#Ec5_41)) y de la ecuación ([5.37](#Ec5_37)), se tiene que $c_0=c_1=1$. Reescribiendo la ecuación ([5.36](#Ec5_36)) con los valores encontrados se llega por último a:

\begin{equation*}
\begin{split}
I=\int_{-1}^1 f(x)dx &\approx c_0f(x_0)+c_1f(x_1) \\
&= f \left( \frac{-\sqrt{3}}{3}\right)+f \left( \frac{\sqrt{3}}{3}\right)
\end{split}
\label{eq:Ec5_47} \tag{5.47}
\end{equation*}

Esta aproximación realizada es "exacta" para polinomios de grado menor o igual a tres ($3$). La aproximación trapezoidal es exacta solo para polinomios de grado uno ($1$).

***Ejemplo:*** Calcule la integral de la función $f(x)=x^3+2x^2+1$ en el intervalo $[-1,1]$ empleando tanto las técnicas analíticas como la cuadratura de Gauss vista.

- ***Solución analítica (exacta)***

$$\int_{-1}^1 (x^3+2x^2+1)dx=\left.\frac{x^4}{4}+\frac{2x^3}{3}+x \right |_{-1}^1=\frac{10}{3}$$

- ***Aproximación numérica por Cuadratura de Gauss***

\begin{equation*}
\begin{split}
\int_{-1}^1 (x^3+2x^2+1)dx &\approx1f\left(-\frac{\sqrt{3}}{3} \right)+1f\left(\frac{\sqrt{3}}{3} \right) \\
&=-\frac{3\sqrt{3}}{27}+\frac{2\times 3}{9}+1+\frac{3\sqrt{3}}{27}+\frac{2\times 3}{9}+1 \\
&=2+\frac{4}{3} \\
&= \frac{10}{3}
\end{split}
\end{equation*}

[Volver a la Tabla de Contenido](#TOC)

#### Cambios de los límites de integración

Obsérvese que los límites de integración de la ecuación ([5.47](#Ec5_47)) son de $-1$ a $1$. Esto se hizo para simplificar las matemáticas y para hacer la formulación tan general como fuera posible. Asumamos ahora que se desea determinar el valor de la integral entre dos límites cualesquiera $a$ y $b$. Supongamos también, que una nueva variable $x_d$ se relaciona con la variable original $x$ de forma lineal,

\begin{equation*}
\begin{split}
x=a_0+a_1x_d
\end{split}
\label{eq:Ec5_48} \tag{5.48}
\end{equation*}

si el límite inferior, $x=a$, corresponde a $x_d=-1$, estos valores podrán sustituirse en la ecuación ([5.48](#Ec5_48)) para obtener

\begin{equation*}
\begin{split}
a=a_0+a_1(-1)
\end{split}
\label{eq:Ec5_49} \tag{5.49}
\end{equation*}

de manera similar, el límite superior, $x=b$, corresponde a $x_d=1$, para dar

\begin{equation*}
\begin{split}
b=a_0+a_1(1)
\end{split}
\label{eq:Ec5_50} \tag{5.50}
\end{equation*}

resolviendo estas ecuaciones simultáneamente,

\begin{equation*}
\begin{split}
a_0=(b+a)/2, \quad a_1=(b-a)/2
\end{split}
\label{eq:Ec5_51} \tag{5.51}
\end{equation*}

sustituyendo en la ecuación ([5.48](#Ec5_48))

\begin{equation*}
\begin{split}
x=\frac{(b+a)+(b-a)x_d}{2}
\end{split}
\label{eq:Ec5_52} \tag{5.52}
\end{equation*}

derivando la ecuación ([5.52](#Ec5_52)),

\begin{equation*}
\begin{split}
dx=\frac{b-a}{2}dx_d
\end{split}
\label{eq:Ec5_53} \tag{5.53}
\end{equation*}

Las ecuacio es ([5.51](#Ec5_51)) y ([5.52](#Ec5_52)) se pueden sustituir para $x$ y $dx$, respectivamente, en la evaluación de la integral. Estas sustituciones transforman el intervalo de integración sin cambiar el valor de la integral. En este caso

\begin{equation*}
\begin{split}
\int_a^b f(x)dx = \frac{b-a}{2} \int_{-1}^1 f \left( \frac{(b+a)+(b-a)x_d}{2}\right)dx_d
\end{split}
\label{eq:Ec5_54} \tag{5.54}
\end{equation*}

Esta integral se puede aproximar como,

\begin{equation*}
\begin{split}
\int_a^b f(x)dx \approx \frac{b-a}{2} \left[f\left( \frac{(b+a)+(b-a)x_0}{2}\right)+f\left( \frac{(b+a)+(b-a)x_1}{2}\right) \right]
\end{split}
\label{eq:Ec5_55} \tag{5.55}
\end{equation*}

[Volver a la Tabla de Contenido](#TOC)

#### Fórmulas de punto superior

La fórmula anterior para la cuadratura de Gauss era de dos puntos. Se pueden desarrollar versiones de punto superior en la forma general:

\begin{equation*}
\begin{split}
I \approx c_0f(x_0) + c_1f(x_1) + c_2f(x_2) +\ldots+ c_{n-1}f(x_{n-1})
\end{split}
\label{eq:Ec5_56} \tag{5.56}
\end{equation*}

con $n$, el número de puntos.

Debido a que la cuadratura de Gauss requiere evaluaciones de la función en puntos espaciados uniformemente dentro del intervalo de integración, no es apropiada para casos donde se desconoce la función. Si se conoce la función, su ventaja es decisiva.

En la siguiente tabla se presentan los valores de los parámertros para $1, 2, 3, 4$ y $5$ puntos.

|$$n$$ |  $$c_i$$   |     $$x_i$$   |
|:----:|:----------:|:-------------:|
|$$1$$ |$$2.000000$$| $$0.000000$$  |
|$$2$$ |$$1.000000$$|$$\pm0.577350$$|
|$$3$$ |$$0.555556$$|$$\pm0.774597$$|
|      |$$0.888889$$| $$0.000000$$  |
|$$4$$ |$$0.347855$$|$$\pm0.861136$$|
|      |$$0.652145$$|$$\pm0.339981$$|
|$$5$$ |$$0.236927$$|$$\pm0.906180$$|
|      |$$0.478629$$|$$\pm0.538469$$|
|      |$$0.568889$$| $$0.000000$$  |

```python
import numpy as np
import pandas as pd
GaussTable = [[[0], [2]], [[-1/np.sqrt(3), 1/np.sqrt(3)], [1, 1]], [[-np.sqrt(3/5), 0, np.sqrt(3/5)], [5/9, 8/9, 5/9]], [[-0.861136, -0.339981, 0.339981, 0.861136], [0.347855, 0.652145, 0.652145, 0.347855]], [[-0.90618, -0.538469, 0, 0.538469, 0.90618], [0.236927, 0.478629, 0.568889, 0.478629, 0.236927]], [[-0.93247, -0.661209, -0.238619, 0.238619, 0.661209, 0.93247], [0.171324, 0.360762, 0.467914, 0.467914, 0.360762,  0.171324]]]
display(pd.DataFrame(GaussTable, columns=["Integration Points", "Corresponding Weights"]))
def IG(f, n):
  n = int(n)
  return sum([GaussTable[n - 1][1][i]*f(GaussTable[n - 1][0][i]) for i in range(n)])
def f(x): return x**9 + x**8
IG(f, 5.0)
```

[Volver a la Tabla de Contenido](#TOC)

#### Ejemplo Cuadratura de Gauss

Determine el valor aproximado de:

$$\int_0^1 \frac{4}{1+x^2}dx$$

empleando cuadratura gaussiana de dos puntos.

Reemplazando los parámetros requeridos en la ecuación ([5.55](#Ec5_55)), donde $a=0$, $b=1$, $x_0=-\sqrt{3}/3$ y $x_1=\sqrt{3}/3$

\begin{equation*}
\begin{split}
\int_0^1 f(x)dx &\approx \frac{1-0}{2} \left[f\left( \frac{(1+0)+(1-0)\left(-\frac{\sqrt{3}}{3}\right)}{2}\right)+f\left( \frac{(1+0)+(1-0)\left(\frac{\sqrt{3}}{3}\right)}{2}\right) \right]\\
&= \frac{1}{2} \left[f\left( \frac{1-\frac{\sqrt{3}}{3}}{2}\right)+f\left( \frac{1+\frac{\sqrt{3}}{3}}{2}\right) \right]\\
&= \frac{1}{2} \left[ \frac{4}{1 + \left( \frac{1-\frac{\sqrt{3}}{3}}{2} \right)^2}+\frac{4}{1 + \left( \frac{1+\frac{\sqrt{3}}{3}}{2} \right)^2} \right]\\
&=3.147541
\end{split}
\end{equation*}

Ahora veamos una breve implementación computacional

```python
import numpy as np
```

```python
def fxG(a, b, x):
    xG = ((b + a) + (b - a) * x) / 2
    return funcion(xG)
```

```python
def GQ2(a,b):
    c0 = 1.0
    c1 = 1.0
    x0 = -1.0 / np.sqrt(3)
    x1 =  1.0 / np.sqrt(3)

    return (b - a) / 2 * (c0 * fxG(a,b,x0) + c1 * fxG(a,b,x1))

```

```python
print(GQ2(0,1))
```

[Volver a la Tabla de Contenido](#TOC)

```python
from IPython.core.display import HTML
def css_styling():
    styles = open('./nb_style.css', 'r').read()
    return HTML(styles)
css_styling()
```

<link href='http://fonts.googleapis.com/css?family=Fenix' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' type='text/css'>

```python

```