```python
from sympy import *
init_printing()
alpha, beta, gamma = symbols('alpha beta gamma')
```

```python
alpha , beta , gamma
```

```python

```


```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 adds
import pprint
```

```python
# or implied, of Diego Navarro Mellén.

from __future__ import print_function

# Special END separator
END = '0e8ed89a-47ba-4cdb-938e-b8af8e084d5c'

# Text attributes
ALL_OFF = '\033[0m'
BOLD = '\033[1m'
UNDERSCORE = '\033[4m'
BLINK = '\033[5m'
REVERSE = '\033[7m'
CONCEALED = '\033[7m'

# Foreground colors
FG_BLACK = '\033[30m'
FG_RED = '\033[31m'
FG_GREEN = '\033[32m'
FG_YELLOW = '\033[33m'
FG_BLUE = '\033[34m'
FG_MAGENTA = '\033[35m'
FG_CYAN = '\033[36m'
FG_WHITE = '\033[37m'

# Background colors
BG_BLACK = '\033[40m'
BG_RED = '\033[41m'
BG_GREEN = '\033[42m'
BG_YELLOW = '\033[43m'
BG_BLUE = '\033[44m'
BG_MAGENTA = '\033[45m'
BG_CYAN = '\033[46m'
BG_WHITE = '\033[47m'

class pretty_output():
    '''
    Context manager for pretty terminal prints
    '''

    def __init__(self, *attr):
        self.attributes = attr

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        pass

    def write(self, msg):
        style = ''.join(self.attributes)
        print('{}{}{}'.format(style, msg.replace(END, ALL_OFF + style), ALL_OFF))

if __name__ == '__main__':

    with pretty_output(FG_RED) as out:
        out.write('This is a test in RED')

    with pretty_output(FG_BLUE) as out:
        out.write('This is a test in BLUE')

    with pretty_output(BOLD, FG_GREEN) as out:
        out.write('This is a bold text in green')

    with pretty_output(BOLD, BG_GREEN) as out:
        out.write('This is a text with green background')

    with pretty_output(FG_GREEN) as out:
        out.write('This is a green text with ' + BOLD + 'bold' + END + ' text included')

    with pretty_output() as out:
        out.write(BOLD + 'Use this' + END + ' even with ' + BOLD + FG_RED + 'no parameters' + END + ' in the with statement')
```

    This is a test in RED
    This is a test in BLUE
    This is a bold text in green
    This is a text with green background
    This is a green text with bold text included
    Use this even with no parameters in the with statement

# Implementación de los métodos

En este notebook se plantea la solución del problema utilizando los siguiente métodos: Newton, Broyden-Fletcher-Goldfarb-Shanno  (BFGS) y el gradiente descendente estocástico (SGD). El presente notebook es autocontenido, sin embargo, la implementación principal se realiza con un enfoque modular.

A continuación, se describe el conjunto de datos que se emplearán y se define el planteamiento del problema. Una explicación más detallada se realiza en el informe (en formato PDF) de este proyecto.

**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).

## 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:

$$
\hat{y}=\left\{
\begin{array}{@{}ll@{}}
0, & \text{si}\ \sigma(\hat{\beta}^Tx)  < 0.5 \\
1, & \text{si} \ \sigma(\hat{\beta}^Tx) \geq 0.5
\end{array}\right. \label{eq:umbral}
$$

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**.

El código asociado a estos componentes se pone a continuación:

```python
def clasifica(X, beta_hat,limit=0.5):
    '''

    Función que clasifica la ocurrencia de probabilidades en dos grupos.

    Emplea el parámetro límite para delimitar si se clasifica en el grupo 0 o 1.

        ** Parámetros:

            - X (mat): matriz de mxp entradas

            - beta_hat (array): optimized parameter

            - limit (float64): 0<limit<1: Threshold for each classification

        ** Salidas:

            - yhat: array of classifed data
    '''
    # Se revisa que los parámetros de entrada sean congruentes con la funcionalidad
    if type(X) is not np.ndarray or type (beta_hat) is not np.ndarray:
        sys.exit('Error: X y beta_hat deben ser de tipo numpy.ndarray')
    if limit > 1 or limit < 0:
        sys.exit('Error:  limit es un paramétro que debe estar entre 0 y 1')

    mu = calc_mu(X,beta_hat)
    yhat = mu
    yhat[mu<limit] = 0
    yhat[mu>=limit] = 1
    return yhat

def sigmoide(z):
    '''
    Función que devuelve el sigmoide de un vector
        - Parámetros:
            -- z (vec): vector numérico de m entradas
        - Salidas
            -- sig (vec): vector númerico de m entradas, cada entrada tiene
                         un valor entre -1 y 1
    '''
    # Se revisa que los parámetros de entrada sean congruentes con la funcionalidad
    if type(z) is not np.ndarray:
        sys.exit('Error: la entrada debe ser de tipo numpy.ndarray')

    sig = 1/(1+ np.exp(-z))

    return sig

def calc_mu(X,beta):
    '''
    Función que calcula la media para una variable aleatoria con distribución bernoulli.
        - Parámetros:
            -- X (mat): matriz de mxp entradas
            -- beta (vec): vector con p entradas
        - Salidas
            -- mu (vec): vector de m entradas
    '''
    a = np.matmul(beta,np.transpose(X))
    mu = sigmoide(a)

    return mu

def f(X,y,beta):
    '''

    Función que computa la log-verosimilitud negativa

        - Parámetros:

            -- X (mat): matriz de mxp entradas

            -- y (vec): vector de de m entradas de la variable output

            -- beta (vec): vector de p entradas

        - Salidas

            -- lvn (int): log-verosimilitud negativa
    '''

    # 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.')
    if beta.shape[0]!= p:
        sys.exit('Error:  El número de columnas de X debe ser igual al número de entradas del vector beta.')

    prob = calc_mu(X,beta)
    # Log-verosimilitud negativa
    lvn = -sum(y*np.log(prob)+(1-y)*(np.log(1-prob)))
    return lvn
```

## 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()
```

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

```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
```

```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 asignan como categoricas a las binarias, incluido el output
#bin_vars = ['OUT', 'HEADCH', 'BLEED', 'DIARR', 'JAUN', 'VOMIT',
#       'PABD', 'WEAK', 'INTER_AGE', f"hasta{age_p25}", f"entre{age_p25+1}y{age_p50}", f"entre{age_p50+1}y{age_p75}", f"mayor{age_p75}"]

#esta asignacion hace que genera problemas al evaluar el sigmoide
#for var in bin_vars:
#    df_proc[var] = df_proc[var].astype('category')

# 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
```

```python
df_proc
```

## 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.

```python
def gradiente_f(X,y,beta):
    '''
    Función que calcula el gradiente asociado la log-verosimilitud negativa del
    problema de regresión logística
        ** Parámetros:
            - X (mat): matriz de mxp entradas
            - y (vec): vector de de m entradas de la variable output
            - beta (vec): vector de p entradas
        ** Salidas:
            - grad (vec): vector de m entradas
    '''

    # 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.')
    if beta.shape[0]!= p:
        sys.exit('Error:  El número de columnas de X debe ser igual al número de entradas del vector beta.')

    mu = calc_mu(X,beta)
    grad = np.matmul(np.transpose(X), mu-y)
    return grad

def hessiana_f(X,y,beta):
    '''
    Función que calcula la matriz Hessiana asociada a la log-verosimilitud negativa del
    problema de regresión logística
        ** Parámetros:
            - X (mat): matriz de mxp entradas
            - y (vec): vector de de m entradas de la variable output
            - beta (vec): vector de p entradas
        ** Salidas
            - hes (vec): vector de m entradas
    '''
    # 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.')
    if beta.shape[0]!= p:
        sys.exit('Error:  El número de columnas de X debe ser igual al número de entradas del vector beta.')

    mu = calc_mu(X,beta)
    S = np.diag(mu*(1-mu))
    hes = np.matmul(np.transpose(X),np.matmul(S,X))
    return hes

def normalize(x):
    '''
    Función que normaliza un vector
        ** Parametros:
            - x: vector a normalizar
        ** Salidas:
            - norm : vector x normalizado
    '''
    # Se revisa que los parámetros de entrada sean congruentes con la funcionalidad
    if type(x) is not np.ndarray:
        sys.exit('Error: la entrada debe ser de tipo numpy.ndarray')

    norm = x/np.sqrt(sum(x*x))
    return norm
```

### Método de máximo descenso

El método de máximo descenso es el método de búsqueda de línea más
sencillo, y plantea que la forma de encontrar el mínimo de una función
$f(\beta_{k})$ es por medio de la búsqueda iterativa de $\beta_{k}$
siguiendo la dirección negativa del gradiente asociado, i-.e. $-p^{T}\nabla f_{k}$ (donde $\nabla f_{k}$ es el gradiente de la función $f$ con respecto
a $\beta_{k}$. ). Cabe anotar que, como es resaltado por Nocedal, este supuesto está
basado en el Teorema de Taylor, el cual establece que:

$$
f(\beta_{k}+\alpha_k p)\approx f(\beta_{k})+\alpha_k p^{T}\nabla f_{k}
$$

donde $\alpha_k$ es el paso del descenso o tasa de aprendizaje en la iteración $k$. Así, en la ecuación anterior
la función $f$ disminuye a una tasa de paso, multiplicada por el
gradiente y el paso, y una dirección $p$, es decir: $p^{T}\nabla f_{k}$.
Por otro lado, dado que el gradiente es negativo se cumple que:

$$
f(\beta_{k}+\alpha_k p)<f(\beta_{k})\label{eq:condicion_desc}
$$

Adicional a lo anterior, es de vital importancia adicionar un componente
al problema, correspondiente a la elección de la tasa de aprendizaje,
$\alpha$, en la medida que, valores muy altos de la misma, pueden
impedir que haya convergencia hacia el mínimo y el algoritmo se estanque,
o que su convergencia sea demasiado lenta.

Así, como es presentado por Nocedal & Wright (2006) y Murphy (2012), el último componente
que se requiere adicionar al problema de optimización planteado, y
que a su vez lo convierte en un método de búsqueda de línea o minimización
de línea (line search), es elegir $\alpha$
tal que se minimice:

$$
\varphi(\alpha)=f(\beta_{k}+\alpha_k p)\label{eq:cond_linesearch}
$$

En el método de descenso de gradiente, la forma de
actualizar el vector de parámetros es como se define a continuación:

$$
\beta_{k+1}=\beta_{k}-\alpha\nabla f_{k}\label{eq:steepest_descent}
$$

De esta forma dado un $\beta_{0}$ inicial elegido aleatoriamente,
la actualización de los pesos continúa hasta que $\|\nabla f_{k}\|\approx0$
o hasta que un número definido de iteraciones se alcance.

### Método de Newton

El método de Newton es parte la familia de métodos de optimización
de segundo orden, en la medida que tienen en cuenta la curvatura de
la función a optimizar, la cual es incorporada por la matriz Hessiana,
$\nabla^{2}f$. Lo anterior, implica un mayor costo en términos de
cómputo con respecto al algoritmo de máximo descenso. Sin embargo,
la existencia de dicha curvatura, en la mayoría de los problemas,
permite lograr niveles de convergencia mucho más rápido.

En particular, en este método, la forma de actualizar el vector de
parámetros es de la siguiente manera:

$$
\beta_{k+1}=\beta_{k}-\alpha_{k}\nabla^{2}f_{k}^{-1}\nabla f_{k}\label{eq:act_newton}
$$

### Método BFGS

El método de propuesto por Broyden, Fletcher, Goldfarb and Shanno
(BFGS) es un algoritmo quasi-newtoniano que aborda una solución a
los problemas en los cuales es muy costoso el cómputo de la matriz
Hessiana de $f_{k}$. Como es descrito por Murphy, este método iterativamente
computa una aproximación de $\nabla^{2}f_{k}$ a partir del gradiente,
$\nabla f_{k}$.

Dado lo anterior, la aproximación de la matriz Hessiana se define
como $H_{k}\approx\nabla^{2}f_{k}$, y se calcula con una función
de rango 2 dada por:

\begin{align}
H_{k+1} & =H_{k}+\frac{w_{k}(w_{k})^{T}}{(w_{k})^{T}z_{k}}-\frac{H_{k}z_{k}(H_{k}z_{k})^{T}}{(z_{k})^{T}H_{k}z_{k}}\label{eq:H}\\
z_{k} & =\beta_{k+1}-\beta_{k}\label{z}\\
w_{k} & =\nabla f_{k+1}-\nabla f_{k}\label{eq:w}
\end{align}

Como es mencionado por Murphy (2012), por lo general el algoritmo se inicializa
con $H_{o}=I$.

```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_H(X,y,beta,beta_new=None,H=None):
    '''
    Función que actualiza los valores de la matriz H del metodo bfgs para cada iteracion
        ** Parametros:
            - X (mat): matriz de mxp entradas
            - y (vec): vector de de m entradas de la variable output
            - beta (array) - valor de cantidad a optimizar en la iteracion actual
            - beta_new (array)- valore de la cantidad a optimizar despues de la actualizacion
            - H (mat)- valor de la matriz H en la iteracion anterior
        ** Salidas:
            - H (mat): valor de la matriz para la siguiente iteracion
    '''

    w = gradiente_f(X,y,beta_new)- gradiente_f(X,y,beta)
    z = beta_new-beta
    Hz = np.matmul(H,z)
    dotwz = np.dot(w,z)
    dotzhz = np.dot(Hz,z)
    H = H+(np.outer(w,w)/dotwz)-(np.outer(Hz,Hz)/dotzhz)

    return H
```

```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
# prueba
# prueba
# No esta funcionando bien
# Longitud de paso con condiciones completas de wolf

def calc_lr_wolf(X, y, beta, lr, pk, c1=10**(-4), c2=0.9, 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 tenía 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')
    #if pk >= 0 :
    #    sys.exit('Error: pk debe ser negativo')

    # Inicializamos
    tao = 0.5
    max_iter = 50
    iter = 0

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

    # Evalauciones periodicas
    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)

    gf_x = np.dot(gradiente_f(X,y, beta+lr*pk) , pk)
    gf_x1 = c2* np.dot(grad, pk)

    while ((f_x>f_x1) & (gf_x<gf_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)

        gf_x = np.dot(gradiente_f(X,y, beta+lr*pk) , pk)
        #gf_x1 = c2* 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)
```

```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)
```

```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)
```

## 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 riesgo_empirico(X,y,beta):

    '''
    Función que calcular el riesgo empírico como la esperanza de la función de pérdida

    evaluada sobre todos los puntos del dominio.

        - Parámetros:
            -- X (mat): matriz de mxp entradas

            -- y (vec): vector de de m entradas de la variable output

            -- beta (vec): vector con p entradas

        - Salidas

            -- loss (float64): riesgo empírico
    '''
    # 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.')
    if beta.shape[0]!= p:
        sys.exit('Error:  El número de columnas de X debe ser igual al número de entradas del vector beta.')

    mu=calc_mu(X,beta)
    loss=-sum(y*np.log(mu)+(1-y)*np.log(1-mu))
    return loss

def gradiente_riesgo_empirico(X,y,beta):

    '''
    Función que calcular el gradiente de la función de riesgo.

        - Parámetros:

            -- X (mat): matriz de mxp entradas.

            -- y (vec): vector de de m entradas de la variable output.

            -- beta (vec): vector con p entradas.

        - Salidas

            -- grad_riesgo_emp (vec): vector de p entradas
    '''
    # 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.')
    if beta.shape[0]!= p:
        sys.exit('Error:  El número de columnas de X debe ser igual al número de entradas del vector beta.')

    m = X.shape[0]
    mu = calc_mu(X,beta)
    grad_riesgo_emp = np.matmul(np.transpose(X),mu-y)/m
    return grad_riesgo_emp
```

```python
import random

def batch(m,q=10):

    '''

    '''

    index=np.random.randint(low=0,high=m,size=q)
    return index

def error_train(X,y,beta):
    prediction=clasifica(X,beta)
    err=round(100*sum(abs(y-prediction))/len(prediction),2)
    return err
```

```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 ((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
        #print("iteraciones1=",iteraciones)
        beta = beta_new
        #x_lote,y_lote = mini_lotes(X,y,q)
        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("iteraciones3=",iteraciones)
    print("Nº DE INTERACIONES: ",iteraciones)
    return beta_new,perdida,error
```

```python
def graf_loss_err(loss,error,title):
    '''
    Función para graficar la pérdida o riesgo emírico y el error
    de entrenamiento en cada iterazación.
        - Entradas:
                    -- loss
                    -- error
                    ---title
        - Salidas:
                    -- plot
    '''

    x=np.arange(0,len(error))
    fig, axs = plt.subplots(2, 1)
    axs[0].plot(x, error)
    axs[0].set_xlabel('Iteraciones')
    axs[0].set_ylabel('Train error %')
    axs[0].grid(True)

    axs[1].plot(x, loss)
    axs[1].set_xlabel('Iteraciones')
    axs[1].set_ylabel('Perdida')
    axs[1].grid(True)
    axs[0].set_title(title, fontsize=14)

    fig.tight_layout()
    plt.show()

#graf_loss_err(loss[0,20],error[0:20],"Batch size")
```

```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
def n_batch(m):
    index=np.arange(0,m)
    np.random.shuffle(index)
    return index
```

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

```python
client
```

```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
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 del modelo logístico

Como se mencionó anteriormente, el problema de regresión logística
es una extensión del problema de regresión lineal, aplicada hacia
un problema de clasificación. En este sentido, la regresión logística
modela las probabilidades de clasificación de una variable dependiente
dentro de dos posibles categorías, 0 y 1.

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}
$$

Lo que es equivalente a:

$$
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}
$$

- Para este ejercicio, no estamos considerando el término equivalente al intercepto en el caso de regresión
lineal (i.e $\beta_{0}=0)$.

- A diferencia de un modelo de regresión tradicional, en este caso los pesos no afectan de forma lineal las probabilidades.
- Para lograr interpretabilidad en cuanto a las probabilidades, es necesario despejar el componente lineal de la ecuación.

El término *logit* y corresponde al logaritmo 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}
$$

- El modelo logístico, dada la naturaleza
dicotómica de la variable de salida del mismo no asume homocedasticidad.

- La interpretación de los resultados del modelo de regresión logística
se realiza en términos del valor del ratio de probabilidades, que corresponde
al operador exponencial de $\lambda$. Dicho operador se lee como
el indicador de variación en el ratio de probabilidades como resultado del
cambio en una unidad en la predicción.

Los resultados para nuestro ejercicio implican:

$$
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.

En este sentido, tomaremos dicho valor para el caso en el cual la persona no tiene 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 que en la ecuación anterior, se tiene $x_{i}=0$ para $i=3,4,5,6,7,8$
y $x_{9}=1$:

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

Si tomamos los valores promedio de cycle treshold $\bar{x}_{1}$,
y de temperatura, $\bar{x}_{2}$ , tenemos lo siguiente:

$$
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
$$

Por otro lado, si quisiéramos analizar, por ejemplo, cual 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.

### Calculo de probabilidades

```python
Por úlim
```