# Grupo 7: Regresión logística

```python
# librerías
import math
import numpy as np
import pandas as pd
from sklearn.preprocessing import OneHotEncoder
import seaborn as sns
import sys
import matplotlib.pyplot as plt
import pprint
from IPython.display import Image
```

```python
import func
```

## Introducción
Tomando como referencia el trabajo de investigación realizado por Colubri et al. (2019), se busca entrenar un modelo de regresión logística que permite pronosticar la supervivencia a enfermedad por virus del ébola.  Para tal propósito, se utilizan datos recolectados por el Cuerpo Internacionalde Medicina (IMC, por sus siglas en inglés) durante 2014 y 2016 en Liberia y Sierra Leona.

>**Objetivo General:** Evaluar el desempeño de diferentes métodos de optimización numérica (método del máximo descenso, método de Newton, método BFGS(Broyden, Fletcher, Goldfarb y Shanno), y SGD), en un contexto de machine learning, utilizando métodos cómputo simples y en paralelo.

>>**Objetivo Particular:** En particular, se mide el desempeño, tanto en términos del uso y tiempo de recursos computacionales para lograr convergencia, como con relación a métricas de desempeño del modelo

**Nota:** Esta implementación se basa en material y actividades impartidas por los profesores de los cursos de [Métodos Numéricos y optimización](https://github.com/ITAM-DS/analisis-numerico-computo-cientifico/blob/master/temas/IV.optimizacion_convexa_y_machine_learning/4.3.Regresion_logistica_R.ipynb) (2010-I) (Prof. Erick Palacios Moreno) y Aprendizaje de Máquina (2019-II) (Prof. Rodrigo Mendoza Smith).

**Orden de Presentación**

> [1. Introducción al problema a resolver.](#section1)

> [2. Conjunto de Datos.](#section2)

> [3. Implementación de los métodos numéricos (demostración con Docker y AWS)](#section3)

> [4. Resultados obtenidos.](#section4)

## 1. Problema de regresión Logística

Como es resaltado por Murphy (2012), el problema de regresión logística es una generalización del problema de regresión lineal, convirtiendolo hacia un problema de clasificación, siempre y cuando la variable de respuesta sea de carácter binario (i.e. $y\in\left\{ 0,1\right\} $), y por tanto se pueda asumir que sigue una distribución Bernoulli. En este caso, dicha variable de respuesta corresponde a si el paciente muere a casua del virus del ébola $(1)$, o no $(0)$.

Dado lo anterior, utilizamos:

$$
{Pr[y\mid\boldsymbol{x,w}]=Ber(y\mid\mu(x))}\label{eq:dist_ber}
$$

donde la media, se define en términos de la probabilidad de que el
paciente muera, es decir, $\mu(x)=E\left[y\mid x\right]=p\left(y=1\mid x\right)$.
Donde el conjunto de variables explicativas está representado por
$x$.

Por otro lado, para la realización del computo de la media, se utiliza
la función sigmoide, $\sigma(x)$, la cual garantiza que dada una
combinación lineal de las variables explicativas, con los parámetros
del modelo (i.e. $\beta^{T}x$), se cumpla que $0\leq\mu(x)\leq1$:

$$
\mu\left(x\right)=\sigma\left(\beta^{T}x\right)\label{eq:mu_x}
$$

donde $\sigma$ está definida por:

\begin{equation}
\sigma(x)=\frac{1}{1+\exp(-x)}=\frac{e^{x}}{e^{x}+1}\label{eq:sigmoide}
\end{equation}

Los resultados asociados a la ecuación $\mu(x)$ están dados
en términos de probabilidades. Así, es necesario definir un umbral clasificatorio para definir si el modelo predice que el paciente fallezca, o no. En este ejercicio, como se hace usualmente, y como realizan Colubri et. al (2019), se toma como umbral el valor de $0.5$. Es decir:

Los parámetros asociados a las variables regresoras, $\hat{\beta}$, son estimados con el conjunto de datos de entrenamiento.

### Función de pérdida

La función de pérdida asociada a este problema es la log-verosimilitud
negativa, o entropía cruzada, definida de las siguiente forma:

$$
LVN(\beta)=-\sum_{i=1}^{N}\left[y_{i}log\mu_{i}+(1-y_{i})log(1-\mu_{i})\right]\label{eq:lvn2}
$$

**Este problema no tiene solución analítica**. Es por esta razón que para minimizar
la función de pérdida en torno a $\beta$ es requerido **utilizar métodos de optimización numérica**.

## 2. Conjunto de datos

Por la restricciones de uso de la base de datos de entrenamiento original (además de una serie de requerimientos protocolares como contar con la aprobación de un Comité de Ética Independiente), optamos por trabajar con una de las dos bases de datos que los autores emplearon para validar sus modelos: KGH. La base de datos en mención, consta de $106$ casos positivos  de  pacientes  con  ébola  y  un  case fatality rate global por encima del setenta por ciento.  Originalmente,  previo  al tratamiento de los datos, la base tenía únicamente $44$ registros de triaje, $58$ registros de carga viral, con un total de 78 valores faltantes en todo el data set. Para  harmonizar  los  datos,  los  autores  transformaron  la  carga  viral  en  CT,  conforme  con  la curva estándar qPCR:

$$log_{(carga \; viral)} = m*CT + c_0$$

Nosotros, para fines del presente trabajo, empleamos una de las versiones imputadas de esta base de datos, dispuesta en el siguiente sitio: [ebola-imc-public](https://github.com/dapivei/ebola-imc-public/blob/master/data/kenema/test/pres-kgh/imputation-50.csv), misma que cuenta con $11$ variables: la variable output, $y_{i}$ asociada a la supervivencia o no del paciente ${i}$ con virus del ébola, y ${j}$ variables explicativas asociadas, $x_{i,j}$. Los regresores escogidos son aquellos que, conforme con nuestra principal referencia, son buenos predictores de la probabilidad de muerte o no de un paciente.

|Tipo| Nombre|Descripción|
|---| --- | --- |
|Variable Numérica| CT |El cycle threshold (CT) es una variable que se calcula a partir de una relación médica bien conocida (qPCR) y la carga viral (una expresión númerica de la cantidad de virus dado un volúmen de fluido que normalmente se correlaciona con la severidad de una infección viral activa).|
|Variable Numérica|TEMP|Temperatura corporal del paciente. Toma valores de $36$ a $39.9$.|
|Variable Numérica|_AGE_ |Edad del paciente. Toma valores de $1$ a $73$.|
|Variable Categórica |_HEADCH_ | Presencia o no dolores de cabeza. Toma valores valores $0$ o $1$, dependiendo de si el paciente presenta o no dolores de cabeza.|
|Variable Categórica |  _BLEED_ | Presencia o no de sangrado. Toma valores valores $0$ o $1$, dependiendo de si el paciente presenta o no sangrado. |
|Variable Categórica |  _DIARR_ | Presencia o no de diarrea. Toma valores valores $0$ o $1$, dependiendo de si el paciente presenta o no diarrea.|
|Variable Categórica | _VOMIT_ | Dificultad para comer, conocido como disfagia, término técnico para describir el síntoma consistente en dificultad para la deglución (problemaspara  tragar).   Esta  dificultad  suele  ir  acompañada  de  dolores,  a  veces lancinantes (disfagia dolorosa u odinofagia .  Toma valores valores $1$ o $0$, dependiendo de si el paciente presenta o no de disfacia
|Variable Categórica | _PABD*_ | Presencia o no de PADB.
|Variable Categórica |_WEAK_ | Presencia o no de debilidad o fatiga general.|
|Variable Categórica |_JAUN_ |Condición  en la cuál la piel, los ojos y los miembros mucosos que vuelven amarillos debido a altos niveles de bilirubina. Toma valores valores $0$ o $1$, dependiendo de si el paciente presenta o no ictericia.|
|Variable Categórica |_OUT_| Muerte o no muerte del paciente.  Toma valores $1$ o $0$.  Dependiendo desi el paciente muere o no muere.|

*: _PABD_ se refiere a Donación de sangre autóloga -personas en donde los trasplantes y las transfusiones la misma persona es a la vez donante y receptora- preoperatoria, Preoperative Autologous Blood Donation)

---------------------

## Importación y exploración del conjunto de datos

En esta sección se importa y transforma los datos, con el fin de obtener el conjunto $\mathcal{D}$.

```python
url="https://raw.githubusercontent.com/afcarl/ebola-imc-public/master/data/kenema/test/pres-kgh/imputation-50.csv"
df_raw=pd.read_csv(url,sep=",")
```

```python
df_raw.head()
# df[df.isnull().any(axis=1)] - no hay NAs
```

      OUT
      CT
      AGE
      TEMP
      HEADCH
      BLEED
      DIARR
      JAUN
      VOMIT
      PABD
      WEAK

      0
      1
      28.652450
      42.0
      36.3
      0
      0
      1
      0
      0
      1
      1

      1
      1
      25.736016
      45.0
      36.5
      1
      0
      1
      0
      0
      1
      1

      2
      1
      20.747653
      65.0
      38.0
      1
      0
      0
      0
      0
      0
      0

      3
      1
      22.736993
      44.0
      38.6
      1
      0
      0
      0
      0
      0
      1

      4
      1
      20.846284
      11.0
      38.4
      1
      0
      0
      0
      1
      0
      1

```python
# verificar tipo de variables
df_raw.dtypes
```

    OUT         int64
    CT        float64
    AGE       float64
    TEMP      float64
    HEADCH      int64
    BLEED       int64
    DIARR       int64
    JAUN        int64
    VOMIT       int64
    PABD        int64
    WEAK        int64
    dtype: object

```python
# Resumen de las variables
df_raw.describe()
```

      OUT
      CT
      AGE
      TEMP
      HEADCH
      BLEED
      DIARR
      JAUN
      VOMIT
      PABD
      WEAK

      count
      106.000000
      106.000000
      106.000000
      106.000000
      106.000000
      106.000000
      106.000000
      106.0
      106.000000
      106.000000
      106.000000

      mean
      0.764151
      25.720411
      34.102170
      37.256604
      0.603774
      0.066038
      0.405660
      0.0
      0.207547
      0.273585
      0.500000

      std
      0.426545
      5.869164
      17.382844
      1.030767
      0.491436
      0.249528
      0.493352
      0.0
      0.407477
      0.447916
      0.502375

      min
      0.000000
      12.100000
      0.830000
      36.000000
      0.000000
      0.000000
      0.000000
      0.0
      0.000000
      0.000000
      0.000000

      25%
      1.000000
      22.149857
      22.000000
      36.300000
      0.000000
      0.000000
      0.000000
      0.0
      0.000000
      0.000000
      0.000000

      50%
      1.000000
      25.236301
      35.500000
      37.250000
      1.000000
      0.000000
      0.000000
      0.0
      0.000000
      0.000000
      0.500000

      75%
      1.000000
      28.680924
      45.000000
      38.225000
      1.000000
      0.000000
      1.000000
      0.0
      0.000000
      1.000000
      1.000000

      max
      1.000000
      39.799999
      80.000000
      39.900000
      1.000000
      1.000000
      1.000000
      0.0
      1.000000
      1.000000
      1.000000

```python
df_raw_cat = df_raw.copy()

cat_vars = ['OUT', 'HEADCH', 'BLEED', 'DIARR', 'JAUN', 'VOMIT',
       'PABD', 'WEAK']
for var in cat_vars:
    df_raw_cat[var] = df_raw_cat[var].astype('category')
df_raw_cat.dtypes

```

    OUT       category
    CT         float64
    AGE        float64
    TEMP       float64
    HEADCH    category
    BLEED     category
    DIARR     category
    JAUN      category
    VOMIT     category
    PABD      category
    WEAK      category
    dtype: object

```python
df_raw_cat.describe()
```

      CT
      AGE
      TEMP

      count
      106.000000
      106.000000
      106.000000

      mean
      25.720411
      34.102170
      37.256604

      std
      5.869164
      17.382844
      1.030767

      min
      12.100000
      0.830000
      36.000000

      25%
      22.149857
      22.000000
      36.300000

      50%
      25.236301
      35.500000
      37.250000

      75%
      28.680924
      45.000000
      38.225000

      max
      39.799999
      80.000000
      39.900000

```python
# Describe categorical data
df_proc_cat = df_raw_cat.select_dtypes(include=['category']).copy()
df_proc_cat.describe()
```

      OUT
      HEADCH
      BLEED
      DIARR
      JAUN
      VOMIT
      PABD
      WEAK

      count
      106
      106
      106
      106
      106
      106
      106
      106

      unique
      2
      2
      2
      2
      1
      2
      2
      2

      top
      1
      1
      0
      0
      0
      0
      0
      1

      freq
      81
      64
      99
      63
      106
      84
      77
      53

```python
cat_vars = cat_vars[1:]

for i, col_val in enumerate(cat_vars):
    sns.catplot(x = col_val, y = None, hue= None, col="OUT",
                data=df_proc_cat, kind="count",
                height=3, aspect=2);

```

**Algunas observaciones sobre los datos**

- El case fatality rate (OUT) de nuestra base de datos se sitúa en 76 por ciento. Es decir, del total de pacientes con ébola, 81 murieron.

- Para este conjunto de datos la variable `JAUN` no tiene variabilidad, por lo tanto no es una variable, y se omite.

Dado lo anterior, se ajusta el set de datos:

- Se crean 3 grupos de edades, utilizando el percetil 25 (22 años), percentil 50 (36 años) y percentil 75 (45 años).

- Se elimina la columna `JAUN`.

## Transformaciones al conjunto de datos

```python
# ajustes en df_raw
df_proc = df_raw
df_proc['INTER_AGE'] = "NA"

df_proc.dtypes
```

    OUT            int64
    CT           float64
    AGE          float64
    TEMP         float64
    HEADCH         int64
    BLEED          int64
    DIARR          int64
    JAUN           int64
    VOMIT          int64
    PABD           int64
    WEAK           int64
    INTER_AGE     object
    dtype: object

```python
# ajustes en df_raw
df_proc = df_raw

# para la variable edad se crean cuatro categorías
age_p25 = math.ceil(df_proc['AGE'].quantile(.25))
age_p50 = math.ceil(df_proc['AGE'].quantile(.50))
age_p75 = math.ceil(df_proc['AGE'].quantile(.75))

df_proc['INTER_AGE'] = "NA"
df_proc.loc[(df_proc['AGE'] <= age_p25), 'INTER_AGE'] = 1
df_proc.loc[(df_proc['AGE'] > age_p25) & (df_proc['AGE'] <= age_p50), 'INTER_AGE'] = 2
df_proc.loc[(df_proc['AGE'] > age_p50) & (df_proc['AGE'] <= age_p75), 'INTER_AGE'] = 3
df_proc.loc[(df_proc['AGE'] > age_p75), 'INTER_AGE'] = 4

## one hot encoding
enc = OneHotEncoder(handle_unknown='ignore')
enc_df = pd.DataFrame(enc.fit_transform(df_proc[['INTER_AGE']]).toarray())
enc_df = enc_df.rename(columns={0: f"hasta{age_p25}", 1: f"entre{age_p25+1}y{age_p50}", 2: f"entre{age_p50+1}y{age_p75}", 3:f"mayor{age_p75}"})
# merge with main df bridge_df on key values
df_proc = df_proc.join(enc_df)

# se omiten las variables JAUN, AGE, INTER_AGE
del_vars = ["JAUN", "AGE", "INTER_AGE"]
for var in del_vars:
    df_proc = df_proc.drop(var, axis=1)

# se comprueban los tipos de variable
df_proc.dtypes
```

    OUT             int64
    CT            float64
    TEMP          float64
    HEADCH          int64
    BLEED           int64
    DIARR           int64
    VOMIT           int64
    PABD            int64
    WEAK            int64
    hasta22       float64
    entre23y36    float64
    entre37y45    float64
    mayor45       float64
    dtype: object

```python
df_proc
```

      OUT
      CT
      TEMP
      HEADCH
      BLEED
      DIARR
      VOMIT
      PABD
      WEAK
      hasta22
      entre23y36
      entre37y45
      mayor45

      0
      1
      28.652450
      36.3
      0
      0
      1
      0
      1
      1
      0.0
      0.0
      1.0
      0.0

      1
      1
      25.736016
      36.5
      1
      0
      1
      0
      1
      1
      0.0
      0.0
      1.0
      0.0

      2
      1
      20.747653
      38.0
      1
      0
      0
      0
      0
      0
      0.0
      0.0
      0.0
      1.0

      3
      1
      22.736993
      38.6
      1
      0
      0
      0
      0
      1
      0.0
      0.0
      1.0
      0.0

      4
      1
      20.846284
      38.4
      1
      0
      0
      1
      0
      1
      1.0
      0.0
      0.0
      0.0

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

      101
      1
      24.191797
      36.4
      0
      0
      1
      1
      1
      1
      0.0
      0.0
      1.0
      0.0

      102
      1
      20.846284
      38.4
      0
      0
      0
      1
      0
      1
      0.0
      1.0
      0.0
      0.0

      103
      0
      38.816561
      36.0
      0
      0
      0
      0
      0
      0
      1.0
      0.0
      0.0
      0.0

      104
      1
      21.960294
      36.4
      0
      0
      0
      0
      0
      0
      0.0
      1.0
      0.0
      0.0

      105
      0
      26.221948
      36.5
      1
      0
      0
      0
      0
      0
      0.0
      0.0
      1.0
      0.0

106 rows × 13 columns

## Planteamiento del problema de regresión

Para los algoritmos a utilizar, es imprecindible la utilización del gradiente y la matriz hessiana asoaciados a $LVN(\beta)$, los cuales se definen a continuación:

$$
\nabla LVN=\frac{d}{d\boldsymbol{\beta}}f(\boldsymbol{\beta})=\sum_{i}(\mu_{i}-y_{i})x_{i}=X^{T}(\mu-y)\label{eq:gradiente}
$$

$$
\nabla^{2}LVN=\frac{d}{d\boldsymbol{\beta}}g(\boldsymbol{\beta})^{T}=\sum\left(\nabla_{\beta}\mu_{i}\right)x_{i}^{T}=\sum\mu_{i}(1-\mu_{i})x_{i}x_{i}^{T}=\boldsymbol{X}^{T}\boldsymbol{S}\boldsymbol{X}\label{eq:hessiana}
$$

donde $\boldsymbol{S}\triangleq diag(\mu_{i}(1-\mu_{i}))$. Adicionalmente, como resalta Murphy (2012), dado que $\boldsymbol{H}$ es positiva definida, entonces $LVN(\beta)$ tiene un mínimo global que puede ser alcanzable utilizando métodos de optimización.

### Métodos de búsqueda de línea

En los métodos de búsqueda de línea son métodos iterativos que consisten en dar pasos sobre una dirección de descenso para una función convexa. En cada iteración damos una paso de la forma:

$$x_{k+1}=x_k +\alpha_k p_k$$

donde $\alpha$ representa el tamaño del paso y $p_k$ la dirección del descenso.

La dirección de descenso la definimos como:

$$p_k=-B^{-1}_k \nabla f_k$$

Con $B$ una matriz simétrica y no singular.

Cuando $B_k$ es definida postiva entonces tenemos que:

$$p^T_k \nabla f_k = -\nabla f^T_k B^{-1}_k \nabla f_k$$

y por tanto $p_k$ es una dirección de descenso.

### Condiciones de Armijo

$$
f\left(x_{k}+\alpha p_{k}\right)\leq f(x_{k})+c_{1}\alpha\nabla f_{k}^{T}p_{k}
$$

### Método de máximo descenso

$$B_k = I$$

### Método de Newton

$$B_k = \nabla^2 f_k$$

### BFGS

$$B^{-1}_k \approx H_k$$

```python
def descent_direction(X, y, beta, method="max",H=None):
    '''
    Función que devuelve vector normalizado (px1) que apunta en la direccion de decenso
        ** Parámetros:
            - X (mat): matriz de mxp entradas
            - y (vec): vector de de m entradas de la variable output
            - beta (vec float64): vector de entradas a optimizar
            - method (str): método que determina la dirección de descenso

                    --Opciones:
                            --- max: método de descenso
                            --- newton: método de Newton
                            --- bfsg: metodo bfsg
            - H (mat pxp): Parámetro para la dirección de decenso del metodo bfgs

        ** Salidas
            - pk (vec): vector normalizado con la direccion del paso
    '''
    if(method == "max"):
        pk = gradiente_f(X,y,beta)

    elif(method == "newton"):
        grad = gradiente_f(X,y,beta)
        hess = hessiana_f(X,y,beta)
        pk = np.linalg.solve(hess,grad)

    elif(method=="bfsg"):
        # Se revisa que los parámetros de entrada sean congruentes con la funcionalidad
        if type(H) is not np.ndarray:
            sys.exit('Error: H debe ser de tipo numpy.ndarray')
        pk = np.matmul(H,gradiente_f(X,y,beta))

    return - normalize(pk)

```

```python
def calc_lr(X, y, beta, lr, pk, c1=10**(-4), tao=0.5, reset_lr=False):
    '''
    Función que calcula el tamaño del paso para cada iteración utilizando la condicion de armijo.
    La tasa de aprendizaje minima es la que tenia en el paso anterior.

        ** Parámetros:

            - X (mat): matriz de mxp entradas
            - y (vec): vector de de m entradas de la variable output
            - lr (float64): tasa de aprendizaje
            - pk (array px1 float64): direccion de decenso
            - c1 (float64) 0<c1<1: parametro de control
            - tao (float64) 0<tao<1: parametro de decrecimiento de lr

        ** Salidas

            - lr (float64): tamaño de paso
    '''
    # Se revisa que los parámetros de entrada sean congruentes con la funcionalidad
    if tao > 1 or tao < 0:
        sys.exit('Error:  tao es un parámetro que debe estar entre 0 y 1')
    if c1 > 1 or c1 < 0:
        sys.exit('Error:  c1 es un paramétro que debe estar entre 0 y 1')

    # Inicializamos
    tao = 0.9
    max_iter = 100
    iter = 0

    # Inicializa lr
    if reset_lr==True: lr = 1

    # Evaluaciones periódicas
    grad = gradiente_f(X,y,beta)
    eval_f = f(X,y, beta)

    # Primera iteracion
    f_x =  f(X,y, beta + lr*pk) #en nocedal es phi(alpha)
    f_x1 = eval_f + c1 * lr *  np.dot(grad,pk) # en nocedal es l(alhpa)

    while ((f_x > f_x1) & (iter < max_iter)):
        lr = lr*tao
        f_x =  f(X,y, beta + lr*pk)
        f_x1 = eval_f + c1 * lr *  np.dot(grad,pk)
        iter+=1

    return lr
```

```python
def gradient_descent(X, y, lr=1, tol=10**(-7), max_iter=10**5, method="max", reset_lr=False, verbose_n=1000):
    '''
    Función que devuelve vector de parámetros beta (px1) resultante del proceso
    de optimización por descenso de gradiente

        ** Parámetros:
            - X (mat): matriz de mxp entradas
            - y (vec): vector de de m entradas de la variable output
            - lr (float64): valor inicial de la tasa de aprendizaje
            - tol (float64): criterio de convergencia
            - max_iter (int): número máximo de iteraciones
            - method (str): método que determina la dirección de descenso
                Opciones:
                    -- max: método de descenso
                    -- newton: método de Newton
                    -- bfsg
        ** Salidas
            - beta_new (vec): vector de p entradas con parámetros que minimizan la función de pérdida
    '''
    # imprime método
    out.write(BOLD + "="*65)
    if (method=="max"):
        out.write(BOLD + 'Método de máximo descenso')
    elif (method=="newton"):
        out.write(BOLD + 'Método de Newton')
    elif (method=="bfsg"):
        out.write(BOLD + 'Método de BFGS')
    out.write(BOLD + "="*65)

    # Se revisa que los parámetros de entrada sean congruentes con la funcionalidad
    m,p = X.shape
    if y.shape[0]!= m:
        sys.exit('Error:  El número de renglones de X debe ser igual al número de entradas del vector y.')

    # Inicializa
    iteraciones=0
    H = None
    dims = X.shape[1]
    tol = tol*dims

    # Inicializamos beta aleatoria
    beta = np.random.normal(1,3,dims)
    if method =="bfsg": H = np.identity(dims)

    # Primera iteracion
    pk =  descent_direction(X, y, beta, method,H)
    beta_new = beta + lr*pk
    if method == "bfsg": H=calc_H(X,y,beta,beta_new,H)

    # Condición de paro.
    while ((np.linalg.norm(gradiente_f(X,y,beta_new)) > tol) & (iteraciones < max_iter)):
        iteraciones+=1 #contador de ciclo

        beta = beta_new
        pk =  descent_direction(X,y,beta,method,H)
        lr = calc_lr(X, y, beta, lr, pk, reset_lr = reset_lr)

        beta_new = beta + lr*pk

        if method == "bfsg": H=calc_H(X,y,beta,beta_new,H)

        # Imprime

        if iteraciones % verbose_n == 0:
            grad=np.linalg.norm(gradiente_f(X,y,beta_new))
            out.write(UNDERSCORE + f'Iteración: {iteraciones}' + ALL_OFF + f' gradiente: {grad:.7E}, alpha: {lr:.4E}')

    print("\n")
    out.write(BOLD  + '** Resultados Finales')
    if iteraciones == max_iter:out.write(BOLD + 'El algoritmo paro porque' + BOLD + FG_RED + 'se alcanzo el nro. máximo de iteraciones.')
    out.write(BOLD + f'* Total de iteraciones: ' +ALL_OFF+ f'{iteraciones}'  )
    out.write(BOLD + f'* Norma del gradiente de f: ' +ALL_OFF+ f'{np.linalg.norm(gradiente_f(X,y,beta_new))}' )
    out.write(BOLD + f'* beta_hat:' )
    pprint.pprint(beta_new)
    print("\n")

    return beta_new
```

## Split and Train

```python
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
np.random.seed(3338014)

data =df_proc.to_numpy()
y = data[:,0]
X = data[:,1:]
x_train, x_test, y_train, y_test=train_test_split(X,y,test_size=.2)

# Scale data
scaler = MinMaxScaler()
x_train = scaler.fit_transform(x_train)
x_test = scaler.fit_transform(x_test)
```

```python
%%time
np.random.seed(3338014) # para replicabilidad
beta_hat = gradient_descent(x_train,y_train,max_iter=10**6,reset_lr=False,verbose_n=500)
yhat = clasifica(x_test,beta_hat)

out.write(BOLD + f'* Error de clasificación: ' + ALL_OFF + f'{round(100*sum(abs(y_test-yhat))/len(yhat),2)}%' )
out.write(BOLD + "="*65)
```

    =================================================================
    Método de máximo descenso
    =================================================================
    Iteración: 500 gradiente: 2.5045446E-01, alpha: 7.1790E-02
    Iteración: 1000 gradiente: 1.9681746E-02, alpha: 5.7264E-03
    Iteración: 1500 gradiente: 3.6459555E-03, alpha: 1.0611E-03
    Iteración: 2000 gradiente: 4.4262414E-04, alpha: 1.2901E-04
    Iteración: 2500 gradiente: 2.3181024E-05, alpha: 6.7516E-06

    ** Resultados Finales
    * Total de iteraciones: 2964
    * Norma del gradiente de f: 9.756061172576794e-07
    * beta_hat:
    array([-10.36242902,  16.25102342,  -3.90446862,   0.60600272,
             0.77744891,   4.55476501,  -0.91897329,   3.3866912 ,
             3.76095218,   3.9617985 ,   4.98358393,   6.21113331])

    * Error de clasificación: 4.55%
    =================================================================
    CPU times: user 650 ms, sys: 47.8 ms, total: 697 ms
    Wall time: 660 ms

```python
%%time
# Método de Newton
np.random.seed(3338014) # para replicabilidad
beta_hat = gradient_descent(x_train,y_train, method="newton",max_iter = 10**5,verbose_n = 20)
yhat = clasifica(x_test,beta_hat)

out.write(BOLD + f'* Error de clasificación: ' + ALL_OFF + f'{round(100*sum(abs(y_test-yhat))/len(yhat),2)}%' )
out.write(BOLD + "="*65)
```

    =================================================================
    Método de Newton
    =================================================================
    Iteración: 20 gradiente: 4.3186382E-01, alpha: 1.0000E+00
    Iteración: 40 gradiente: 6.6389532E-04, alpha: 7.0697E-03
    Iteración: 60 gradiente: 5.1340283E-05, alpha: 5.6392E-04
    Iteración: 80 gradiente: 2.6953093E-06, alpha: 2.9513E-05

    ** Resultados Finales
    * Total de iteraciones: 87
    * Norma del gradiente de f: 1.1609129137769218e-06
    * beta_hat:
    array([-10.36243506,  16.25103288,  -3.90446995,   0.60600623,
             0.77744971,   4.55476817,  -0.91897444,   3.3866927 ,
             3.76095382,   3.96180007,   4.98358595,   6.21113592])

    * Error de clasificación: 4.55%
    =================================================================
    CPU times: user 177 ms, sys: 16.6 ms, total: 194 ms
    Wall time: 71.7 ms

```python
%%time
# Método BFSG
np.random.seed(333814) # para replicabilidad
beta_hat = gradient_descent(x_train,y_train, method = "bfsg",max_iter = 10**6,lr=1,verbose_n = 10000)
yhat = clasifica(x_test,beta_hat)

out.write(BOLD + f'* Error de clasificación: ' + ALL_OFF + f'{round(100*sum(abs(y_test-yhat))/len(yhat),2)}%' )
out.write(BOLD + "="*65)
```

    =================================================================
    Método de BFGS
    =================================================================
    Iteración: 10000 gradiente: 7.0627661E-02, alpha: 2.0276E-02
    Iteración: 20000 gradiente: 3.6716352E-03, alpha: 1.0611E-03
    Iteración: 30000 gradiente: 1.9548109E-04, alpha: 5.5533E-05
    Iteración: 40000 gradiente: 2.3716148E-05, alpha: 6.7516E-06

    ** Resultados Finales
    * Total de iteraciones: 47837
    * Norma del gradiente de f: 1.1624129428409921e-06
    * beta_hat:
    array([-10.36240975,  16.25097655,  -3.90446056,   0.60599614,
             0.77744804,   4.55475336,  -0.91896656,   3.38668298,
             3.76094818,   3.96179347,   4.98357503,   6.21112378])

    * Error de clasificación: 4.55%
    =================================================================
    CPU times: user 16.6 s, sys: 810 ms, total: 17.4 s
    Wall time: 19 s

## Descenso del gradiente estocastico

Calculamos la función de riesgo empírico como la esperanza de la funcion de perdida evaluada sobre todos los puntos del dominio.

$$L_{emp}=\frac{1}{m} \sum^{m}_{i=1} y_i log(\mu_i) + (1-y_i) log(1-\mu_i)$$

$$ \mu_i = (1+e^{-\beta^T x_i})^{-1}= \sigma(\beta^T x_i)$$

Y el gradiente de la función de riesgo esta dado por:

$$\nabla L=\frac{dL}{d\mu_i} =\frac{1}{m} \sum^{m}_{i=1} x_i(\mu_i-y_i)$$

```python
def SGD(X,y,batch_size,verbose_n=100,max_iter=10**5):

     # Se revisa que los parámetros de entrada sean congruentes con la funcionalidad
    m,p = X.shape
    if y.shape[0]!= m:
        sys.exit('Error:  El número de renglones de X debe ser igual al número de entradas del vector y.')

    # Inicializa
    m=X.shape[0]
    epsilon = 10**(-6)
    beta = np.random.normal(0,1,X.shape[1])
    step_size=.01
    iteraciones = 0
    epoca=0
    ipe=int(m/batch_size)#iteraciones por epoca

    # Primera iteracion
    index=batch(m,batch_size)
    x_lote=X[index,:]
    y_lote = y[index]
    beta_new = beta - step_size * gradiente_riesgo_empirico(x_lote,y_lote,beta)

    perdida=riesgo_empirico(X,y,beta)
    error=error_train(X,y,beta)

    while iteraciones<max_iter:
        iteraciones +=1
        beta = beta_new
        index=batch(m,batch_size)
        x_lote=X[index,:]
        y_lote = y[index]
        beta_new = beta - step_size * gradiente_riesgo_empirico(x_lote,y_lote,beta)
        #print("iteraciones2=",iteraciones)
        if iteraciones%10000==0:
            epoca+=1
            loss=riesgo_empirico(X,y,beta)
            perdida=np.append(perdida,loss)
            err=error_train(X,y,beta)
            error=np.append(error, error_train(x_test,y_test,beta_hat))
            print(f'loss:{loss:.4}, epoca:{epoca}, iter:{iteraciones}')
    print("Nº DE INTERACIONES: ",iteraciones)
    return beta_new,perdida,error
```

```python
%%time
np.random.seed(3338014) # para replicabilidad
# Método de SGD
for batch_size in range(9,10):

    print("*========================================================================*")
    print("Tamaño de minilote= ",batch_size)
    beta_hat, loss, error = SGD(x_train, y_train, max_iter=10**5, batch_size=batch_size)
    yhat = clasifica(x_test,beta_hat)
    titulo="Batch size = "+ str(batch_size)
    graf_loss_err(loss,error,titulo)
    print("beta_hat=", beta_hat)
    print("Error de clasificacion=",round(100*sum(abs(y_test-yhat))/len(yhat),2),"%")
    print("\n")
```

```python
import multiprocessing
import time
from dask.distributed import Client, progress
client = Client()
```

```python
client
```

Client

Scheduler: tcp://127.0.0.1:59415

Dashboard: http://127.0.0.1:59416/status

Cluster

Workers: 4

Cores: 8

Memory: 8.59 GB

```python
# paso 1. Dividir el dominio en partes iguales
cores = multiprocessing.cpu_count() # cpus disponibles
#n_subint = int(density_p/p) # número de puntos o nodos en cada core o cpu
```

```python
def construye_indices(ids,m,cores):
    '''
    Argumentos:
    ----------
    * ids: Identificador del core dónde se está corriendo el task.
    * m (array): Numero total de puntos de entrenamiento.
    * cores (int) : Número de cores o cpus disponibles

    Salidas:
    -------
    * (index_from, index_to): Rango de indices que se seleccionan del verctor de permutaciones
    '''

    tamano_int = int(m/cores) #tamaño de cada sub intervalo.
    index_from = ids*tamano_int #construyen los subintervalo
    index_to = index_from + tamano_int
    if ids==(cores-1): index_to=m
    return (index_from,index_to)

def evalua_gradiente(intervalo,perm,X,y,beta):
    """
    Función que evalúa el gradiente del riesgo empirico para cada conjunto de indices de permutaciones
    Argumentos:
    ----------
    * intervalo: Intervalo de indices del vector perm a utilizar
    * perm: Vector de permutaciones
    * X: Puntos de entrenamiento
    * y: Etiquetas de los puntos de entrenamiento
    * beta: Vector de parametros a optimizar

    Salidas:
    * Evaluacion del gradiente del riesgo empirico para un mini lote
    --------

    """
    index=perm[intervalo[0]:intervalo[1]]
    x_lote = X[index,:]
    y_lote = y[index]
    gradiente=gradiente_riesgo_empirico(x_lote,y_lote,beta)
    return gradiente

```

```python
def SGD_paralelo(X,y,verbose_n=100,max_iter=10**5):

     # Se revisa que los parámetros de entrada sean congruentes con la funcionalidad
    m,p = X.shape
    if y.shape[0]!= m:
        sys.exit('Error:  El número de renglones de X debe ser igual al número de entradas del vector y.')

    # Inicializa
    m=X.shape[0]
    epsilon = 10**(-6)
    beta = np.random.normal(0,1,X.shape[1])
    step_size=.01
    iteraciones = 0
    epoca=0

    # Primera iteracion
    perm=n_batch(m)
    #calcula los indices
    indices = client.map(construye_indices,range(cores),
                **{'m':m,'cores':cores})

    #evalua el gradiente en cada batch en paralelo
    grad_riesgo_empirico=client.map(evalua_gradiente,indices,
                **{'perm':perm,'X':X,'y':y,'beta':beta})

    results=client.gather(grad_riesgo_empirico)
    actualiza=sum(results)

    beta_new = beta - step_size * actualiza
    #gradiente_riesgo_empirico(x_lote,y_lote,beta)

    perdida=riesgo_empirico(X,y,beta)
    error=error_train(X,y,beta)

    # while ((np.linalg.norm(gradiente_f(X,y,beta_new)) > epsilon) & (iteraciones < max_iter)):
    # while abs(f(X,y,beta) - f(X,y,beta_new)) > epsilon:
    while iteraciones<max_iter:
        iteraciones +=1
        beta = beta_new
        perm=n_batch(m)
        #calcula los indices
        indices = client.map(construye_indices,range(cores),**{'m':m,'cores':cores})
        #evalua el gradiente en cada batch en paralelo
        grad_riesgo_empirico=client.map(evalua_gradiente,indices,
                     **{'perm':perm,'X':X,'y':y,'beta':beta})
        #gather results
        results=client.gather(grad_riesgo_empirico)
        actualiza=sum(results)

        beta_new = beta - step_size * actualiza
        end_time = time.time()

        if iteraciones%100==0:
            epoca+=1
            loss=riesgo_empirico(X,y,beta)
            perdida=np.append(perdida,loss)
            err=error_train(X,y,beta)
            error=np.append(error, error_train(x_test,y_test,beta_hat))
            print(f'loss:{loss:.4}, epoca:{epoca}, iter:{iteraciones}')
        #print("iteraciones3=",iteraciones)
    print("Nº DE INTERACIONES: ",iteraciones)
    return beta_new,perdida,error
```

```python
%%time
# Método de SGD
np.random.seed(3338014)
for batch_size in range(9,10):

    print("*========================================================================*")
    #print("Tamaño de minilote= ",batch_size)
    beta_hat, loss, error = SGD_paralelo(x_train, y_train, max_iter=10**4)
    yhat = clasifica(x_test,beta_hat)
    titulo="Batch size = "+ str(batch_size)
    graf_loss_err(loss,error,titulo)
    print("beta_hat=", beta_hat)
    print("Error de clasificacion=",round(100*sum(abs(y_test-yhat))/len(yhat),2),"%")
    print("\n")
```

## Resultados

### Rendimiento de los algoritmos

# Resultados del modelo logístico

- El problema de regresión logística es una extensión del problema de regresión lineal aplicada a clasificación.

- La probabilidad de que un paciente muera a causa del virus del ébola para este modelo
se define de la siguiente forma:

$$
P\left(y=1\right)=\frac{e^{\beta^{T}x}}{1+e^{\beta^{T}x}}\label{eq:prob_y1a}
$$

ó:

$$
P\left(y=1\right)=\frac{e^{\beta_{0}+\beta_{1}x_{1}+\ldots+\beta_{p}x_{p}}}{1+e^{\beta_{0}+\beta_{1}x_{1}+\ldots+\beta_{p}x_{p}}}\label{eq:prob_y1b}
$$

- No estamos considerando el término equivalente al intercepto en el caso de regresión
lineal (i.e $\beta_{0}=0)$.
- No se afectan de forma lineal las probabilidades.
- Interpretaciones: espejar el componente lineal de la ecuación.
- **logit**: log del cociente entre la probabilidad de morir y la probabilidad de sobrevivir al virus (ratio de
probabilidades: línea recta, dependiente del conjunto de covariables, $x$):

$$
\lambda=\log\left\{ \frac{P\left(y=1\right)}{1-P\left(y=1\right)}\right\} =\beta_{1}x_{1}+\ldots+\beta_{p}x_{p}\label{eq:param_logit}
$$

- No asume homocedasticidad: modelo dicotómico.
- La interpretación de los resultados del modelo de regresión logística: $exp(\lambda)$.
- Es el indicador de variación en el ratio de probabilidades como resultado del cambio en una unidad en la predicción.

**Resultados con métodos no estocásticos**

$$
ratio\:probs=\exp(\log\{-10.95x_{1}+18.88x_{2}-4.42x_{3}+1.14x_{4}+1.40x_{5}+4.76x_{6}-0.94x_{7}+3.006x_{8}+3.73x_{9}+4.45x_{10}+5.20x_{11}+6.38x_{12}
$$

donde $x_{1},\ldots,x_{12}$ corresponden a: cycle threshold,
temperatura, dolor de cabeza, sangrado, diarrea, vómito, presencia
de PABD, debilidad, edad hasta 22 años, edad entre 23 y 36 años, edad
entre 37 y 45 años, y edad mayor a 45 años, respectivamente.

- Se debe calcular un valor de referencia con el cual se pueda comparar.

- Caso sin ninguna complicación de salud adicional al virus del ébola, y su edad se encuentra en un rango de 0 años a 22 años

Es decir $x_{i}=0$ para $i=3,4,5,6,7,8$ y $x_{9}=1$, por lo tanto:

$$
ratio\:probs=\exp\left(\log\left\{ -10.95x_{1}+18.88x_{2}+3.73\right\} \right)
$$

Reemplazamos en el ratio de probabilidades:

$$
ratio\:probs_{0}=\exp\left(\log\left\{ -10.95\bar{x}_{1}+18.88\bar{x}_{2}+3.73\right\} \right)=\exp\left(\log\left\{ -10.95(25.72)+18.88(37.25)+3.73\right\} \right)=425.75
$$

**Ejemplo:**

¿Cuál es el efecto de que la persona tenga sangrado sobre la probabilidad de que la persona
muera por presencia del virus del ébola, debemos calcular el ratio
de probabilidades ahora con $x_{4}=1$ ?:

$$
ratio\:probs_{1}=\exp\left(\log\left\{ -10.95\bar{x}_{1}+18.88\bar{x}_{2}+1.14+3.73\right\} \right)=\exp\left(\log\left\{ -10.95(25.72)+18.88(37.25)+1.14+3.73\right\} \right)=426.8
$$

Ahora, si calculamos el ratio entre $ratio\:probs_{0}$ y $ratio\:probs_{1}$, tenemos:

$$
\frac{ratio\:probs_{1}}{ratio\:probs_{0}}=\frac{426.89}{425.75}=1.0026\label{eq:delt_odd}
$$

- Puesto que esta ecuación es mayor a 1, esto indica que la probabilidad
de la variable dependiente incrementa.
- Una persona con edad de 0 años a 22 años que tiene ébola, tener sangrado, afecta positivamente su probabilidad de morir.

```python

```


Mathematical Optimization for Engineers

Lab 11 - Full discretization method

In this exercise, we want to solve the van der Pol oscillator optimal control problem using the full discretization approach.
The problem with state constraint is as follows:
$\begin{align}
\min_{u(t)} \int_{t_{0}}^{t_{f}} x_1^2(t) &+ x_2^{2}(t) + u(t)^2 dt\\
s.t.  \dot{x} &= f(x(t),u(t))\\
x(t_{0}) &= x_{0} \\
x_1(t) &\geq - 0.4 \quad \forall t \in [t_{0},t_{f}]\\
-0.3 &\leq u(t) \leq 1.0\\
\end{align}
$

For the full discretization appraoch, the differential equations are discretized usually by implicit Runge-Kutta methods. In this exercise, we will use the implicit Euler method due to its simplicity.

### Imports

```python
import numpy as np

from scipy.optimize import minimize
from scipy.optimize import Bounds
from scipy.optimize import NonlinearConstraint

from matplotlib import pyplot as plt
```

### Right-hand side function of ODE

```python
def eval_rhs(x, u):
    # your code here

    return f
```

### Constraints for optimization - discretized ODE

```python
def cons(y, x0, finalTime, M, n_u):
    # time step h (equidistant here)
    # your code here
    # number of states n_x
    # your code here
    ceq = np.zeros(n_x*M)

    # backward Euler y_(k+1) = y_(k) + h.f(t_(k+1),y_(k+1))

    # evalue RHS first time step f1
    # your code here

    # first element of constraint ceq[0:n_x]
    # your code here

    # int(z) for z in [] just converts all elements in the list to integer
    # it's an implementation detail because python only allows integer indices

    # second time step onward
    for i in # your code here. #Hint: np.arange

        # populate equality constraints using the backward Euler method
        # You could define indicies needed for the states and constraints
        # your code here

    return ceq
```

### objective

```python
def objective(y, x0):
    # define objective
    # your code here
    return obj
```

### Setting up optimization problem and solve it

```python
n_x = 3 # dimension of state vector x
n_u = 1 # dimension of control vector u

M=50 # Number of discretization intervals

x1_0 = 0 # initial value of x1
x2_0 = 1 # initial value of x2
x3_0 = 0 # initial value of x3

x_initial = [0.0, 1.0, 0.0]
finalTime = 5 # final time

x1_min = # your code here # lower bound on x1
u_min = # your code here # lower bound on u
u_max = # your code here # upper bound on u

n = # your code here # number of optimization variables
iu = (np.arange(3,n,4)).astype(int) # indices of control variables
ix1 = [int(z)-3 for z in iu] # indices of state x1
ix2 = [int(z)-2 for z in iu] # indices of state x2
ix3 = [int(z)-1 for z in iu] # indices of state x3

lb = -float("inf")*np.ones(n) # initialize lower bounds on all variables
ub = float("inf")*np.ones(n) # initialize upper bounds on all variables

lb[iu] = # your code here) # lower bound of control
ub[iu] = # your code here # upper bound of control
lb[ix1] = # your code here # lower bound on x1

x_guess = np.zeros(n)  # initial guess of optimzation variable

x_guess[ix1] = x1_0  # initial guess for state x1
x_guess[ix2] = x2_0  # initial guess for state x2
x_guess[ix3] = x3_0  # initial guess for state x3

x_guess[iu] = 0.0  # initial guess for control u

bounds = Bounds(lb, ub)

consWithInitial = lambda y: cons(y, x_initial, finalTime, M, n_u)

nonlinear_constraints = NonlinearConstraint(consWithInitial, 0, 0)
# call optimization with bounds, constraints and objective
# your code here

print('Optimal function value is')
print(res.fun)
```

### Plot trajectories

```python
xopt = res.x

t = np.linspace(0, finalTime, M+1)
x1 = np.append([x1_0],list(xopt[np.arange(0,len(xopt),4)]))
x2 = np.append([x2_0],list(xopt[np.arange(1,len(xopt),4)]))
x3 = np.append([x3_0],list(xopt[np.arange(2,len(xopt),4)]))
u = np.append([0.0],list(xopt[np.arange(3,len(xopt),4)]))

plt.figure()
ax1 = plt.axes(xlim=(-4,4), ylim=(-4, 4))
plt.subplot(311)

plt.subplot(221)
plt.plot(t, x1)
plt.title('x1')

plt.subplot(222)
plt.plot(t, x2)
plt.title('x2')

plt.subplot(223)
plt.plot(t, x3)
plt.title('x3')
#
plt.subplot(224)
plt.plot(t, u)
plt.title('u')

plt.tight_layout()

plt.show()

```

```python

```


# Интегралы

**Интеграл** - это "действие" обратное производной

$v(t)=t(8-t)$

$s(t)=\int v(t) dt$

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from sympy import *

def showPlot(x_start, x_max, step, callback, title='Function', x_title='X', y_title='Y'):
    x = np.arange(x_start, x_max, step)
    y = callback(x)

    plt.plot(x, y)
    plt.xlabel(x_title)
    plt.ylabel(y_title)
    plt.title(title)
    plt.grid(True)
    plt.show()

def value_of_expr(x_vector, symbol, expr):
    y = np.empty_like(x_vector)
    for index, element  in enumerate(x_vector):
        y[index] = (expr.subs(symbol, element))
    return y

init_printing()
```

```python
t, v = symbols('t v')

vt = t*(8-t)
showPlot(0, 8, 0.01, lambda i: value_of_expr(i, t, vt), title='Velocity', x_title='time', y_title='velocity')
vt
```

```python
distance = integrate(vt, t)
integral
```

```python
showPlot(0, 8, 0.01, lambda i: value_of_expr(i, t, distance), title='Distance', x_title='time', y_title='distance')
```

# Таблица интегралов

# Определенные интегралы

$\int_{a}^{b} f(x) dx = F(X) |^{b}_{a}=F(b)-F(a)$

```python
distance = integrate(vt, (t, 0, 8))
N(distance)
```

```python
showPlot(0, 8, 1, lambda i: value_of_expr(i, t, vt), title='Velocity', x_title='time', y_title='velocity')
```

```python
i = np.arange(0, 8, .1)
y = value_of_expr(i, t, vt)
print(y)
print(sum(y*.1))
```

    [ 0.    0.79  1.56  2.31  3.04  3.75  4.44  5.11  5.76  6.39  7.    7.59
      8.16  8.71  9.24  9.75 10.24 10.71 11.16 11.59 12.   12.39 12.76 13.11
     13.44 13.75 14.04 14.31 14.56 14.79 15.   15.19 15.36 15.51 15.64 15.75
     15.84 15.91 15.96 15.99 16.   15.99 15.96 15.91 15.84 15.75 15.64 15.51
     15.36 15.19 15.   14.79 14.56 14.31 14.04 13.75 13.44 13.11 12.76 12.39
     12.   11.59 11.16 10.71 10.24  9.75  9.24  8.71  8.16  7.59  7.    6.39
      5.76  5.11  4.44  3.75  3.04  2.31  1.56  0.79]
    85.32000000000002

```python

```


# Berechnung der Durchschnittlichen Spiellänge mittels Markov-Ketten
### Import der benötigten Bibliotheken

```python
import sympy as sy

```

### Definition der Wahrscheinlichkeiten der einzelnen Würfelwerte

```python
P_4D2 = {
    "dice": sy.nsimplify(1/16, tolerance=0.001, rational=True),
    "diceI": sy.nsimplify(1/4, tolerance=0.001, rational=True),
    "diceII": sy.nsimplify(3/8, tolerance=0.001, rational=True),
    "diceIII": sy.nsimplify(1/4, tolerance=0.001, rational=True),
    "diceIV": sy.nsimplify(1/16, tolerance=0.001, rational=True)
}
```

### Definition der Übergangsmatrizen

```python
def RundenMatrix(dice, diceI, diceII, diceIII, diceIV):
    return sy.Matrix(
        [[dice,  diceI,  diceII,  diceIII,  diceIV * dice,  diceIV * diceI,  diceIV * diceII,  diceIV * diceIII,  diceIV * diceIV * dice,  diceIV * diceIV * diceI,  diceIV * diceIV * diceII,  diceIV * diceIV * diceIII,  diceIV * diceIV * diceIV, 0, 0, 0],
         [0,  dice,  diceI,  diceII,  diceIII * dice,  diceIV + diceIII * diceI,  diceIII * diceII,  diceIII * diceIII,  diceIII * diceIV *
            dice,  diceIII * diceIV * diceI,  diceIII * diceIV * diceII,  diceIII * diceIV * diceIII,  diceIII * diceIV * diceIV, 0, 0, 0],
            [0, 0,  dice,  diceI,  diceII * dice,  diceIII + diceII * diceI,  diceIV + diceII * diceII,  diceII * diceIII,  diceII * diceIV *
                dice,  diceII * diceIV * diceI,  diceII * diceIV * diceII,  diceII * diceIV * diceIII,  diceII * diceIV * diceIV, 0, 0, 0],
            [0, 0, 0,  dice,  diceI * dice,  diceII + diceI * diceI,  diceIII + diceI * diceII,  diceIV + diceI * diceIII,  diceI *
                diceIV * dice,  diceI * diceIV * diceI,  diceI * diceIV * diceII,  diceI * diceIV * diceIII,  diceI * diceIV * diceIV, 0, 0, 0],
            [0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII,  diceIV * dice,  diceIV *
                diceI,  diceIV * diceII,  diceIV * diceIII,  diceIV * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII * dice,  diceIV + diceIII *
                diceI,  diceIII * diceII,  diceIII * diceIII,  diceIII * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0,  dice,  diceI,  diceII * dice,  diceIII + diceII * diceI,
                diceIV + diceII * diceII,  diceII * diceIII,  diceII * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0,  dice,  diceI * dice,  diceII + diceI * diceI, diceIII +
                diceI * diceII,  diceIV + diceI * diceIII,  diceI * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,
                diceII,  diceIII,  diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,
                diceII,  diceIII,  diceIV, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII,
                diceIV*(dice + diceII + diceIII + diceIV),  diceIV * diceI],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII *
                (dice + diceII + diceIII + diceIV),  diceIV + (diceIII * diceI)],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice + diceIV,  diceI,  diceII *
                (dice + diceII + diceIII + diceIV),  diceIII + (diceII * diceI)],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice + diceIII + diceIV,
                diceI*(dice + diceII + diceIII + diceIV),  diceII + (diceI * diceI)],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                dice + diceII + diceIII + diceIV,  diceI],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]])

def SchritteMatrix(dice, diceI, diceII, diceIII, diceIV):
    return sy.Matrix([
        [dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII,  diceIV, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII,  diceIV, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII, diceIV],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice +
            diceIV,  diceI,  diceII,  diceIII],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            dice+diceIII + diceIV,  diceI,  diceII],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            dice+diceII + diceIII + diceIV,  diceI],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            dice + diceI + diceII + diceIII + diceIV],
    ])

def SchritteMatrixNichtGenauInsZiel(dice, diceI, diceII, diceIII, diceIV):
    return sy.Matrix([
        [dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII,  diceIV, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII,  diceIV, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII ,diceIV],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice, diceI,  diceII, diceIII +diceIV],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII+ diceIII+ diceIV],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI+  diceII+ diceIII+ diceIV],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice+  diceI+  diceII+ diceIII+ diceIV], ])

```

### Festlegen welcher Würfel verwendet wird

```python
M_S = SchritteMatrix(**P_4D2)
M_R = RundenMatrix(**P_4D2)
# M_S = SchritteMatrix(**D4)
# M_R = RundenMatrix(**D4)

```

### Überprüfung ob die Zeilensumme jeweils 1 ist

```python
print("Zeilensumme Schritte Matrix:", [sum(M_S.row(i)) for i in range(16)])
print("Zeilensumme Runden Matrix:",[sum(M_R.row(i)) for i in range(16)])
```

    Zeilensumme Schritte Matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    Zeilensumme Runden Matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

### Löschen der letzten Spalte
Achtung findet inplace statt!

```python
M_S.col_del(-1)
M_R.col_del(-1)

```

### Löschen der letzten Zeile

Achtung findet inplace statt!

```python
M_S.row_del(-1)
M_R.row_del(-1)

```

### Definition der 15x15 - Einheitsmatrix

```python
I = sy.eye(15)
```

### Berechnung der Ergebnismatrizen

```python
E_S = (I-M_S)**-1
E_R = (I-M_R)**-1
```

### Berechnng der jeweils ersten Zeilensumme

```python
v=sy.Matrix([[1]+[0]*14])
ZS_S= sum(v*E_S)
ZS_R= sum(v*E_R)

```

## Ausgabe der Ergebnisse

```python
print("Schritte gerundet:", float(ZS_S))
print("Schritte exakt für Latex:", sy.latex(ZS_S))
print("Schritte exakt:")

ZS_S

```

    Schritte gerundet: 9.867846767445288
    Schritte exakt für Latex: \frac{1280316980941868}{129746337890625}
    Schritte exakt:

$\displaystyle \frac{1280316980941868}{129746337890625}$

```python
print("Runden gerundet:", float(ZS_R))
print("Runden exakt für Latex:", sy.latex(ZS_R))
print("Runden exakt:")
ZS_R
```

    Runden gerundet: 8.391927856222578
    Runden exakt für Latex: \frac{1088821907187203}{129746337890625}
    Runden exakt:

$\displaystyle \frac{1088821907187203}{129746337890625}$

```python
print("Schritte/Runden gerundet:", float(ZS_S/ZS_R))
print("Schritte/Runden exakt für Latex:", sy.latex(ZS_S/ZS_R))
print("Schritte/Runden exakt:")
ZS_S/ZS_R

```

    Schritte/Runden gerundet: 1.175873641493274
    Schritte/Runden exakt für Latex: \frac{1280316980941868}{1088821907187203}
    Schritte/Runden exakt:

$\displaystyle \frac{1280316980941868}{1088821907187203}$

## Berechnung für die Markov-Kette wenn das Ziel nicht genau getroffen werden muss

```python

M_U = SchritteMatrixNichtGenauInsZiel(**P_4D2)

```

```python
print("Zeilensumme Schritte Matrix:", [sum(M_U.row(i)) for i in range(16)])

```

    Zeilensumme Schritte Matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

```python
M_U.col_del(-1)
M_U.row_del(-1)
```

```python
I = sy.eye(15)
E_U = (I-M_U)**-1
v=sy.Matrix([[1]+[0]*14])
ZS_U= sum(v*E_U)
```

```python
v*E_U*sy.ones(15,1)

```

$\displaystyle \left[\begin{matrix}\frac{3448413510068397376}{437893890380859375}\end{matrix}\right]$

```python
print("Schritte gerundet:", float(ZS_U))
print("Schritte exakt für Latex:", sy.latex(ZS_U))
print("Schritte exakt:")

ZS_U

```

    Schritte gerundet: 7.874997997960489
    Schritte exakt für Latex: \frac{3448413510068397376}{437893890380859375}
    Schritte exakt:

$\displaystyle \frac{3448413510068397376}{437893890380859375}$

# Vergleichswerte mit verschiedenen Würfeln

```python
def berechneRunden(würfel):
    M_R = RundenMatrix(**würfel)
    # print("Zeilensumme Runden Matrix:", [sum(M_R.row(i)) for i in range(16)])
    M_R.col_del(-1)
    M_R.row_del(-1)
    I = sy.eye(15)
    E_R = (I-M_R)**-1
    v = sy.Matrix([[1]+[0]*14])
    ZS_R = sum(v*E_R)
    return ZS_R

```

```python
def berechneSchritte(würfel):
    M_S = SchritteMatrix(**würfel)
    # print("Zeilensumme Schritte Matrix:", [sum(M_S.row(i)) for i in range(16)])
    M_S.col_del(-1)
    M_S.row_del(-1)
    I = sy.eye(15)
    E_S = (I-M_S)**-1
    v = sy.Matrix([[1]+[0]*14])
    ZS_S = sum(v*E_S)
    return ZS_S

```

```python
def ausgabe(name, schritte, runden):
    # print(name)
    # print("Runden:", runden," = ", float(runden))
    # print("Schritte:", schritte," = ", float(schritte))
    # print(name,"&$", sy.latex(schritte),"$&$", sy.latex(runden),"$\\\\")
    # print(name,"&$", float(schritte),"$&$", float(runden),"$\\\\")
    print("{}&${:.3f}$&${:.3f}$\\\\".format(name, float(schritte), float(runden)))

```

```python
P_D4 = {
    "dice": sy.nsimplify(0, tolerance=0.001, rational=True),
    "diceI": sy.nsimplify(1/4, tolerance=0.001, rational=True),
    "diceII": sy.nsimplify(1/4, tolerance=0.001, rational=True),
    "diceIII": sy.nsimplify(1/4, tolerance=0.001, rational=True),
    "diceIV": sy.nsimplify(1/4, tolerance=0.001, rational=True),
}
P_D2 = {
    "dice": sy.nsimplify(1/2, tolerance=0.001, rational=True),
    "diceI": sy.nsimplify(1/2, tolerance=0.001, rational=True),
    "diceII": sy.nsimplify(0, tolerance=0.001, rational=True),
    "diceIII": sy.nsimplify(0, tolerance=0.001, rational=True),
    "diceIV": sy.nsimplify(0, tolerance=0.001, rational=True),
}
P_2D2 = {
    "dice": sy.nsimplify(1/4, tolerance=0.001, rational=True),
    "diceI": sy.nsimplify(2/4, tolerance=0.001, rational=True),
    "diceII": sy.nsimplify(1/4, tolerance=0.001, rational=True),
    "diceIII": sy.nsimplify(0, tolerance=0.001, rational=True),
    "diceIV": sy.nsimplify(0, tolerance=0.001, rational=True),
}
P_3D2 = {
    "dice": sy.nsimplify(1/8, tolerance=0.001, rational=True),
    "diceI": sy.nsimplify(3/8, tolerance=0.001, rational=True),
    "diceII": sy.nsimplify(3/8, tolerance=0.001, rational=True),
    "diceIII": sy.nsimplify(1/8, tolerance=0.001, rational=True),
    "diceIV": sy.nsimplify(0, tolerance=0.001, rational=True),
}
P_3D2_0TO4 = {
    "dice": sy.nsimplify(0, tolerance=0.001, rational=True),
    "diceI": sy.nsimplify(3/8, tolerance=0.001, rational=True),
    "diceII": sy.nsimplify(3/8, tolerance=0.001, rational=True),
    "diceIII": sy.nsimplify(1/8, tolerance=0.001, rational=True),
    "diceIV": sy.nsimplify(1/8, tolerance=0.001, rational=True),
}

```

```python
ausgabe("D4",berechneSchritte(P_D4), berechneRunden(P_D4))
ausgabe("D2",berechneSchritte(P_D2), berechneRunden(P_D2))
ausgabe("2D2",berechneSchritte(P_2D2), berechneRunden(P_2D2))
ausgabe("3D2",berechneSchritte(P_3D2), berechneRunden(P_3D2))
ausgabe(r"3D2 0$\to$4", berechneSchritte(P_3D2_0TO4), berechneRunden(P_3D2_0TO4))
ausgabe("4D2",berechneSchritte(P_4D2), berechneRunden(P_4D2))

```

    D4&$8.798$&$7.349$\\
    D2&$30.000$&$27.000$\\
    2D2&$15.750$&$13.497$\\
    3D2&$11.444$&$9.660$\\
    3D2 0$\to$4&$9.256$&$7.613$\\
    4D2&$9.868$&$8.392$\\

```python
[berechneSchritte(d) for d in [P_D2, P_2D2, P_3D2, P_4D2, P_3D2_0TO4, P_D4]]

```

    [30,
     75331762/4782969,
     1108840853928/96889010407,
     1280316980941868/129746337890625,
     417425672509/45097156608,
     9225465/1048576]

```python
[berechneRunden(d) for d in [P_D2, P_2D2, P_3D2, P_4D2, P_3D2_0TO4, P_D4]]

```

    [27,
     88553/6561,
     1871893342281/193778020814,
     1088821907187203/129746337890625,
     10986162935393/1443109011456,
     61648355/8388608]


#3.2 Continunity and Differentiation

##Limits and Continuity

```python
from sympy import *
x = symbols('x')
f = log(x)/x
y = limit(f,x,0)
print('f(x)=', f)
print(y)
```

    f(x)= log(x)/x
    -oo

```python
x = symbols('x')
f = (1/x)**2
y = limit(f,x,2)
print('f(x)=', f)
print(y)
```

    f(x)= x**(-2)
    1/4

```python
x = symbols('x')
f = (5*x**3 - 2*x**2 + 1)/(-3*x**3 + 4*x +8)
y = limit(f,x,0)
print('f(x)=', f)
print(y)
```

    f(x)= (5*x**3 - 2*x**2 + 1)/(-3*x**3 + 4*x + 8)
    1/8

Derivatives

### Single-Variable Case

```python
import sympy as sp
x=sp.Symbol('x')
f=sp.sin(x)+x**2+sp.exp(4*x)
print('f(x) =',f)

df=sp.diff(f,x)
print('First derivative:', df)

ddf=sp.diff(f,x,2)
print('Second derivative:',ddf)
```

    f(x) = x**2 + exp(4*x) + sin(x)
    First derivative: 2*x + 4*exp(4*x) + cos(x)
    Second derivative: 16*exp(4*x) - sin(x) + 2

```python
x=sp.Symbol('x')
f=(5*x**3 + 6*x - 9)/(log(3*x/2))
print('f(x) =',f)

df=sp.diff(f,x)
print('First derivative:', df)

ddf=sp.diff(f,x,2)
print('Second derivative:', ddf)
```

    f(x) = (5*x**3 + 6*x - 9)/log(3*x/2)
    First derivative: (15*x**2 + 6)/log(3*x/2) - (5*x**3 + 6*x - 9)/(x*log(3*x/2)**2)
    Second derivative: (30*x - 6*(5*x**2 + 2)/(x*log(3*x/2)) + (1 + 2/log(3*x/2))*(5*x**3 + 6*x - 9)/(x**2*log(3*x/2)))/log(3*x/2)

### General Case

Partial Derivative

```python
import sympy as sym
x, y = sym.symbols('x y')
f = x**4 + x*y**4
partial_df = f.diff(x, y)
print('Partial derivative:', partial_df)
```

    Partial derivative: 4*y**3

```python
from sympy import symbols, cos, diff
x, y, z = symbols('x y z', real=True)
f = 7*x*y - y*log(z) + z**3 + z**8*y

#differntiating function f  respect to a
print('Partial derivative wrt x', diff(f, x))
print('Partial derivative wrt y', diff(f, y))
print('Partial derivative wrt z', diff(f, z))
```

    Partial derivative wrt x 7*y
    Partial derivative wrt y 7*x + z**8 - log(z)
    Partial derivative wrt z 8*y*z**7 - y/z + 3*z**2

Jacobian

```python
!pip install symengine
```

    Collecting symengine
      Downloading symengine-0.9.0-cp37-cp37m-manylinux2010_x86_64.whl (37.5 MB)
         |████████████████████████████████| 37.5 MB 3.0 MB/s
    Installing collected packages: symengine
    Successfully installed symengine-0.9.0

```python
import symengine
vars = symengine.symbols('x y')
f = symengine.sympify(['x**4 + 3*(y**2)*x', '5*y**2 + 2*x*y + 1'])
J = symengine.zeros(len(f),len(vars))
for i, fi in enumerate(f):
  for j, s in enumerate(vars):
    J[i,j] = symengine.diff(fi, s)
print(J)
```

    [4*x**3 + 3*y**2, 6*x*y]
    [2*y, 2*x + 10*y]

```python
print(symengine.Matrix.det(J))
```

    -12*x*y**2 + (4*x**3 + 3*y**2)*(2*x + 10*y)

```python
import symengine
vars = symengine.symbols('x y z')
f = symengine.sympify(['z*tan(x**2 - y**2)', 'x*y*ln(z/2)'])
J = symengine.zeros(len(f),len(vars))
for i, fi in enumerate(f):
  for j, s in enumerate(vars):
    J[i,j] = symengine.diff(fi, s)
print(J)
```

    [2*x*z*(1 + tan(-x**2 + y**2)**2), -2*y*z*(1 + tan(-x**2 + y**2)**2), -tan(-x**2 + y**2)]
    [y*log((1/2)*z), x*log((1/2)*z), x*y/z]

Chain Rule

```python
import sympy as sym
x = sym.symbols('x')
sym.diff((x**2 - 3*x + 5)**3)
```

$\displaystyle \left(6 x - 9\right) \left(x^{2} - 3 x + 5\right)^{2}$

Directional Derivative

```python
!pip install numdifftools
```

    Collecting numdifftools
      Downloading numdifftools-0.9.40-py2.py3-none-any.whl (99 kB)
         |████████████████████████████████| 99 kB 3.5 MB/s
    Requirement already satisfied: scipy>=0.8 in /usr/local/lib/python3.7/dist-packages (from numdifftools) (1.4.1)
    Requirement already satisfied: numpy>=1.9 in /usr/local/lib/python3.7/dist-packages (from numdifftools) (1.21.5)
    Collecting algopy>=0.4
      Downloading algopy-0.5.7.zip (189 kB)
         |████████████████████████████████| 189 kB 34.2 MB/s
    Requirement already satisfied: statsmodels>=0.6 in /usr/local/lib/python3.7/dist-packages (from numdifftools) (0.10.2)
    Requirement already satisfied: pandas>=0.19 in /usr/local/lib/python3.7/dist-packages (from statsmodels>=0.6->numdifftools) (1.3.5)
    Requirement already satisfied: patsy>=0.4.0 in /usr/local/lib/python3.7/dist-packages (from statsmodels>=0.6->numdifftools) (0.5.2)
    Requirement already satisfied: pytz>=2017.3 in /usr/local/lib/python3.7/dist-packages (from pandas>=0.19->statsmodels>=0.6->numdifftools) (2018.9)
    Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.7/dist-packages (from pandas>=0.19->statsmodels>=0.6->numdifftools) (2.8.2)
    Requirement already satisfied: six in /usr/local/lib/python3.7/dist-packages (from patsy>=0.4.0->statsmodels>=0.6->numdifftools) (1.15.0)
    Building wheels for collected packages: algopy
      Building wheel for algopy (setup.py) ... done
      Created wheel for algopy: filename=algopy-0.5.7-py3-none-any.whl size=107608 sha256=2fd27c631bb35cb9dc8a0a0cc8393797fd89190b72ffab52e9e6a709d0bb900a
      Stored in directory: /root/.cache/pip/wheels/a8/fd/06/21dc6aba9f680fdbde386c06b1a8a1cf3d9a5f6bd96e31dcd4
    Successfully built algopy
    Installing collected packages: algopy, numdifftools
    Successfully installed algopy-0.5.7 numdifftools-0.9.40

```python
import numpy as np
import numdifftools as nd
vec = np.r_[1, 2]
f = lambda x: (5*x**3 + 6*x - 9)/(3*x**3/2)
re, info = nd.directionaldiff(f, [2, -3], vec, full_output=True)
print('The directional derivative of f at point (2,-3) in the direction vector (1,2) is', re)
```

    The directional derivative of f at point (2,-3) in the direction vector (1,2) is [0.0559017  0.46377706]

```python
print(info)
```

    info(f_value=array([3.58333333, 4.        ]), error_estimate=array([2.07216189e-13, 4.87776486e-13]), final_step=array([0.0078125, 0.0078125]), index=array([12, 13]))

Hessian

```python
import numdifftools.nd_algopy as nda
f = lambda x: (5*x[0]**3 + 6*x[1] - 9)/(3*x[0]**3/2)
hess_f = nda.Hessian(f)
hess = hess_f([1,1]) #minimize at (1,1)
hess
```

    array([[-24., -12.],
           [-12.,   0.]])

##Taylor’s Theorem

```python
import math
def tay_series_in_x(n):
    x = math.pi/4
    tay_se = 0
    for i in range(n):
        tay_se += ((-1)**n * (x)**(2*n+1)) / np.math.factorial(2*n+1)
    return tay_se
```

```python
tay_series_in_x(4)
```

    1.253446756151248e-06

#3.3 Unconstrained Optimization

##Necessary and Sufficent Conditions of Local Minimizers

Local Minimizer

```python
from scipy.optimize import minimize
f = lambda x: (5*x[0]**3 + 6*x[0]**2 + x[0] - 9)
x0 = [1, 2.1, 4]
loc_min = minimize(f, x0, method='Nelder-Mead')
loc_min
```

     final_simplex: (array([[-0.0945155 ,  2.75647652,  4.90846093],
           [-0.0944733 ,  2.75646613,  4.90839586],
           [-0.09452481,  2.75649113,  4.90847296],
           [-0.0944599 ,  2.75644522,  4.908496  ]]), array([-9.04513804, -9.04513804, -9.04513804, -9.04513804]))
               fun: -9.045138041306283
           message: 'Optimization terminated successfully.'
              nfev: 96
               nit: 49
            status: 0
           success: True
                 x: array([-0.0945155 ,  2.75647652,  4.90846093])

Global Minimizer

```python
from scipy.optimize import basinhopping
f = lambda x: (5*x[0]**3 + 6*x[0]**2 + x[0] - 9)
x0 = [1]
glo_min = basinhopping(f, x0)
glo_min
```

                            fun: -1.6849058174187942e+26
     lowest_optimization_result:       fun: -1.6849058174187942e+26
     hess_inv: array([[1]])
          jac: array([0.])
      message: 'Optimization terminated successfully.'
         nfev: 3
          nit: 0
         njev: 1
       status: 0
      success: True
            x: array([-3.22999519e+08])
                        message: ['requested number of basinhopping iterations completed successfully']
          minimization_failures: 0
                           nfev: 375
                            nit: 100
                           njev: 125
                              x: array([-3.22999519e+08])

##Convexity and Global Minimizers

###Global Minimizers of Convex Functions

```python
from scipy.optimize import minimize
from numpy.random import rand
import sympy as sp
def func(x):
	return sp.sin(x[0])+x[1]**2
def derivative(x):
	return np.array([np.cos(x[0]), 2*x[1]])
a, b = -3,3
pt = a + rand(2) * a - (b + 1/2)
result = minimize(func, pt, method='BFGS', jac=derivative)
result
```

          fun: -0.9999999999999819
     hess_inv: array([[1.00139636e+00, 5.28874735e-06],
           [5.28874735e-06, 4.99776051e-01]])
          jac: array([-1.18120478e-07, -2.11052302e-07])
      message: 'Optimization terminated successfully.'
         nfev: 6
          nit: 5
         njev: 6
       status: 0
      success: True
            x: array([-7.85398175e+00, -1.05526151e-07])

```python
print('Status : %s' % result['message'])
print('Total Evaluations: %d' % result['nfev'])
solution = result['x']
evaluation = func(solution)
print('Solution: f(%s) = %.5f' % (solution, evaluation))
```

    Status : Optimization terminated successfully.
    Total Evaluations: 6
    Solution: f([-7.85398175e+00 -1.05526151e-07]) = -1.00000

```python
def func(x):
	return sp.sin(x[0])+x[0]**2
def derivative(x):
	return np.array([np.cos(x[0]), 2*x[0]])
x0=rand(2)
result = minimize(func, x0, method='Newton-CG', jac=derivative)
result
```

         fun: 1.80937198308175
         jac: None
     message: "Warning: CG iterations didn't converge.  The Hessian is not positive definite."
        nfev: 1
        nhev: 0
         nit: 0
        njev: 81
      status: 3
     success: False
           x: array([0.98732741, 0.73207205])

```python
print('Status : %s' % result['message'])
print('Total Evaluations: %d' % result['nfev'])
solution = result['x']
evaluation = func(solution)
print('Solution: f(%s) = %.5f' % (solution, evaluation))
```

    Status : Warning: CG iterations didn't converge.  The Hessian is not positive definite.
    Total Evaluations: 1
    Solution: f([0.98732741 0.73207205]) = 1.80937


# Week 4 Lab Session (ctd..): Advertising Data

* In this lab session we are going to look at how to answer questions involing the use of simple and multiple linear regression in Python.

* The questions are based on the book "An introduction to Statistical Learning" by James et al.

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import scipy.stats as stats
import statsmodels.api as sm
import statsmodels.formula.api as smf
import seaborn as sns
```

```python
data_advert = pd.read_csv('http://www-bcf.usc.edu/~gareth/ISL/Advertising.csv', index_col=0)
data_advert.head()
```

      TV
      radio
      newspaper
      sales

      1
      230.1
      37.8
      69.2
      22.1

      2
      44.5
      39.3
      45.1
      10.4

      3
      17.2
      45.9
      69.3
      9.3

      4
      151.5
      41.3
      58.5
      18.5

      5
      180.8
      10.8
      58.4
      12.9

```python
data_advert.describe()
```

      TV
      radio
      newspaper
      sales

      count
      200.000000
      200.000000
      200.000000
      200.000000

      mean
      147.042500
      23.264000
      30.554000
      14.022500

      std
      85.854236
      14.846809
      21.778621
      5.217457

      min
      0.700000
      0.000000
      0.300000
      1.600000

      25%
      74.375000
      9.975000
      12.750000
      10.375000

      50%
      149.750000
      22.900000
      25.750000
      12.900000

      75%
      218.825000
      36.525000
      45.100000
      17.400000

      max
      296.400000
      49.600000
      114.000000
      27.000000

```python
plt.subplot(131)
plt.scatter(data_advert.TV,data_advert.sales)
plt.xlabel('TV')
plt.ylabel('Sales')

plt.subplot(132)
plt.scatter(data_advert.radio,data_advert.sales)
plt.xlabel('radio')

plt.subplot(133)
plt.scatter(data_advert.newspaper,data_advert.sales)
plt.xlabel('newspaper')
plt.subplots_adjust(top=0.8, bottom=0.08, left=0.0, right=1.3, hspace=5, wspace=0.5)
```

## QUESTION 1: Is there a relationship between advertising sales and budget?

* Test the null hypothesis
\begin{equation}
H_0: \beta_1=\ldots=\beta_p=0
\end{equation}
versus the alternative
\begin{equation}
H_a: \text{at least one $\beta_j$ is nonzero}
\end{equation}
* For that compute the F-statistic in Multiple Linear Regression 'sales ~ TV+radio+newspaper' using $\texttt{ols}$ from ${\bf Statsmodels}$
* If there is no relationship between the response and predictors, the F-statistic takes values close to 1. If $H_a$ is true, than F-statistic is expected to be significantly greater than 1. Check the associated p-values.

```python
results = smf.ols('sales ~ TV+radio+newspaper', data=data_advert).fit()
print(results.summary())
```

                                OLS Regression Results
    ==============================================================================
    Dep. Variable:                  sales   R-squared:                       0.897
    Model:                            OLS   Adj. R-squared:                  0.896
    Method:                 Least Squares   F-statistic:                     570.3
    Date:                Sun, 28 Oct 2018   Prob (F-statistic):           1.58e-96
    Time:                        23:46:29   Log-Likelihood:                -386.18
    No. Observations:                 200   AIC:                             780.4
    Df Residuals:                     196   BIC:                             793.6
    Df Model:                           3
    Covariance Type:            nonrobust
    ==============================================================================
                     coef    std err          t      P>|t|      [0.025      0.975]
    ------------------------------------------------------------------------------
    Intercept      2.9389      0.312      9.422      0.000       2.324       3.554
    TV             0.0458      0.001     32.809      0.000       0.043       0.049
    radio          0.1885      0.009     21.893      0.000       0.172       0.206
    newspaper     -0.0010      0.006     -0.177      0.860      -0.013       0.011
    ==============================================================================
    Omnibus:                       60.414   Durbin-Watson:                   2.084
    Prob(Omnibus):                  0.000   Jarque-Bera (JB):              151.241
    Skew:                          -1.327   Prob(JB):                     1.44e-33
    Kurtosis:                       6.332   Cond. No.                         454.
    ==============================================================================

    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

## QUESTION 2: How strong is the relationship?

You should base your discussion on the following quantities:
* $RSE$ - computed using $\texttt{scale}$ atribute: $\texttt{np.sqrt(results.scale)}$
    * Compute the percentage error, i.e. $RSE/(mean sale)$
* $R^2$ - computed using $\texttt{rsquared}$ atribute: $\texttt{results.rsquared}$

## QUESTION 3: Which media contribute to sales?

* Examine the p-values associated with each predictor’s t-statistic

## QUESTION 4: How large is the effect of each medium on sales?

* Examine 95% confidence intervals associated with each predictor
* Compare your results with three separate simple lineare regression

## QUESTION 5: Is the relationship linear?
* You can use residual versus fitted value plot to investigate this

## QUESTION 6: Is there interaction among the advertising media?
* Consider model sales ~ TV + radio + TV:radio
* How much more variability are we able to explain with this model?

```python
results = smf.ols('sales ~ TV + radio + TV:radio', data=data_advert).fit()
print(results.summary())
```

                                OLS Regression Results
    ==============================================================================
    Dep. Variable:                  sales   R-squared:                       0.968
    Model:                            OLS   Adj. R-squared:                  0.967
    Method:                 Least Squares   F-statistic:                     1466.
    Date:                Mon, 29 Oct 2018   Prob (F-statistic):          2.92e-144
    Time:                        00:09:17   Log-Likelihood:                -270.04
    No. Observations:                 200   AIC:                             550.1
    Df Residuals:                     195   BIC:                             566.6
    Df Model:                           4
    Covariance Type:            nonrobust
    ==============================================================================
                     coef    std err          t      P>|t|      [0.025      0.975]
    ------------------------------------------------------------------------------
    Intercept      6.7284      0.253     26.561      0.000       6.229       7.228
    TV             0.0191      0.002     12.633      0.000       0.016       0.022
    radio          0.0280      0.009      3.062      0.003       0.010       0.046
    newspaper      0.0014      0.003      0.438      0.662      -0.005       0.008
    TV:radio       0.0011   5.26e-05     20.686      0.000       0.001       0.001
    ==============================================================================
    Omnibus:                      126.161   Durbin-Watson:                   2.216
    Prob(Omnibus):                  0.000   Jarque-Bera (JB):             1123.463
    Skew:                          -2.291   Prob(JB):                    1.10e-244
    Kurtosis:                      13.669   Cond. No.                     1.84e+04
    ==============================================================================

    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
    [2] The condition number is large, 1.84e+04. This might indicate that there are
    strong multicollinearity or other numerical problems.