Skip to content

My words, sometimes technical, sometimes not

Esencia del Algebra Lineal

El álgebra lineal está por todas partes en estadística y data science. Matrices, vectores y transformaciones son términos que se escuchan seguido y están detrás de muchos de los métodos y algoritmos que se usan hoy por hoy. Aunque no sea necesario saber del tema para correr un modelo empaquetado en una librería de R, es muy útil entender lo que hacemos realmente ya que nos permite ver a los modelos como algo lógico y no una caja negra mágica.

Hay una serie de videos excelente en inglés que mediante visualizaciones y animaciones permite entender la intuición de muchos de los conceptos básicos, que solo con un libro puede ser medio críptico o poco imaginable. Para el que le interese: ESSENCE OF LINEAR ALGEBRA por 3Blue1Brown.

Este post, aunque quizás medio desordenado y sin mucha prolijidad, es una recopilación de algunas notas. Puede que queden términos en inglés intercalados.

Matrices y vectores

  • Vector vive en n dimensiones.
  • Suma de vectores es combinación lineal
  • En \(R^{2}\) \(\hat \imath = \left< 1, 0 \right> \text{y} \hat \jmath = \left< 0, 1 \right>\) forman una base. Cualquier punto es una combinación lineal de ellos.
  • Span es el espacio que pueden generar x vectores. \(R^{2}\) es el span de \(\left< 1, 0 \right> \left< 0, 1 \right>\)
  • Vector puede ser pensado como una flecha desde el origen (0,0) a las coordenadas que lo identifican. O como un punto directo en las coordenadas..
  • Matriz es una transformación. Lleva un vector a otro punto. Si transformamos cada posible vector de un espacio por la matriz podemos ver como el espacio es transformado. Ej: rotar, invertir, estirar.
  • Si transformamos una base, cada punto nuevo puede generarse transformando la nueva base.
    Por ej: \(z = \left< 3, 2 \right> \text{es } 3\begin{bmatrix} 1 \\ 0 \end{bmatrix} + 2\begin{bmatrix} 0 \\ 1 \end{bmatrix} = 3\hat \imath + 2\hat \jmath\)
    Aplicando la transformación de la matriz A = \(\begin{pmatrix} A & B \\ C & D \end{pmatrix} \text{obtenemos los nuevos vectores base } \hat \imath^{*} \text{y } \hat \jmath^{*}\)
    \(z^{*} = 3\hat \imath^{*} + 2\hat \jmath^{*}\)
  • Multiplicar 2 matrices es transformar un espacio con la primera matriz ( desde la derecha) y luego transformar el resultado por la segunda matriz. Ej: Rotar un espacio y luego invertirlo.
  • AB != BA -> El orden de las transformaciones importa y se lee de derecha a izquierda.
  • La matriz (transformación) ya dice como van a ser las nuevas bases.
    Si la matriz es \(\begin{pmatrix} A & B \\ C & D \end{pmatrix}\), el nuevo \(\hat \imath^{*}\) es \(\begin{bmatrix} A \\ C \end{bmatrix}\) y \(\hat \jmath^{*}\) es \(\begin{bmatrix} B \\ D \end{bmatrix}\)
    Ej: \(z = \left< 3, 2 \right> z^{*} = \begin{bmatrix} A & B \\ C & D \end{bmatrix}\begin{bmatrix} 3 \\ 2 \end{bmatrix} = \begin{bmatrix} 3A + 2B \\ 3C + 2D \end{bmatrix}\)
    Se puede ver también como: \(\(z = 3\hat \imath + 2\hat \jmath \text{ } z^{*} = 3\hat \imath^{*} + 2\hat \jmath^{*} = 3\begin{bmatrix} A \\ C \end{bmatrix} + 2\begin{bmatrix} B \\ D \end{bmatrix} = \begin{bmatrix} 3A + 2B \\ 3C + 2D \end{bmatrix}\)\)  

  • !!!. Las transformaciones afectan el area (en R2, el volumen en R3..) de las figuras en el espacio (todas por igual). El DETERMINANTE de una matriz mide ese cambio.

  • Si el determinante es 0 significa que se perdió una dimensión o que todo se comprimió. Pasa de \(R^{2}\) a una recta (o a un punto!)
  • Si el determinante ** es < 0** significa que el espacio se invirtió (en sentido.. como dar vuelta una hoja) pero |DET| siguen siendo el cambio en el area.
  • A-1A = I -> una transformación que no hace nada.
  • Si DET(A) = 0 no existe la matriz inversa. Ej. \(R^{2}\) -> si det(A) = 0 la transformación lleva el espacio a una recta. No hay función que lleve cada vector de la recta a un punto en \(R{2}\). No hay vuelta atrás.    

  • Si una transformación lleva todos los puntos a una recta tiene rango 1, si lleva a un plano rango 2, y así.. RANGO es el número de dimensiones del output. Rango completo es cuando mantiene las dimensiones del input.

  • El conjunto de posibles outputs de \(A\vec v\) es el Column Space = Span de las columnas
  • Cuando perdés dimensiones por la transformación todo un conjunto de vectores pasa a ser (0,0). Eso se llama Null Space o Kernel
  • Matrices no cuadradas cambian la dimensión del espacio.
    $$ \begin{bmatrix} A & D \ B & E \ C & F \end{bmatrix} \begin{bmatrix} 1 \ 1 \end{bmatrix} = \begin{bmatrix} A + D \ B + E \ C + F \end{bmatrix} $$ Quedan todos los puntos de \(R^{2}\) en un plano en el espacio \(R^{3}\). De acá viene la restricción para multiplicar matrices. La cantidad de columnas de la transformación tiene que ser igual a la dimensión del input

DOT PRODUCT o PRODUCTO INTERNO

  • Dot product entre dos vectores equivale a proyectar uno en el otro y multiplicar sus largos. \(\vec A \cdot \vec B = |A^{*}| * |B|\)
    \(A^{*}\) es el vector A proyectado en B.
  • \(\vec B\) es un vector 2D pero también se lo puede ver como una matriz 1x2 que lleva del 2D a la recta.
    \(\vec B \cdot \vec A = B \vec A \text{que sería llevar A al espacio transformado por B.}\)
    \(B = \begin{bmatrix} B_x & B_y \end{bmatrix}\) tiene en sus columnas donde queda \(\hat \imath \text{y } \hat \jmath\) (los vectores unitarios) al ser transformados o algun valor escalado de esto.
    \(\vec A \cdot \vec B\) es el valor de A en la recta a la que te lleva la transformación B.
  • Es equivalente proyecto B en A.
  • Si Dot Product > 0, tienen dirección similar.
  • Si Dot Product = 0, son ortogonales - proyección que cae en el origen.
  • Si Dot Product < 0, tienen direcciones opuestas.

   

CROSS PRODUCT

  • Está definido para vectores en \(R^{3}\)
  • El cross product \(\vec u \times \vec v\) es el area del paralelograma que se puede imaginar con las paralelas de los vectores (imaginandolo en \(R^{2}\). El signo depende de la orientación de los vectores. El vector de la "derecha" tiene que estar primero para que el cross product sea > 0.
  • En realidad el paralelograma formado por dos vectores en R3 tiene area equivalente al Largo del vector output de su cross product. Ese vector es ortogonal al paralelograma.

   

CAMBIO DE BASE

  • Distintos sistemas de coordenadas definen \(\hat \imath = \left< 1, 0 \right>, \hat \jmath \left< 0, 1 \right>\) como algo distinto. NO hay una sola "grilla" válida. El espacio no tiene grilla predeterminada.
  • Un mismo vector tiene distintas coordenadas según el sistema de bases desde donde se lo mire.
  • Para pasar de una base a otra se aplica una transformación lineal.
    Si \(\vec v\) es un vector que queremos pasar de una base a otra, lo transformamos por la nueva base.
    Y \(\hat \imath^{*} = \left< \hat \imath^{*}_1, \hat \imath^{*}_2 \right>, \hat \jmath^{*} = \left< \hat \jmath^{*}_1, \hat \jmath^{*}_2 \right>\) Entonces: \(\(\begin{bmatrix} \hat \imath^{*}_1 & \hat \jmath^{*}_1 \\ \hat \imath^{*}_2 & \hat \jmath^{*}_2 \end{bmatrix} \begin{bmatrix} v_1 \\ v_2 \end{bmatrix} = \begin{bmatrix} v^{*}_1 \\ v^{*}_2 \end{bmatrix}\)\)
    Donde \(\begin{bmatrix} v^{*}_1 \\ v^{*}_2 \end{bmatrix}\) es el vector en la nueva base, es decir, serían las coordenadas del vector \(\vec v\) en el nuevo sistema de coordenadas y representando ese punto bajo el sistema de coordenadas original. -> Como se vería \(\vec v\) en la nueva base? desde un punto de vista de la base original.
    La matriz transforma un vector siguiendo en el lenguaje de la base original.
    Ej: Si \(\vec v\) es (1,2) en el sistema cartesiano típico y aplicamos la matriz de cambio de base, un vector (1,2) bajo otros ejes se ubicaría en otro punto del espacio. Qué punto es ese bajo el sistema cartesiano? Es (1,2) en el nuevo, pero queremos saber su equivalente en el sistema original.
  • Por otra parte si queremos saber que coordenadas tomaría el vector \(\vec v\) bajo otra base debemos multiplicar por la inversa de la transformación. Transforma el vector al lenguaje de la nueva base. Responde a la pregunta. Qué coordenadas toma el punto V_1, V_2 del espacio en el sistema nuevo?  
  • Para aplicar una transformación a otra base conviene llevar el vector a transformar a la base original, transformar y reconvertir a la nueva base. $$ [A]^{-1}[T][A]\vec v = \vec v^{*}$$
    A lo expresa en términos de la base original, luego se le aplica la transformación T y luego se lo devuelve al lenguaje de la nueva transformación.

   

Eigenvalues y Eigenvectors (autovalores y autovectores)

  • !!! Al aplicar una transformación lineal a un espacio algunos vectores no cambian de dirección, solo se estiran o contraen pero sobre la misma recta. El resto sí se mueve. Los que se mantienen son los eigenvectors, y su factor de expansión o contracción es su eigenvalue.
    Si A es la matriz de transformación, \(\vec v\) es un eigenvector y \(\lambda\) su eigenvalue. $$ A\vec v = \lambda \vec v$$
  • Si una transformación es una matriz diagonal, lo único que hace es estirar \(\hat \imath \text{y } \hat \jmath\) por lo tanto los vectores base son eigenvectors y la diagonal son los eigenvalues.
  • Si cambiamos la base a una formada por los eigenvectors (que spanean el espacio) de la matriz podemos aplicar la transformación (la matriz original de donde salieron los eigenvectors) a esta nueva base y solo la va a estirar, por lo tanto es una transformación diagonal, que permite calculos mucho más fácil. Después habría que volver a la base original.
    A -> Matriz de transformación E -> Matriz de autovectores que forman la nueva base \(\begin{bmatrix} e_11 & e_21 \\ e_12 & e_22 \end{bmatrix}\) D -> Matriz Diagonal cuyos valores son los eigenvalues $$ E^{-1}AE=D$$

E cambia la base a eigenvectors (expresado en la base original), A aplica transformación y E-1 lo lleva al lenguaje de la nueva base (queda expresado en las nuevas coordenadas)    

Espacios Vectoriales Abstractos

  • !!! Ver funciones como un tipo especial de vectores.
  • Las funciones se pueden sumar y escalar \(f(x) + g(x) \text{y } 2f(x)\)
  • Existen transformaciones lineales de funciones, convierten una función en otra. También conocidas como "operadores"
  • Para que una transformación sea lineal tiene que cumplir aditividad y mulitplicación por escalar
    $$ L(\vec v + \vec w) = L(\vec v) + L(\vec w)$$ $$ L(c\vec v) = cL(\vec v)$$
  • En general cualquier espacio que cumpla los axiomas los espacios vectoriales puede ser considerado uno.

Funciones de Probabilidad y Distribucion

Variables Aleatorias

Consideremos un experimento cuyo espacio muestral denominaremos S.
Una funcion valuada en el dominio de los reales definida en S es una variable aleatoria.

En otras palabras es una función que asigna a cada resultado posible de un experimento un valor real.

Por ejemplo:

Si el experimento es lanzar una moneda 10 veces hay 210 combinaciones posibles de caras (o) y cruz (x).
Si definimos la variable aleatoria X como cantidad de caras entonces X(s) será la cantidad de caras del experimento.
Si s resulta ser la secuencia ooxxxoxxxo entonces X(s) = 4.

Distribucion de una variable aleatoria

Si tenemos la distribución de probabilidad del espacio muestral del experimento podemos determinar la distribución de probabilidad de cualquier variable aleatoria válida.

Volviendo al ejemplo de la moneda. Dijimos que hay 210 combinaciones de cara o cruz. La cantidad de combinaciones de X caras en 10 lanzamientos es \(P(X = x) = \binom{n}{x} \frac{1}{2^{10}}\) para \(x = 0,1,2,..,10\)

Distribuciones Discretas

Una variable aleatoria tiene una distribución discreta si solo puede tomar valores de una secuencia (generalmente finita pero puede no serlo).

  • La función de probabilidad le otorga una probabilidad puntual a cada valor de esa secuencia.
  • Los valores por fuera de la secuencia tienen probabilidad = 0
  • La suma de todas las probabilidades tiene que ser 1

Distribución Uniforme

En el caso de la dsitribución uniforme, supongamos que la variable puede tomar valores de 1 a k. La función de probabilidad será \(f(x) = \frac{1}{k}\) para x = 1,2,...,k. Y 0 para todos los otros valores.

si k = 10
Los valores de la variable serán cualquier entero entre 1 y 10
Cada valor tendrá probabilidad \(\frac{1}{10}\)

Distribución Binomial

En el caso de la dsitribución binomial se asumen dos posibles resultados, uno con probabilidad p y su contraparte con probabilidad 1-p.
Por ejemplo la probabilidad p de que una máquina genere un producto defectuoso y 1-p de que sea no defectuoso.
Si una máquina produce n productos va a generar X productos defectuosos. La variable aleatoria X tendrá una distribución discreta y sus posibles valores irán de 0 a n.
Para cualquier valor de x (entre 0 y n), la probabilidad de que la máquina genere x productos defectuosos entre los n producidos (de una secuencia particular) es \(p^{x}q^{(n-x)}\)
Como existen \(\binom{n}{x}\) distintas secuencias posibles con x defectuosos entre los n productos tenemos que:
\(Pr(X = x) = \binom{n}{x}p^{x}q^{(n-x)}\)
La función de probabilidad será \(f(x) = \binom{n}{x}p^{x}q^{(n-x)}\) para x = 0,1,2,...,n. Y 0 para todos los otros valores.

Para usar esta distribución en R tenemos los siguientes comandos:

  • Para generar n escenarios al azar donde se producen size productos con probabilidad p de ser defectuosos. El resultado es la variable x por escenario. Es decir la cantidad de defectuosos.
    En el primer escenario x = 0, en el segundo x = 1 y así.
set.seed(1)
rbinom(n = 10, size = 5, p = 0.2 )
##  [1] 0 1 1 2 0 2 3 1 1 0
# random binomial
  • Para saber la probabilidad de obtener x productos defectuosos si una máquina produce size productos y la probabilidad de que produzca un defectuoso es prob.
    Hay probabilidad de 0.0264 de obtener 5 defectuosos si producimos 10 con probabilidad 0.2.
dbinom(x = 5, size = 10, prob = 0.2)
## [1] 0.02642412
  • Para saber la probabilidad acumulada de obtener q o menos productos defectuosos si la máquina fabrica size objetos, con probabilidad de defecto prob. Hay probabiliad de 0.879 de obtener 3 o menos defectuosos si la máquina produce 10 objetos con probabilidad 0.2 de defecto. Es decir, es la suma de obtener exactamanete 0 defectuosos, más exactamente 1 defectuoso, más exactamnente 2 defectuosos, más exactamente 3 defectuosos.
pbinom(q = 3, size = 10, prob = 0.2)
## [1] 0.8791261

Distribuciones Continuas

Una variable aleatoria X tiene una distribución continua si existe una función f definida en los reales tal que para algún intervalo A
\(Pr(X \in A) = \int_{A} f(x)\)

La función f es la función de densidad de probabilidad. PDF por sus siglas en inglés.
La probabilidad de que X tome algún valor en un intervalo se encuentra integrando f en ese rango.

Por ejemplo para la distribución uniforme en un intervalo (a,b) podemos ver que su pdf (o función de densidad de probabilidad) es
\(f(x) = \begin{cases}\frac{1}{b-a} & \text{para } a \leq x \leq b \\ 0 & \text{resto}\\ \end{cases}\)

Distribución Normal

Para la distribución Normal tenemos los siguientes comandos:

  • Para obtener n variables aleatorias provenientes de una normal con media mean y desvío sd
set.seed(1)
rnorm(n = 5, mean = 10, sd = 2)
## [1]  8.747092 10.367287  8.328743 13.190562 10.659016
  • Para obtener el valor de la pdf de la normal para algún valor de X en particular. Recuerden que no es una probabilidad, solo es el valor de la función. Las probabilidad se encuentra integrando la función en el intervalo deseado.
    Si graficáramos los valores de dnorm para el intervalo -3,3 obtendríamos la forma típica de la normal.
dnorm(0.5, mean = 0, sd = 1)
## [1] 0.3520653
  • Para obtener la probabilidad acumulada hasta determinado punto. También conocido como Función de Distribución o Función de Distribución Acumulada C.D.F. por sus siglas en ingles Por ejemplo, cual es la probabilidad de obtener un valor igual o menos a 1.5 si tomamos una muestra de una normal estándar

\(N \sim (0,1)\)

pnorm(q = 1.5, mean = 0, sd = 1)
## [1] 0.9331928

Hay 93.31% de chances de obtener un valor inferior a 1.5 si tomamos una muestra al azar de una normal con media 0 y desvío 1.

  • La inversa también se puede calcular facilmente en R. Que valor debe tomar la variable aleatoria normal si deseo tenes un 93.31% de chances de obtener un valor menor o igual a ese?
qnorm(p = 0.9331, mean = 0, sd = 1)

## [1] 1.499284
La diferencia respecto al código anterior es porque redondeamos la probabilidad.

Introduccion a graficos con mapas

Data

Vamos a ver un ejemplo sencillo para representar información visualmente sobre mapas. En este caso un pequeño dataset de incendios forestales en Argentina de 2012 a 2015. La idea es usar ggplot y mantener el enfoque de gráficos por capas.

Vamos a necesitar.

> tidyverse
> rgdal
> rgeos
Tendremos como input las provincias, departamento, cantidad de focos por incendio, area afectada y año de inicio y fin. Cada observación es un incendio.

Para este ejemplo nos vamos a centrar en las provincias, los focos y su efecto sin importar la fecha.

Empezamos cargando la data.

library(tidyverse)
# Load Raw data
raw <- read.csv("../../static/post/2019-03-30-introduccion-a-graficos-en-mapas/focosincendio.csv", sep = ";")
raw <- as.tibble(raw)
## Warning: `as.tibble()` was deprecated in tibble 2.0.0.
## i Please use `as_tibble()` instead.
## i The signature and semantics have changed, see `?as_tibble`.
glimpse(raw)
## Rows: 120
## Columns: 11
## $ pais_id         <int> 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,~
## $ pais            <chr> "Argentina", "Argentina", "Argentina", "Argentina", "Argentina", "Argentina", "Argentina", "Argentina", "Argentina", "Argentina~
## $ provincia_id    <int> 6, 14, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 30, 30, 30, 30, 30, 30, 30, 30, 30, ~
## $ provincia       <chr> "Buenos Aires", "Córdoba", "Corrientes", "Corrientes", "Corrientes", "Corrientes", "Corrientes", "Corrientes", "Corrientes", "C~
## $ departamento_id <int> 833, 14, 56, 28, 28, 28, 70, 84, 84, 84, 112, 112, 119, 119, 126, 147, 154, 154, 161, 168, 168, 168, 168, 8, 15, 15, 15, 15, 28~
## $ departamento    <chr> "Tres Arroyos", "Calamuchita", "General Alvear", "Concepción", "Concepción", "Concepción", "Goya", "Itizaingó", "Ituzaingo", "I~
## $ sup_afectada    <dbl> 2400.00, 50.00, 257.00, 130.00, 5.00, 146.00, 30.00, 294.30, 378.00, 158.00, 300.00, 450.00, 450.00, 15.00, 20.00, 141.00, 295.~
## $ uni_med_id      <chr> "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "ha", "~
## $ cant_focos      <int> 1, 1, 1, 2, 1, 1, 1, 3, 1, 3, 1, 1, 5, 2, 2, 1, 3, 1, 1, 18, 2, 3, 2, 1, 0, 0, 1, 7, 0, 2, 1, 0, 0, 0, 5, 10, 10, 3, 1, 1, 1, 1~
## $ año_inicial     <int> 2014, 2015, 2012, 2012, 2013, 2015, 2012, 2012, 2013, 2014, 2012, 2013, 2012, 2013, 2015, 2012, 2012, 2014, 2012, 2012, 2013, 2~
## $ año_final       <int> 2014, 2015, 2012, 2012, 2013, 2015, 2012, 2012, 2013, 2014, 2012, 2013, 2012, 2013, 2015, 2012, 2012, 2014, 2012, 2012, 2013, 2~

Exploramos un poco el dataset. Lo que nos vas a interesar represetnar es la segunda parte del código. Variables agregadas a nivel provincia.

# Generate Summary to Explore
sum_year <- raw %>% group_by(año_inicial) %>%
  summarise(focos = sum(cant_focos), sup = sum(sup_afectada, na.rm = TRUE)) %>%
  mutate(sup_prom = sup/focos)

# Actual data to be plotted
sum_prov <- raw %>% group_by(provincia) %>%
  summarise(focos = sum(cant_focos), sup = sum(sup_afectada, na.rm = TRUE)) %>%
  mutate(sup_prom = sup/focos) %>%
  arrange(desc(sup_prom)) %>%
  mutate(provincia = as.character(provincia))

head(sum_prov)
## # A tibble: 6 x 4
##   provincia    focos   sup sup_prom
##   <chr>        <int> <dbl>    <dbl>
## 1 Buenos Aires     1 2400    2400  
## 2 Corrientes      55 5440.     98.9
## 3 Misiones        10  679      67.9
## 4 Córdoba          1   50      50  
## 5 Entre Ríos      12  594      49.5
## 6 Jujuy           62 2721.     43.9

Tenemos por provincia la cantidad de focos y la superficie afectada. Vamos a usar la superficie promedio por foco para visualizar la magnitud de cada foco de incendio.

Ahora llega lo importante. ¿Cómo representar esta data provincial en un mapa de Argentina?

Mapa

Necesitamos un ShapeFile de Argentina, que basicamente es el tipo de archivo que se usa para representar mapas en gráficos. Contiene divisiones del país (provincias) con sus respectivas coordenadas y nombres.

Utilizaremos data descargada del siguiente link. url <- "http://biogeo.ucdavis.edu/data/diva/adm/ARG_adm.zip"

Lo descargué y deszipee en la computadora. Lo leemos con una librería particular RGDAL.

dsn contiene la ruta a la carpeta con los archivos del shapefile. Layer apunta a al set de archivos que contiene la data que queremos. Generalmente hay otros sets con información no relevante al gráfico.

argentina <- rgdal::readOGR(dsn = "../../static/post/2019-03-30-introduccion-a-graficos-en-mapas/ARG_adm", layer = "ARG_adm1", use_iconv=TRUE, encoding='UTF-8')
## OGR data source with driver: ESRI Shapefile 
## Source: "D:\DataScience\StatsBlog\blogStats\static\post\2019-03-30-introduccion-a-graficos-en-mapas\ARG_adm", layer: "ARG_adm1"
## with 24 features
## It has 9 fields
## Integer64 fields read as strings:  ID_0 ID_1

Es un archivo S4 por lo que se utiliza "@" para acceder a su contenido. Por ejemplo:

head(argentina@data)
##   ID_0 ISO    NAME_0 ID_1                 NAME_1           TYPE_1        ENGTYPE_1 NL_NAME_1
## 0   12 ARG Argentina    1           Buenos Aires        Provincia         Province      <NA>
## 1   12 ARG Argentina    2                Córdoba        Provincia         Province      <NA>
## 2   12 ARG Argentina    3              Catamarca        Provincia         Province      <NA>
## 3   12 ARG Argentina    4                  Chaco        Provincia         Province      <NA>
## 4   12 ARG Argentina    5                 Chubut        Provincia         Province      <NA>
## 5   12 ARG Argentina    6 Ciudad de Buenos Aires Distrito Federal Federal District      <NA>
##                                                                               VARNAME_1
## 0                                                                   Baires|Buenos Ayres
## 1                                                                               Cordova
## 2                                                                                  <NA>
## 3                                                        El Chaco|Presidente Juan Peron
## 4                                                                                  <NA>
## 5 BUENOS AIRES D.F.|Capital Federal|Distretto Federale|Distrito Federal|Federal Capital

Este tipo de archivos tiene una estructura complicada y hay varias librerías útiles. Con el fin de mantenernos dentro del tidyverse usaremos el enfoque de ggplot por capaz para graficar. Primero necesitamos llevar la información del shapefile a un dataframe.

# Transformo a dataframe.
argentina_df <- broom::tidy(argentina)
## Regions defined for each Polygons
# id es la provincia
head(argentina_df)
## # A tibble: 6 x 7
##    long   lat order hole  piece group id   
##   <dbl> <dbl> <int> <lgl> <fct> <fct> <chr>
## 1 -60.2 -33.3     1 FALSE 1     0.1   0    
## 2 -60.2 -33.3     2 FALSE 1     0.1   0    
## 3 -60.2 -33.3     3 FALSE 1     0.1   0    
## 4 -60.2 -33.3     4 FALSE 1     0.1   0    
## 5 -60.2 -33.3     5 FALSE 1     0.1   0    
## 6 -60.2 -33.3     6 FALSE 1     0.1   0

Ahora genero un diccionario de ids con su respectiva provincia para poder linkear mi mapa con la data de incendios.

ids <- cbind.data.frame(provincia = as.character(argentina@data$NAME_1), id = as.character(rownames(argentina@data))) %>%
  mutate(provincia = as.character(provincia), id = as.character(id))

El dataframe generado a partir del shapefile tiene las coordenadas que forman cada provincia y el id. Ahora lo que haremos es pegarle la data de incendios para poder utilizarla sobre el mapa. Está generado el log de los focos porque en una prueba intenté usar la data transformada para suavizar outliers pero no se verá en esta versión.

# Agrego provincia por ID y data de incendios a la data del shapefile.
argentina_df2 <- argentina_df %>% left_join(ids, by = "id") %>%
  left_join(sum_prov, by = "provincia") %>%
  mutate(focos = ifelse(is.na(focos),0.5,focos),
         logfocos = log(focos), 
         logfocos2 = logfocos - min(logfocos))

Por ahora vamos a poder ser capaces de graficar un mapa de argentina con sus provincias delimitadas y pintarlas según la cantidad de focos de incendio por ejemplo. Pero además vamos a querer agregar alguna forma sobre cada provincia. Por ejemplo un punto de distinto tamaño según el area afectada por cada foco en promedio. Para eso necesitamos localizar el centroide de cada poligono, es decir, el centro de cada provincia. Para ellos usamos la librería RGEOS Luego a cada centro le agrego la data que voy a querer usar. En este caso la superficie promedio afectada por foco de cada provincia.

# Calculo el centro de cada poligono (provincias)
# para obtener el "centro" donde iran los puntos o nombres.
centros <- rgeos::gCentroid(argentina, byid = TRUE) %>%
  as.data.frame() %>%
  mutate(id = rownames(.))

# Agrego data relevante para el ploteo (superficie promedio)
centros2 <- centros %>% left_join(ids, by = "id") %>%
  left_join(sum_prov, by = "provincia") %>%
  mutate(focos = ifelse(is.na(focos),0.5,focos),
         sup = ifelse(is.na(sup),0,sup),
         log_sup_prom = log(sup_prom),
         sup_prom_sin_outlier = ifelse(sup_prom > 150, 150,sup_prom) ) # esto es para suavizar el outlier. Buscar otro enfoque.

Grafico!

Ya tenemos todo. Tenemos el mapa, tenemos la cantidad de focos de incendio por provincia y tenemos el centro de cada provincia donde vamos a incluir un punto que muestra la intensidad de los incendios. Simplemente graficamos sigueindo la lógica por capas de ggplot. Las provincias sin puntos son aquellas que no tuvieron ningún incendio.

ggplot() +
  geom_polygon(data = argentina_df2, aes(x=long, y = lat, group = group,  fill = focos), color = "white") + # mapa de argentina
  # coloreado segun cantidad de focos
  coord_fixed(0.8) + # tamañp del mapa
  scale_fill_gradient2("Cantidad de Focos de incendio", low = "white", mid = "lightgreen", high = "darkred") + # escala de colores para focos
  geom_point(data = centros2, aes(x = x, y = y, size = sup_prom_sin_outlier)) + # puntos por provincia con superficie promedio
  scale_size(name = "Superficie Promedio Afectada (ha)",range = c(1,5)) + # escala de los puntos
  guides(fill = guide_legend(order = 1), # Orden de los leyendas a la derecha.
         size = guide_legend(order = 2)) # Por algun motivo esto discretizo la leyenda de focos

Image

Como correr un proyecto con R en Google Cloud

Hay situaciones en que nuestras computadoras no alcanzan para correr ciertos algoritmos por la cantidad de memoria o núcleos que tenemos (y comprar otra no es una opción..). Una solución es correr nuestro proyecto en "la nube", es decir en servidores ajenos mantenidos por empresas. Los servicios de esta índole más conocidos son:

  • Google Cloud
  • Amazon AWS
  • Microsoft Azure

En este post usaremos el primero. Como es de esperar, estos servicios son pagos y si su negocio lo amerita son una gran opción. Igualmente Google Cloud ofrece U$S300 de regalo al crear una cuenta por lo que podrían hacer uso para algún proyecto o pruebas. Les aseguro que no es particularmente bajo el monto. Solo tienen que registrarse y asociar una tarjeta de crédito y no abonar nada.

Google Cloud ofrece un montón de servicios y opciones de las cuales presentaremos lo más básico pero igualmente suficiente para correr un xgboost en gigas y gigas de datos con cientos de variables je.

  1. Crear una máquina virtual instalando R
  2. Crear un Bucket que sirve como Disco duro para guardar data, outputs, etc
  3. Correr un algoritmo

Máquina Virtual

Una vez registrados, lo primero que vamos a hacer es crear la máquina virtual e instalar R y ciertos paquetes. Lamentablemente el proceso es bastante engorroso para quienes no conocen bash ni están familiarizados con Cloud. Es todo por consola y para nada intuitivo sin leer la documentación, que está en inglés. Es un proceso largo y la documentación más clara que encontré se encuentra en este BLOG. No tiene sentido intentar decir lo mismo que él pero peor. Les recomiendo seguirlo y van a obtener una máquina virtual con Rstudio instalado y ciertas dependencias útiles para la mayoría de las librerías que se usan.

Crear Bucket

Ir a Clickear los 3 puntos a la derecha y entrar a "Create key"

Se les descargará un archivo .json que deben renombrar a ** privatekey_inicial.json ** . Luego ir a:

Crear un bucket cuyo nombre no tenga espacios ni caracteres raros. Va a pedirles que sea un nombre que no esté siendo usado por nadie más. Hecho esto el bucket está creado y se le pueden subir archivos. Luego desde los scripts también se le van a poder escribir directamente, es decir, guardar los outputs. Ahora vamos a cambiar los permisos del bucket. Copiarse el account id desde aquí.

Luego ir a storage y en los 3 puntitos de nuesto bucket clickear "Edit Bucket permissions" y pegar el ID.

Crear Imagen

Lo que haremos ahora es crear una imagen de la maquina virtual que generamos (donde instalamos R) para poder levantar futuras máquinas y que directamente tengan instalado R y los paquetes si lo deseamos (invocando a esta imagen). Dejo imágenes de un tutorial que van a ser más claras que yo.

Luego ir a Compute Engine → Images

Status

Resumiendo brevemente:

  • Tenemos una cuenta y U$S 300 disponibles.
  • Creamos una VM con R y paquetes.
  • Generamos una imagen de esa VM para poder retutilizarla.
  • Creamos un bucket donde almacenar inputs y outputs, linkeado a la imagen.

Estamos casi listos. Falta correr algun script!

Cómo correr un script de R.

Una vez seteado todo lo anterior (y asegurándonos de haber apagado la maquina virtual utilizada) lo que debemos hacer es crear otras VM (instance) con los núcleos y Ram que cremos convenientes - esto depende totalmente de la complejidad del algoritmo que vayan a correr-, asignando una región donde preferentemente sea de noche o fin de semana para que esté menos saturada. Lo más importante es cambiar el boot disk y seleccionar dentro de "custom" la imagen que hayamos generado con R. De esta manera la VM que iniciemos ya tendrá R, Rstudio y librerías instaladas.

Justo debajo de lo que se ve en la imagen hay opciones extras de "Management, security, disks", etc. Depende la importancia de lo que estén haciendo puede ser bueno asegurarse que la opción Preemptibility esté OFF. Si está ON, la VM se correrá en un servidor que máximo puede durar 24hs (posterior a eso se apaga automáticamente la VM) y más importante aún, están sujetos a disponibilidad de Google, es decir que si hay mucha demanda de servidores pueden apagarles el suyo sin consultar. Lo positivo es que son mucho más baratas. Al tenerlo OFF, se aseguran que su VM estará encendida durante todo lo que tarde el script en correr y no va a depender de la demanda. Queda a criterio de cada uno.

Ya estamos listos para correr. Hay dos maneras sencillas:

  1. Directo desde Rstudio en la VM.

Haciendo click en el IP de su instancia. Se abrirá Rstudio y deberán poner Usuario y Contraseña seteados durante la instalación. Ahi pueden trabajar como si fuera directamente R. Recomiendo tener Script listo porque no es muy dinámico trabajar en vivo ahí.

  1. Desde la terminal, llamando a script en el bucket.

Otra opción es subir su script al bucket que generaron y en Rstudio (como en 1.) ir a la terminal y correr:

Rscipt --vanilla ~/cloud/cloud1/pathToScript/script.r

O desde la consola (que basicamente system() simular ser la temrinal) :

system(Rscipt --vanilla ~/cloud/cloud1/pathToScript/script.r)
  1. Desde la terminal de la instancia.

Y ahí deberían poder correr sin problema.

Rscipt --vanilla ~/cloud/cloud1/pathToScript/script.r

En cualquiera de los 3 casos presten atención a las rutas que usan en sus scripts para referenciar al bucket. Si linkearon el bucket a la imagen de la VM como vimos en el post deberían poder usar la siguiente ruta genérica.

"~/cloud/cloud1/RestoDelPath/"

Y eso es todo. Ya pueden levantar una VM en google cloud y correr algoritmos con gigas y gigas de data sin quemar su PC!

Organización de Proyectos

Este va a ser un breve post sobre cómo organizar los proyectos que hagan en R. Es al día de hoy la que utilizo y si googlean van a ver que existe y es usada en el ámbito. Puede ser esta tal cual o alguna alternativa similar.

El enfoque es muy sencillo e intuitivo. La idea es tener por separado cada componente del proyecto y de manera clara y segmentada para poder acceder rápidamente a lo necesario, ya sea código, datos, outputs, etc.

Primero y principal. CREEN UN PROYECTO en R. Esto va a facilitar todo el manejo de rutas, llamados a otros códigos y mismo para compartir si es necesario con otra gente ya que las rutas que utilicemos serán relativas a la ubicación en Disco del proyecto. Por lo tanto si el proyecto está creado en C:\ProyectoR y en algún codigo llamamos a read.csv("/Datos/datos.csv") esto funcionará en cualquier computadora donde el proyecto tenga en su directorio una carpeta "Datos", independientemente de la ruta donde se encuentre. Yo puedo mover toda la carpeta del proyecto a C:\OtraRuta y ejecutar ahí el read.csv sin tener que actualizar la ruta. Es uno de los problemas más básicos y molestos al trabajar con códigos ajenos o mover nuestros proyectos de lugar.

Carpetas

Una vez creado el proyecto, lo que sugerimos es crear una estructura de carpetas como la que se ve en la imagen siguiente.

DATA

Contiene la data que será el input de nuestros proyecto. A su vez podemos guardar archivos intermedios que hayamos ido procesando. Pueden adaptarlo como prefieran pero sugerimos guardar en "raw",la data que será input del análisis, ya sean csvs, txts, htmls, etc. En "working"" ir guardando objetos importantes o que lleven tiempo de procesar asi se pueden leer directamente en vez de tener que correr el código nuevamente en una próxima sesión. Para ellos se usa el comando saveRDS(). En "final" guardar los objetos finales del análisis.

DOCS

Acá guardamos archivos auxiliares útiles como diccionarios de variables, links a webs, consignas, documentación, etc.

OUTPUT

Acá exportamos los resultados del análisis, desde gráficos que vayamos a usar en el reporte, el informe final que hagamos (PDF, HTML,etc), las conclusiones que saquemos, etc. Dependiendo de la complejidad del proyecto puede separar en carpetas al interior si hay outputs muy variados.

SRC

Carpeta para todo nuestro código. Algunos eligen no usarla y dejar los códigos en la ruta del proyecto pero me parece un poco desprolijo. Recomendamos tener muchos scripts con títulos claros y segmentados por lo que hacen. Es decir, uno para levantar la data, otro para análisis exploratorio, otro para feature engineering y así. A su vez, recomendamos tener un script propio para la funciones que definan ustedes y si les resulta cómodo otro para las librerías, de manera tal de tener todo claro, separado y no tener que andar buscando dentro de un grán código lo que necesitan. Además es más sencillo para modificar y arreglar bugs. Recuerden que para invocar código de otro script simplemente lo corren usando source("Script.R"). Dejamos un ejemplo ilustrativo.

GITHUB

En algún otro post lo dijimos pero recomendamos altamente UTILIZAR GITHUB para manejar sus proyectos, tener backups, compartirlos y actualizarlos desde cualquier computadora! Y hacer blogs como este siguiendo este POST

Lo que también sugerimos es no subir la carpeta data a github por dos motivos. Primero por una cuestión de espacio, si tienen data muy pesada Github no les va a permitir incluirla en el repositorio. Por otra parte si la data es confidencial o tiene datos privados mejor que no esté a disposición de cualquiera si tienen cuenta pública. Obviamente queda a criterio y comodidad de cada uno si corresponde subir la data o no. Para evitar que una carpeta sea subida a github solo deben incluirla en el archivo .gitignore de su repositorio.

Curvas ROC

La curva ROC y AUC (area bajo la curva) permiten evaluar la eficacia de un modelo clasificador y elegir el mejor umbral de corte donde determinar qué observación es predicha positiva y cual negativa.

Vamos a generar rapidamente un clasificador con regresión logísitca utilizando el dataset mtcars ya provisto por R. Solo a modo ilustrativo utilizaremos AM (caja manual o automática) como la variable a predecir y mpg y drat como independientes. No separamos en train y test dadas las pocas observaciones.

library(tidyverse)
library(modelr)
library(pROC)
df <- mtcars %>% select(am, mpg, drat) %>% mutate(am = as.factor(am))
summary(df)
##  am          mpg             drat      
##  0:19   Min.   :10.40   Min.   :2.760  
##  1:13   1st Qu.:15.43   1st Qu.:3.080  
##         Median :19.20   Median :3.695  
##         Mean   :20.09   Mean   :3.597  
##         3rd Qu.:22.80   3rd Qu.:3.920  
##         Max.   :33.90   Max.   :4.930
# Clase dentro de todo balanceada

mdl.log <- glm(formula = am ~., data = df, family = binomial(link="logit"))
fit <- predict(mdl.log, newdata = df, type = "response")


roc(df[,1],  fit , percent=F,   boot.n=1000, ci.alpha=0.9, stratified=FALSE, plot=TRUE, grid=TRUE, show.thres=TRUE, legacy.axes = TRUE, reuse.auc = TRUE,
    # print.thres = c(0.30,0.35, 0.40, 0.45,0.48, 0.50,0.55, 0.60),#
    print.auc = TRUE, print.thres.col = "blue", ci=TRUE, ci.type="bars", print.thres.cex = 0.7, main = paste("ROC curve using","(N = ",nrow(df),")") )

Image

## 
## Call:
## roc.default(response = df[, 1], predictor = fit, percent = F,     ci = TRUE, plot = TRUE, boot.n = 1000, ci.alpha = 0.9, stratified = FALSE,     grid = TRUE, show.thres = TRUE, legacy.axes = TRUE, reuse.auc = TRUE,     print.auc = TRUE, print.thres.col = "blue", ci.type = "bars",     print.thres.cex = 0.7, main = paste("ROC curve using", "(N = ",         nrow(df), ")"))
## 
## Data: fit in 19 controls (df[, 1] 0) < 13 cases (df[, 1] 1).
## Area under the curve: 0.9433
## 95% CI: 0.8695-1 (DeLong)

Básicamente entrenamos un modelo logístico y graficamos la curva ROC prediciendo sobre el mismo dataset con el que fue entrenado. No es lo adecuado pero dadas las pocas observaciones y el propósito explicativo no lo tomamos como un problema. La curva ROC es la más oscura y como vemos empieza en (0,0) y termina en el (1,1). El eje X es 1 - Especificidad (Falsos Negativos) y el eje Y es Sensitividad (Verdaderos Positivos) por lo tanto lo deseable es estar lo más arriba a la izquierda posible. El punto (0,1) sería óptimo ya que habría 0 falsos negativos y 100% de verdaderos positivos.

Lo que representa la curva es la combinación de Sensitividad y (1 - especificidad) para varios puntos de corte. Recordemos que la regresión logística devuelve un valor entre 0 y 1 por lo tanto hay que determinar en qué valor empezamos a considerar una predicción como positiva o negativa. En este caso positivo sería tener un valor de 1 en am, por lo tanto tener caja automática. Cada punto de la curva corresponde a algún punto de corte. Como decíamos antes, el mejor debería ser el más "arriba a la izquierda" aunque depende el problema eso puede cambiar, dependiendo del costo de equivocarse en uno u otro sentido.

El peor escenario es que la curva siga a la diagonal, lo que equivaldría a ser iguales a un modelo eligiendo siempre la clase mayoritaria, totalmente inútil. Si estuviera por debajo de la diagonal, sería peor aún, pero bastaría con invertir las predicciones para pasar a estar por encima. Un viejo truco no muy científico.

El área bajo la curva (AUC) es una medida resumen de la curva ROC ya que justamente describe el área entre la curva ROC y la diagonal. Valores mayores se corresponden con curvas ROC más alejadas de la diagonal y por lo tanto que separan mejor a la clase dependiente. Es útil para comparar modelos.

Como crear un blog con Blogdown y Netlify

En este post vamos a ver el proceso reusmido para crear un blog donde podemos generar contenido directamente desde RStudio, utilizando el paquete blogdown, Github y Netlify. La idea es que al finalizar la configuración, simplemente creemos el post en Rstudio (un markdown) usando blogdown y que al subirlo a github automaticamente se actualice el blog y se vea reflejado en nuestra página. Es lo que estoy haciendo en este momento.

Lo primero y más importante más allá de este tema en particular es tener una cuenta en github. Si no la tienen se los recomiendo ampliamente para hacer version control - actualizar código de forma segura y con backups constantes en un servidor + compartir proyectos. Es gratis, al menos la versión básica que alcanza y sobra para el uso cotidiano. En nuestro repositorio crearemos un proyecto para el blog y ahi se subirán nuestros posts en formato html. A su vez, estará el theme y otras configuraciones básicas del blog.

Lo segundo es instalar el paquete blogdown en nuestro R. Es una obra maestra de Yihui Xie, ingeniero de RStudio y creador de varios paquetes. En un nuevo proyecto de R, ejecutan el siguiente comando.

blogdown::new_site()

Con eso ya tienen generado la estructura básica de lo que será su blog. Se crearán carpetas y archivos en la ruta del proyecto con contenido de prueba para tener algo funcional. Blogdown es bastante complejo y hay un millón de configuraciones y detalles que uno puede personalizar. No entraremos en eso acá porque se haría super extenso. Para eso está la documentación oficial en ingles.

Eventualmente van a tener que cambiar el archivo config.toml con ciertos pasos de la guía y luego podrán explorar todas las posibilidades que presenta. Entre ellas pueden (y recomiendo) descargar otro theme para cambiar el formato. El que uso actualmente es tranquilpeak.

Para crear un post nuevo simplemente escriben el comando.

blogdown::new_post()

Lo cual genera un script con un YAML, que es la configuración con el título, tags y otros metadatos del post. Simplemente escriben como cualquier markdown debajo. Cuando terminan el post (o mientras para ir visualizando como queda) corren

blogdown::serve_site()
Lo cual generará el archivo html correspondiente que luego será usado en su web y les permite ver el resultado temporal de su post.

Luego lo que deben hacer es pushear la carpeta que se les generó del blog a su repositorio en github. O al menos las carpetas y archivos que se ven en la siguiente imagen. Public no la pusheen.

gitsnap

Llegado a este punto tenemos el contenido inicial del blog, pero no hay sitio web. Ahí es donde entra en juego Netlify. No vamos a entrar en el paso a paso minucioso pero básicamente deben crearse una cuenta, generar una nueva web y linkearla a su repositorio Github. Es bastante lineal. Luego configuran el nombre de la web y otros detalles y en cuestión de minutos ya están publicados! (Y Gratis.) Para ver bien esta etapa les recomiendo la explicación del link de blogdown.

Una vez puesto en marcha solo es cuestión de abrir su proyecto en R (en su pc), crear un nuevo post con blogdown::new_post() y pushear a github! Prueben chusmeando todas las configuraciones para cambiar la estética del blog!

Hola MAP. Chau Apply

Introducción

La idea de este post es introducirlos a la familia de funciones MAP, propias de tidyverse. A grandes rasgos son un remplazo MUY útil a la familia de funciones APPLY, propias de R base. Estas últimas se suelen enseñar en todos los cursos introductorios de R, como la manera correcta de aplicar funciones a listas o columnas de dataframes. No es que no sirvan, pero dado el surgimiento de tantas librerías que facilitan el manejo de la data, no tiene sentido seguir insistiendo con ellas dado que hay nuevas con mayor flexibilidad, muy sencillas de utilizar y mucho más amenas.

  • Lo mejor que tienen las funciones MAP es:
    • Consistencia en los inputs.
    • Flexibilidad del output.
    • Integración con todo el universo tidyverse y prolijidad.

Empecemos.

library(purrr) # MAP está contenida acá
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.0.5

Como regla general, MAP aplica funciones a elementos de una lista o de un vector. Su output es otra lista. Muy similar a lapply().

l1 <- list( a= c(100,200), b = c(8,10))
map(l1, max)

## $a
## [1] 200
## 
## $b
## [1] 10
A cada lista le calcula el máximo y devuelve una lista con cada elemento siendo el resultado de la función.

Tenemos la flexibilidad para pasarle funciones anónimas..

map(l1, function(x) max(x))
## $a
## [1] 200
## 
## $b
## [1] 10

Aplicando funciones a elementos de un vector. Cada numero de 1 a 5 es usado como primer input de la funcion rnorm, sd y n son otros parámetros de rnorm. El resultado de nuevo es una lista.

set.seed(1)
1:5 %>% map(., rnorm,sd =2, n=5)
## [[1]]
## [1] -0.2529076  1.3672866 -0.6712572  4.1905616  1.6590155
## 
## [[2]]
## [1] 0.3590632 2.9748581 3.4766494 3.1515627 1.3892232
## 
## [[3]]
## [1]  6.023562  3.779686  1.757519 -1.429400  5.249862
## 
## [[4]]
## [1] 3.910133 3.967619 5.887672 5.642442 5.187803
## 
## [[5]]
## [1] 6.837955 6.564273 5.149130 1.021297 6.239651

Consistencia entre variantes

Por ahora solo vimos la versión de lapply en MAP, pero esta familia tiene varios integrantes.

map_if

Ejecuta la función solo si el elemento cumple determinada condición. Devuelve una lista.

l2 <- list(a = 213, b = "string", c = c(1,2))
map_if(l2, is.numeric, function(x) x*2)

## $a
## [1] 426
## 
## $b
## [1] "string"
## 
## $c
## [1] 2 4
El output es la lista original con los elementos correspondientes transformados. Vemos que no hubo ningún problema con "string" ya que fue omitido.

map_at

Ejecuta la función solo en los elementos que seleccionemos. No hace falta que cumplan alguna condición. Misma función de antes pero solo aplicada al tercer elemento. Devuelve una lista.

map_at(l2, c(3), function(x) x*2)
## $a
## [1] 213
## 
## $b
## [1] "string"
## 
## $c
## [1] 2 4

Variantes súper útiles que permiten no utilizar loops y que dan mucho control de manera sencilla sobre las funciones a ejecutar. Por otra parte, en términos de consistencia, la estructura es siempre la misma. El primer argumento es x= y luego viene la función a aplicar. En el caso de map_if y map_at entre medio surge el condicionante. Si recuerdan, la familia apply cambia el orden de los inputs según si es apply, lapply, mapply, sapply...

Flexibilidad del output

Por el momento vimos que todos los outputs eran listas. Lo interesante es que podemos controlar eso y cambiar el formato del resultado, ahorrándonos conversiones molestas con unlist y etc.

l3 <- list(c(1,2,4), c(100,200), c(5000,6000))
map_dbl(l3, max)
## [1]    4  200 6000

Nos devuelve un vector con los resultados de aplicar la función max a cada elemento!

De este mismo tipo esta.

  • map_chr # vector caracter
  • map_int # vector de integers
  • map_lgl # vector de booleanos

Cadenas de Markov

Las cadenas de Markov son herramientas muy útiles para modelar transiciones entre estados. Imaginemos un escenario sencillo con dos posibles estados: día de lluvia o día soleado. En el momento t el estado supongamos que es "día soleado". ¿Cuál será el estado en t+1 ? Dadas las probabilidades de transición de un estado a otro podemos simular escenarios tras el paso del tiempo. Mismo en algunos casos puede ser útil entender si en el largo plazo esta simulación converge hacia algún resultado estable en el tiempo.

Mostraremos ejemplos sencillos, pero la metodología es escalable a procesos complejos como pueden usarse en meteorología, aplicaciones financieras, etc. El algoritmo de búsqueda de Google está basado en cadenas de Markov, para que vean la utilidad. 1

Lo que necesitamos para este ejercicio son las probabilidades de transición de un estado a otro y los valores iniciales en cada estado.

Supongamos una clase de estadística con n estudiantes, en la que dos estados son posibles. Estado A : El alumno está atento. Estado B : El alumno está aburrido.

  • Las probablidades son las siguientes:
    • En t+1 el 80% de A sigue en A (y por lo tanto el 20% de A pasa a B)
    • En t+1 el 25% de B pasa a A (y por lo tanto el 75% de B queda en B)

La matriz que representa esa probabilidades la vamos a llamar * Matriz de Transición *.

tmatrix <-  as.matrix(c(0.8,0.25))  
# 80% de A sigue en A, 25% de B pasa a A en t+1

tmatrix <- t(tmatrix) 
# trasponemos porque necesitamos esta información en la primera fila.

tmatrix <- rbind(tmatrix, 1 - tmatrix[1,]) 
# Agregamos la segunda fila, que es 1 menos la primera.

tmatrix # Matriz de transción
##      [,1] [,2]
## [1,]  0.8 0.25
## [2,]  0.2 0.75

La matriz de valores iniciales es la siguiente: En un primero momento (t), el 10% de los alumnos está atento y el 90% aburrido.

smatrix <- as.matrix(c(0.1,0.9)) # Matriz inicial, 10% Atento, 90% aburridos

Ya tenemos toda la información necesaria para hacer nuestras primeras simulaciones. Supongamos que cada período de tiempo son 10 minutos, por lo tanto si t es el momento 0, t+1 es a los 10 de minutos de empezada la clase, t+2 a los 20 y así sucesivamente.

Para evaluar el porcentaje de alumnos concentrados en determinado momento de la clase lo que debemos hacer es multiplicar la matriz de estado inicial por la matriz de transición tantas veces como momentos a futuro querramos simular.

Por ejemplo si queremos ver que pasará con nuestros alumnos luego de 20 minutos de clase debemos multiplicar smatrix * tmatrix 2 veces.

for (i in 1:2){            # Loopeamos 2 veces. %*% es la multiplicacion matricial.
  smatrix = tmatrix %*% smatrix  
# smatrix va tomando nuevos valores en cada iteracion, 
# reflejando el movimiento de un estado a otro
}

smatrix  # Despues de 2 iteraciones -> A 42% , B 58%
##         [,1]
## [1,] 0.41775
## [2,] 0.58225

Vemos que luego de 2 transiciones el estado A está compuesto por casi 42% de alumnos (concentrados) y 58% no atentos. Son movimientos bastante rápidos de un estado a otro.

Veamos qué pasa luego de 10 iteraciones.

smatrix <- as.matrix(c(0.1,0.9)) # Reseteamos smatrix a su estado inicial

for (i in 1:10){           
# Loopeamos 10 veces. %*% es la multiplicacion matricial.
  smatrix = tmatrix %*% smatrix   
# smatrix va tomando nuevos valores en cada iteracion, 
# reflejando el movimiento de un estado a otro
}

smatrix  # Despues de 10 iteraciones -> A 55% , B 45%
##           [,1]
## [1,] 0.5544017
## [2,] 0.4455983

Luego de 10 movimientos, A pasa a tener el 55% de los alumnos dejando a 45% en B (no atentos). En este ejemplo con el paso del tiempo los alumnos se concentran más y más en la clase. Pero es este un proceso continuo e infinito? Llega un momento en que dada la matriz de transción todos los alumnos pasan a estar en el estado A, es decir, atentos?

Para analizar esto podemos ver qué sucede luego de 1000 iteraciones (sería una clase muy muy larga...)

smatrix <- as.matrix(c(0.1,0.9)) # Reseteamos smatrix a su estado inicial

for (i in 1:1000){  # Loopeamos 10 veces. %*% es la multiplicacion matricial.
  smatrix = tmatrix %*% smatrix  
# smatrix va tomando nuevos valores en cada iteracion,
# reflejando el movimiento de un estado a otro
}

smatrix  # Despues de 1000 iteraciones -> A 55% , B 45%
##           [,1]
## [1,] 0.5555556
## [2,] 0.4444444

El resultado es casi idéntico luego de 1000 iteraciones al intento de tan solo 10 iteraciones. Por lo tanto podemos ver que esta cadena de Markov converge rápidamente a 55% en A y 45% en B. Es un estado estacionario del cual no podemos movernos dada la matriz de transición que tenemos.

Este ejemplo sencillo permite ver la intuición detrás de esta potente herramienta en unos pocos pasos. Obviamente como comentamos antes se puede utilizar para procesos mucho más complejos y de muy diversas areas. Queríamos mostrar los fundamentos con una aplicación rápida y que se comprenda que lo que se requiere es una matriz de transición y un estado inicial.


  1. Para una explicación gráfica y muy didáctica dejamos el siguiente link en inglés. 

Simulación de Monty Hall

Vamos a ver en un corto y sencillo ejemplo cómo hacer una simulación en R. El caso a utilizar es el "famoso" problema de Monty Hall, asociado a un programa televisivo de Estados Unidos.

En breve, el problema consiste en que un concursante debe elegir una entre 3 puertas (A,B y C). Detrás de una hay un premio (en general un automóvil) y tras las otras dos no hay nada (o una cabra en algunos ejemplos, lo cual no me parece tan malo en realidad..). Una vez que el concursante eligió una puerta, el organizador del programa, que sabe qué puerta oculta el premio y cuáles no, abre una de las dos puertas restantes, tras la cual no hay premio (recuerden que sabe qué hay detrás de cada puerta). Ante el nuevo escenario, el concursante debe elegir si mantiene su elección original o decide cambiar por la puerta restante, es decir, la que no eligió ni la que abrió el organizador.

  • Qué conviene hacer ante tal incertidumbre?
    • Cambiar?
    • Mantenerse fiel a la decisión original sin caer en los juegos psicológicos del programa?
    • Es indistinto? 50/50 entre las dos puertas.

La simulación deberíaa darnos una respuesta acertada. Comencemos.

Generamos las puertas y un vector donde vamos a guardar el resultado de la simulación.

puertas <- c("A","B","C")
xdata   <- c()

Ahora lo que vamos a hacer es simular 10000 escenarios distintos emulando la lógica del problema. En cada uno vamos a asignarle a una puerta al azar el premio (con el comando "sample"). Luego elegiremos, como si fueramos el concursante, una puerta al azar. Descartaremos una de las puertas sin premio y por último y más importante, vamos a analizar en cada escenario qué hubiera pasado si nos quedabamos con la puerta elegida originalmente y qué hubiera pasado si cambiábamos. Si no hay diferencia entre cambiar y no cambiar, luego de 10000 simulaciones deberíamos haber ganado 5000 veces al cambiar y 5000 al no cambiar (con algún margen de error). En caso contrario, alguna de las dos estrategias es superadora.

set.seed(10)
for (i in 1:10000) {  # 10000 iteraciones
  premio <- sample(puertas)[1] # Asignar al premio una puerta al azar
  eleccion <- sample(puertas)[1] # Concursante elige una puerta al azar
  abrir <- sample(puertas[which(puertas != eleccion 
  & puertas != premio)])[1]
  # "Abren" una que no es la que elegiste ni la que tiene premio
  cambiarsi <- puertas[which(puertas != eleccion 
  & puertas != abrir)] # Situacion si cambiaras. 
  if(eleccion == premio) (xdata = c(xdata,"nocambiargana")) 
  # Caso en que eleccion original ganara y guardas resultado
  if(cambiarsi == premio)(xdata = c(xdata, "cambiargana"))
  # Caso en que cambiar ganara y guardas resultado
}

LLegado este punto tenemos un vector xdata que tiene para cada una de las 10000 iteraciones, qué estategia hubiera ganado. Si cambiar de puerta o no cambiar. Ahora simplemente contamos cuántas hay de cada una y analizamos el resultado.

length(which(xdata == "cambiargana")) # Cantidad que hubieran ganado si cambiabas
## [1] 6623
length(which(xdata == "nocambiargana")) 
## [1] 3377
# Cantidad que hubieran ganado si no cambiabas

table(xdata)
## xdata
##   cambiargana nocambiargana 
##          6623          3377

Para sorpresa o no de ustedes, la elección parece obvia. Cambiar de puerta nos hace elegir el premio un 66% de las veces y no cambiar tan solo un 33%.

Entender por qué es interesante.

Al elegir inicialmente una puerta entre las 3 opciones, la probabilidad de acertarle al premio es ⅓. De ahí que la estrategia de no cambiar de puerta es efectiva solo un 33% de las veces. Es simplemente quedarse con la elección inicial que tenia ⅓ de chances de ser correcta, independientemente de la puerta que abra el organizador.

Ahora, supongamos que en la elección inicial elegimos una puerta que no contiene el premio ( 66% de probabilidades). La estrategia de no cambiar de puerta nos hace perder indiscutidamente.

Si juntamos las dos proposiciones obtenemos que no cambiar de puerta nos hace ganar ⅓ de las veces, cuando elegimos bien por azar la puerta del premio inicialmente, y nos va a hacer perder el resto de las veces - ⅔ de las veces - que es cuando elegimos una puerta sin premio al principio.

Otro razonamiento equivalente es que cambiar de puerta nos hacer perder siempre y cuando hayamos elegido la puerta del premio originalmente (⅓) pero nos va a hacer ganar siempre que hayamos elegido una sin premio (⅔) porque la nueva puerta si o sí tendrá el premio, ya que la tercera es la que abre el organizador y no tiene premio.

En caso de que la rápida intuición nos hiciera creer que la elección entre cambiar y no cambiar era indistinto y ambas tenían 50% de chances de garantizarnos el premio, el ejercicio de simulación nos hubiera hecho elegir correctamente.

En este caso razonar el problema es posible ya que involucra pocas opciones pero en situaciones más complejas la simulación puede ayudar enormemente a la toma de decisiones.