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

## 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 1 a 73.|
|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.|

## Problema de regresión Logística

Matemáticamente, este conjunto se define de la siguiente manera:

$$\mathcal{D}=\left\{ \left(x_{i},y_{i}\right)\in\mathbb{R}^{p}\times\left\{ 0,1\right\} :i\in\left[m\right]\right\} $$.

El método de _regresión logística_ asume que $Pr\left[y_{i}\mid x_{i},\beta\right]\sim Bernoulli\left(\mu_{i}\right)$
con los siguientes supuestos sobre la media, $\mu_{i}$:

$$
\mu_{i}=\sigma\left(\beta^{T}x_{i}\right) \label{eq-3.1} \tag{1}
$$
$$
\sigma(z)=\left(1+\exp\left(-z\right)\right)^{-1} \label{eq-3.2} \tag{2}
$$

donde $\beta\in\mathbb{R}^{p}$.

Dado lo anterior, nuestro problema es encontrar un modelo tal que $\hat{\beta}\in\mathbb{R}^{p}$ explica de la mejor manera posible a $\mathcal{D}$.

Para lograr lo anterior, debemos estimar el conjunto de parámetros $\hat{\beta}$ para modelar $Pr\left[y\mid x,\hat{\beta}\right]$ y predecir la etiqueta $\hat{y}\in\left\{ 0,1\right\} $ de un nuevo
dato $x$ por medio de:

$$
\hat{y}=\begin{cases}
1 & si\,\sigma\left(\hat{\beta}^{T}x\right)\geq0.5\\
0 & si\,\sigma\left(\hat{\beta}^{T}x\right)<0.5
\end{cases}\label{eq-3.3} \tag{3}
$$

la función de pérdida que queremos minimizar en este problema corresponde a la _log-verosimilitud negativa_ , que está dada por:

$$
F(\beta):=LVN(\beta)=-\sum_{i=1}^{m}\left[y_{i}log\mu_{i}+(1-y_{i})log(1-\mu_{i})\right]\label{eq-3.4} \tag{4}
$$

Una vez planteado lo anterior, queremos encontrar $\hat{\beta}$ por medio de métodos numéricos de optimización de tal forma que se minimize ([4](#mjx-eqn-eq1)) para el conjunto de datos dado.

_En los siguientes fragmentos de código se realiza el planteamiento del problema, desde la importación de datos hasta el proceso de entrenamiento del modelo utilizando distintos algoritmos de optimización que se explican con brevedad._

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

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

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

    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

A continuación se plantea el código que computa las ecuaciones ([1](#mjx-eqn-eq1)), ([2](#mjx-eqn-eq1)) y ([4](#mjx-eqn-eq1)), planteadas inicialmente.

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

Reescribiendo la ecuación de la función de pérdida ([4](#mjx-eqn-eq1)), tenemos:

$$F(\beta)=- \sum_{i=1}^{m}[y_i log\mu_i + (1-y_i)log(1-\mu_i)]$$

Las expresiones correspondientes al gradiente y a la matriz hessiana asociados a este problema, se plantean a continuación:

\begin{align}
\nabla F(\beta) & =\frac{d}{d\beta}F(\beta)\nonumber \\
 & =\sum_{i}\left(\mu_{i}-y_{i}\right)x_{i}\nonumber \\
 & =\boldsymbol{X}^{T}\left(\boldsymbol{\mu}-\boldsymbol{y}\right)\label{eq:gradient}
\end{align}

Por otro lado, la ecuación que describe la matrix Hessiana es la siguiente:

\begin{align}
\nabla^{2}F(\beta) & =\frac{d}{d\beta}\nabla F\left(\beta\right)^{T}\nonumber \\
 & =\sum_{i}\left(\nabla_{\beta}\mu_{i}\right)x_{i}^{T}\nonumber \\
 & =\sum_{i}\mu_{i}\left(1-\mu_{i}\right)x_{i}x_{i}^{T}\nonumber \\
 & =\boldsymbol{X^{T}SX}\label{eq:hessian}
\end{align}

donde $\boldsymbol{S}\triangleq diag\left(\mu_{i}\left(1-\mu_{i}\right)\right)$.
Como es resaltado por Murphy (2012), es definida positiva, lo que implica que ([4](#mjx-eqn-eq1)) es convexa
y tiene un mínimo global.

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

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

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

```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
    '''
    # 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:
            print("************************************************************************")
            grad=np.linalg.norm(gradiente_f(X,y,beta_new))
            print(f'GRADIENTE: {grad:.7E}, LEARNING RATE: {lr:.4E}, Nº ITERACIONES: {iteraciones}')

    print("*========================================================================*")
    if iteraciones == max_iter:print("Alcanzó el número máximo de iteraciones")

    print("ITERACIONES: ",iteraciones)
    print("GRADIENTE DE F: ",np.linalg.norm(gradiente_f(X,y,beta_new)))
    print("*========================================================================*")

    return beta_new
```

## Split and Train

```python
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler

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

beta_hat = gradient_descent(x_train,y_train,max_iter=10**6,reset_lr=False,verbose_n=100000)
yhat = clasifica(x_test,beta_hat)

print("BETA_HAT: ", beta_hat)
print("ERROR DE CLASIFICACIÓN: ",round(100*sum(abs(y_test-yhat))/len(yhat),2),"%")
print("*========================================================================*")

```

    /Users/danielapintoveizaga/Library/Python/3.7/lib/python/site-packages/ipykernel_launcher.py:72: RuntimeWarning: divide by zero encountered in log
    /Users/danielapintoveizaga/Library/Python/3.7/lib/python/site-packages/ipykernel_launcher.py:72: RuntimeWarning: invalid value encountered in multiply

    *========================================================================*
    ITERACIONES:  14972
    GRADIENTE DE F:  1.1991469950207157e-06
    *========================================================================*
    BETA_HAT:  [-207.30981145  607.26475844  -77.35480567    0.66725438   25.50300541
      120.11341839  -59.10582665   91.50642227   24.95644907   27.20024392
       67.40971947   71.84324543]
    ERROR DE CLASIFICACIÓN:  13.64 %
    *========================================================================*
    CPU times: user 2.04 s, sys: 86.6 ms, total: 2.13 s
    Wall time: 2.06 s

```python
%%time
# Método de Newton
beta_hat = gradient_descent(x_train,y_train, method="newton",max_iter = 10**5,verbose_n = 10)
yhat = clasifica(x_test,beta_hat)

print("BETA_HAT: ", beta_hat)
print("ERROR DE CLASIFICACIÓN: ",round(100*sum(abs(y_test-yhat))/len(yhat),2),"%")
print("*========================================================================*")
```

    ************************************************************************
    GRADIENTE: 2.2161580E+01, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 10
    ************************************************************************
    GRADIENTE: 5.7651872E+00, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 20
    ************************************************************************
    GRADIENTE: 1.9273256E+00, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 30
    ************************************************************************
    GRADIENTE: 9.7325156E-01, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 40
    ************************************************************************
    GRADIENTE: 5.8676466E-01, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 50
    ************************************************************************
    GRADIENTE: 4.0312244E-01, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 60
    ************************************************************************
    GRADIENTE: 2.9780635E-01, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 70
    ************************************************************************
    GRADIENTE: 2.2801381E-01, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 80
    ************************************************************************
    GRADIENTE: 1.7786662E-01, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 90
    ************************************************************************
    GRADIENTE: 1.4016474E-01, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 100
    ************************************************************************
    GRADIENTE: 1.1108688E-01, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 110
    ************************************************************************
    GRADIENTE: 8.8345234E-02, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 120
    ************************************************************************
    GRADIENTE: 7.0422575E-02, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 130
    ************************************************************************
    GRADIENTE: 5.6234722E-02, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 140
    ************************************************************************
    GRADIENTE: 4.4970875E-02, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 150
    ************************************************************************
    GRADIENTE: 3.6009336E-02, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 160
    ************************************************************************
    GRADIENTE: 2.8867028E-02, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 170
    ************************************************************************
    GRADIENTE: 2.3165848E-02, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 180
    ************************************************************************
    GRADIENTE: 1.8608596E-02, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 190
    ************************************************************************
    GRADIENTE: 1.4961011E-02, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 200
    ************************************************************************
    GRADIENTE: 1.2038038E-02, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 210
    ************************************************************************
    GRADIENTE: 9.6931989E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 220
    ************************************************************************
    GRADIENTE: 7.8103161E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 230
    ************************************************************************
    GRADIENTE: 6.2970619E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 240
    ************************************************************************
    GRADIENTE: 5.0799194E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 250
    ************************************************************************
    GRADIENTE: 4.1002474E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 260
    ************************************************************************
    GRADIENTE: 3.3111969E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 270
    ************************************************************************
    GRADIENTE: 2.6752900E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 280
    ************************************************************************
    GRADIENTE: 2.1625102E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 290
    ************************************************************************
    GRADIENTE: 1.7487931E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 300
    ************************************************************************
    GRADIENTE: 1.4148287E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 310
    ************************************************************************
    GRADIENTE: 1.1451100E-03, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 320
    ************************************************************************
    GRADIENTE: 9.2717489E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 330
    ************************************************************************
    GRADIENTE: 7.5100122E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 340
    ************************************************************************
    GRADIENTE: 6.0852432E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 350
    ************************************************************************
    GRADIENTE: 4.9325050E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 360
    ************************************************************************
    GRADIENTE: 3.9994813E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 370
    ************************************************************************
    GRADIENTE: 3.2439995E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 380
    ************************************************************************
    GRADIENTE: 2.6320462E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 390
    ************************************************************************
    GRADIENTE: 2.1361744E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 400
    ************************************************************************
    GRADIENTE: 1.7342249E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 410
    ************************************************************************
    GRADIENTE: 1.4082989E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 420
    ************************************************************************
    GRADIENTE: 1.1439326E-04, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 430
    ************************************************************************
    GRADIENTE: 9.2943229E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 440
    ************************************************************************
    GRADIENTE: 7.5534020E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 450
    ************************************************************************
    GRADIENTE: 6.1400359E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 460
    ************************************************************************
    GRADIENTE: 4.9922784E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 470
    ************************************************************************
    GRADIENTE: 4.0599675E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 480
    ************************************************************************
    GRADIENTE: 3.3024679E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 490
    ************************************************************************
    GRADIENTE: 2.6868508E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 500
    ************************************************************************
    GRADIENTE: 2.1864228E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 510
    ************************************************************************
    GRADIENTE: 1.7795379E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 520
    ************************************************************************
    GRADIENTE: 1.4486381E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 530
    ************************************************************************
    GRADIENTE: 1.1794764E-05, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 540
    ************************************************************************
    GRADIENTE: 9.6048940E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 550
    ************************************************************************
    GRADIENTE: 7.8228915E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 560
    ************************************************************************
    GRADIENTE: 6.3725164E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 570
    ************************************************************************
    GRADIENTE: 5.1918385E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 580
    ************************************************************************
    GRADIENTE: 4.2305389E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 590
    ************************************************************************
    GRADIENTE: 3.4477229E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 600
    ************************************************************************
    GRADIENTE: 2.8101472E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 610
    ************************************************************************
    GRADIENTE: 2.2907826E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 620
    ************************************************************************
    GRADIENTE: 1.8676471E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 630
    ************************************************************************
    GRADIENTE: 1.5228602E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 640
    ************************************************************************
    GRADIENTE: 1.2418744E-06, LEARNING RATE: 1.0000E+00, Nº ITERACIONES: 650
    *========================================================================*
    ITERACIONES:  652
    GRADIENTE DE F:  1.1922499579476228e-06
    *========================================================================*
    BETA_HAT:  [-191.47292429  562.44031196  -71.36867787  123.1299603    20.34578023
      115.58259723  -55.47819992   84.83809904   23.16274978   25.55337006
       65.80294491   69.46380373]
    ERROR DE CLASIFICACIÓN:  13.64 %
    *========================================================================*
    CPU times: user 165 ms, sys: 16.5 ms, total: 181 ms
    Wall time: 176 ms

    /Users/danielapintoveizaga/Library/Python/3.7/lib/python/site-packages/ipykernel_launcher.py:72: RuntimeWarning: divide by zero encountered in log
    /Users/danielapintoveizaga/Library/Python/3.7/lib/python/site-packages/ipykernel_launcher.py:72: RuntimeWarning: invalid value encountered in multiply

```python
%%time
# Método BFSG

beta_hat = gradient_descent(x_train,y_train, method = "bfsg",max_iter = 10**6,lr=1,verbose_n = 100000)
yhat = clasifica(x_test,beta_hat)

print("BETA_HAT: ", beta_hat)
print("ERROR DE CLASIFICACIÓN: ",round(100*sum(abs(y_test-yhat))/len(yhat),2),"%")
print("*========================================================================*")
```

    /Users/danielapintoveizaga/Library/Python/3.7/lib/python/site-packages/ipykernel_launcher.py:72: RuntimeWarning: divide by zero encountered in log
    /Users/danielapintoveizaga/Library/Python/3.7/lib/python/site-packages/ipykernel_launcher.py:72: RuntimeWarning: invalid value encountered in multiply

    ************************************************************************
    GRADIENTE: 6.7604013E-04, LEARNING RATE: 1.8530E-01, Nº ITERACIONES: 100000
    ************************************************************************
    GRADIENTE: 1.8246751E-06, LEARNING RATE: 1.8530E-01, Nº ITERACIONES: 200000
    *========================================================================*
    ITERACIONES:  207214
    GRADIENTE DE F:  1.1997522410752398e-06
    *========================================================================*
    BETA_HAT:  [-1.92597509e+02  5.62066406e+02 -7.16006035e+01 -9.05363951e-02
      2.33935569e+01  1.11631145e+02 -5.46857075e+01  8.48952910e+01
      2.35974039e+01  2.56294180e+01  6.29470910e+01  6.70883012e+01]
    ERROR DE CLASIFICACIÓN:  13.64 %
    *========================================================================*
    CPU times: user 38.1 s, sys: 225 ms, total: 38.3 s
    Wall time: 38.2 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 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 mini_lotes(X,y,q=3):
    '''

    '''

    cols = X.shape[1]
    data = np.hstack((X,y[:,None]))
    np.random.shuffle(data)
    data = data[0:q]
    X = data[:,0:cols]
    y = data[:,cols]
    return X,y

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
batch_size=10
```

```python

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

    '''

    '''

     # 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/q)#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%1000==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
x=1
y=[]
for y in range(1,7):
    x=np.append(x,y)
print(x)
```

    [1 1 2 3 4 5 6]

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

# Método de SGD
for batch_size in range(1,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

```