Visualización de Datos con R

Introducción a R

Instalación e Interfaz

  • Lo primero que debe hacer para empezar a utilizar R es instalarlo en su ordenador. R funciona en casi todas las plataformas disponibles, incluidos los sistemas Windows, Mac OS X y Linux. Links de descarga para R y RStudio.

  • RStudio es un entorno de desarrollo integrado (IDE) disponible para R, el cual tiene un buen editor con resaltado de sintaxis, un visor de objetos de R y un gran número de características agradables que están integradas.

Gestión de Data Frames con el paquete paquete dplyr

Data Frames

  • Un data frame es una estructura de datos clave en estadística y en R. La estructura básica de un data frame es que hay una observación por fila y cada columna representa una variable, medida, rasgo o característica de esa observación. R tiene una implementación interna de los data frame que es probablemente la que se utiliza con más frecuencia.

El paquete dplyr

  • El paquete dplyr fue desarrollado por Hadley Wickham de RStudio y es una versión optimizada y destilada de su paquete plyr. Una importante contribución del paquete dplyr es que proporciona una “gramática” (en particular, verbos) para la manipulación de datos y para operar con data frames. Con esta gramática, se puede comunicar de forma comprensible lo que se está haciendo a un data frame. Esto es útil porque proporciona una abstracción para la manipulación de datos que antes no existía. Otra contribución útil es que las funciones de dplyr son muy rápidas, ya que muchas operaciones clave están codificadas en C++.

Gramática dplyr

  • Algunos de los “verbos” clave proporcionados por el paquete dplyr son

    • select: devuelve un subconjunto de las columnas de un dataframe, utilizando una notación flexible
    • filter: extraer un subconjunto de filas de un dataframe basándose en condiciones lógicas
    • arrange: reordenar las filas de un dataframe
    • rename: renombrar las variables de un dataframe
    • mutate: añadir nuevas variables/columnas o transformar variables existentes
    • summarise: generar estadísticas de resumen de diferentes variables en el dataframe, posiblemente dentro de los estratos
    • %>%: el operador “pipe” se utiliza para conectar varias acciones verbales en una pipeline

Propiedades comunes de la función dplyr

  • Todas las funciones que trataremos en este capítulo tendrán algunas características comunes. En particular,

    1. El primer argumento es un dataframe.
    2. Los argumentos siguientes describen qué hacer con el dataframe especificado en el primer argumento, y puede referirse a las columnas del dataframe directamente sin usar el operador $ (sólo use los nombres de las columnas).
    3. El resultado de retorno de una función es un nuevo dataframe
    4. Los dataframe deben estar correctamente formateados y anotados para que todo esto sea útil. En resumen, debe haber una observación por fila y cada columna debe representar un rasgo o característica de esa observación.

Instalación del paquete dplyr

  • El paquete dplyr se puede instalar desde CRAN o desde GitHub utilizando el paquete devtools y la función install_github(). El repositorio de GitHub suele contener las últimas actualizaciones del paquete y la versión de desarrollo.

  • Para instalarlo desde CRAN, basta con ejecutar

install.packages("dplyr")
  • Para instalar desde GitHub puedes ejecutar
install_github("hadley/dplyr")
  • Después de instalar el paquete es importante que lo cargues en su sesión de R con la función library()
library(dplyr)
  • Si la instalación de dplyr no fue realizada correctamente, un mensaje de instalación requerida aparecerá resaltado en amarillo en la parte superior de RStudio. Haga click en Install y espere un momento mientras se lleva a cabo la instalación. Puede usar también esta estrategia de instalación en lo que sigue

  • NOTA: Si alguna vez se encuentra con un problema en el que R confunde la función que quiere llamar, puede especificar el nombre completo de una función utilizando el operador ::. Por ejemplo, la función de filter del paquete dplyr tiene el nombre completo dplyr::filter. Llamar a las funciones con su nombre completo resolverá cualquier confusión sobre la función a la que se quiere llamar.

select()

Para los ejemplos de esta sección utilizaremos un dataset que contiene información sobre la contaminación del aire y la temperatura de la ciudad de Chicago (ver Air pollution dataset). Todos los datos usados en este curso los podrás descargar en: RDataSets

  • Después de descomprimir el archivo, puede cargar los datos en R utilizando la función readRDS(). Esta función proporciona los medios para guardar y restaurar un único objeto R en una conexión. Esta difiere de save y load, que guardan y restauran uno o más objetos con nombre en un entorno. Son ampliamente utilizadas por el propio R, por ejemplo para almacenar los metadatos de un paquete y para almacenar las bases de datos de help.search: la extensión de archivo ".rds" es la más utilizada.
chicago <- readRDS("C:/Data/chicago.rds")
  • Puede ver algunas características básicas del dataset con las funciones dim() y str().
dim(chicago)
## [1] 6940    8
str(chicago)
## 'data.frame':    6940 obs. of  8 variables:
##  $ city      : chr  "chic" "chic" "chic" "chic" ...
##  $ tmpd      : num  31.5 33 33 29 32 40 34.5 29 26.5 32.5 ...
##  $ dptp      : num  31.5 29.9 27.4 28.6 28.9 ...
##  $ date      : Date, format: "1987-01-01" "1987-01-02" ...
##  $ pm25tmean2: num  NA NA NA NA NA NA NA NA NA NA ...
##  $ pm10tmean2: num  34 NA 34.2 47 NA ...
##  $ o3tmean2  : num  4.25 3.3 3.33 4.38 4.75 ...
##  $ no2tmean2 : num  20 23.2 23.8 30.4 30.3 ...
head(chicago)
##   city tmpd   dptp       date pm25tmean2 pm10tmean2 o3tmean2 no2tmean2
## 1 chic 31.5 31.500 1987-01-01         NA   34.00000 4.250000  19.98810
## 2 chic 33.0 29.875 1987-01-02         NA         NA 3.304348  23.19099
## 3 chic 33.0 27.375 1987-01-03         NA   34.16667 3.333333  23.81548
## 4 chic 29.0 28.625 1987-01-04         NA   47.00000 4.375000  30.43452
## 5 chic 32.0 28.875 1987-01-05         NA         NA 4.750000  30.33333
## 6 chic 40.0 35.125 1987-01-06         NA   48.00000 5.833333  25.77233
  • La función select()puede utilizarse para seleccionar las columnas de un dataframe en las que desee centrarse. A menudo se tiene un dataframe grande que contiene “todos” los datos, pero un análisis determinado puede utilizar solo un subconjunto de variables u observaciones. La función select() le permite obtener las pocas columnas que pueda necesitar.

  • Supongamos que queremos tomar sólo las 3 primeras columnas. Hay varias formas de hacerlo. Podríamos, por ejemplo, utilizar índices numéricos. Pero también podemos utilizar los nombres directamente.

names(chicago)[1:3]
## [1] "city" "tmpd" "dptp"
subset <- select(chicago, names(chicago)[1:3])
head(subset)
##   city tmpd   dptp
## 1 chic 31.5 31.500
## 2 chic 33.0 29.875
## 3 chic 33.0 27.375
## 4 chic 29.0 28.625
## 5 chic 32.0 28.875
## 6 chic 40.0 35.125
subset <- select(chicago, city:dptp)
head(subset)
##   city tmpd   dptp
## 1 chic 31.5 31.500
## 2 chic 33.0 29.875
## 3 chic 33.0 27.375
## 4 chic 29.0 28.625
## 5 chic 32.0 28.875
## 6 chic 40.0 35.125
  • Tenga en cuenta que el signo : normalmente no se puede utilizar con nombres o cadenas, pero dentro de la función select() puede utilizarlo para especificar un rango de nombres de variables. También puede omitir variables con la función select() utilizando el signo negativo
subset <- select(chicago, -(city:dptp))
head(subset)
##         date pm25tmean2 pm10tmean2 o3tmean2 no2tmean2
## 1 1987-01-01         NA   34.00000 4.250000  19.98810
## 2 1987-01-02         NA         NA 3.304348  23.19099
## 3 1987-01-03         NA   34.16667 3.333333  23.81548
## 4 1987-01-04         NA   47.00000 4.375000  30.43452
## 5 1987-01-05         NA         NA 4.750000  30.33333
## 6 1987-01-06         NA   48.00000 5.833333  25.77233
  • El código equivalente en R base sería el siguiente, el cual es menos intuitivo
i <- match("city", names(chicago))
j <- match("dptp", names(chicago))
head(chicago[, -(i:j)])
##         date pm25tmean2 pm10tmean2 o3tmean2 no2tmean2
## 1 1987-01-01         NA   34.00000 4.250000  19.98810
## 2 1987-01-02         NA         NA 3.304348  23.19099
## 3 1987-01-03         NA   34.16667 3.333333  23.81548
## 4 1987-01-04         NA   47.00000 4.375000  30.43452
## 5 1987-01-05         NA         NA 4.750000  30.33333
## 6 1987-01-06         NA   48.00000 5.833333  25.77233
  • La función select() también permite una sintaxis especial que permite especificar nombres de variables basados en patrones. Así, por ejemplo, si quisiéramos conservar todas las variables que terminan con un “2”, podríamos hacer
subset <- select(chicago, ends_with("2"))
str(subset)
## 'data.frame':    6940 obs. of  4 variables:
##  $ pm25tmean2: num  NA NA NA NA NA NA NA NA NA NA ...
##  $ pm10tmean2: num  34 NA 34.2 47 NA ...
##  $ o3tmean2  : num  4.25 3.3 3.33 4.38 4.75 ...
##  $ no2tmean2 : num  20 23.2 23.8 30.4 30.3 ...
  • O si quisiéramos mantener todas las variables que empiezan por “d”, podríamos hacer
subset <- select(chicago, starts_with("d"))
str(subset)
## 'data.frame':    6940 obs. of  2 variables:
##  $ dptp: num  31.5 29.9 27.4 28.6 28.9 ...
##  $ date: Date, format: "1987-01-01" "1987-01-02" ...
  • También puede utilizar expresiones regulares más generales si es necesario. Consulte la página de ayuda escribiendo en consola (??select) para más detalles.

filter()

  • La función filter() se utiliza para extraer subconjuntos de filas de un dataframe. Esta función es similar a la función subset() existente en R, pero es bastante más rápida. Supongamos que queremos extraer las filas del dataframe PM2.5 (la materia particulada 2.5, incluye sustancias químicas orgánicas, polvo, hollín y metales.) que sean superiores a 30 (que es un nivel razonablemente alto), podríamos hacer
chic.f <- filter(chicago, pm25tmean2 > 30)
str(chic.f)
## 'data.frame':    194 obs. of  8 variables:
##  $ city      : chr  "chic" "chic" "chic" "chic" ...
##  $ tmpd      : num  23 28 55 59 57 57 75 61 73 78 ...
##  $ dptp      : num  21.9 25.8 51.3 53.7 52 56 65.8 59 60.3 67.1 ...
##  $ date      : Date, format: "1998-01-17" "1998-01-23" ...
##  $ pm25tmean2: num  38.1 34 39.4 35.4 33.3 ...
##  $ pm10tmean2: num  32.5 38.7 34 28.5 35 ...
##  $ o3tmean2  : num  3.18 1.75 10.79 14.3 20.66 ...
##  $ no2tmean2 : num  25.3 29.4 25.3 31.4 26.8 ...
  • Puede ver que ahora sólo hay 194 filas en el dataframe y la distribución de los valores de pm25tmean2 es
summary(chic.f$pm25tmean2)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   30.05   32.12   35.04   36.63   39.53   61.50
  • Podemos colocar una secuencia lógica arbitrariamente compleja dentro de filter(), por lo que podríamos, por ejemplo, extraer las filas en las que PM2.5 es mayor que 30 y la temperatura, tmpd es mayor que 80 grados Fahrenheit
chic.f <- filter(chicago, pm25tmean2 > 30 & tmpd > 80)
select(chic.f, date, tmpd, pm25tmean2)
##          date tmpd pm25tmean2
## 1  1998-08-23   81   39.60000
## 2  1998-09-06   81   31.50000
## 3  2001-07-20   82   32.30000
## 4  2001-08-01   84   43.70000
## 5  2001-08-08   85   38.83750
## 6  2001-08-09   84   38.20000
## 7  2002-06-20   82   33.00000
## 8  2002-06-23   82   42.50000
## 9  2002-07-08   81   33.10000
## 10 2002-07-18   82   38.85000
## 11 2003-06-25   82   33.90000
## 12 2003-07-04   84   32.90000
## 13 2005-06-24   86   31.85714
## 14 2005-06-27   82   51.53750
## 15 2005-06-28   85   31.20000
## 16 2005-07-17   84   32.70000
## 17 2005-08-03   84   37.90000

arrange()

  • La función arrange() se utiliza para reordenar las filas de un dataframe según una de las variables/columnas. Reordenar las filas de un dataframe (conservando el orden correspondiente de otras columnas) suele ser una tarea ardua en R. La función arrange() simplifica la tarea. Aquí podemos ordenar las filas del dataframe chicago por fecha, de modo que la primera fila sea la observación más antigua y la última la más reciente
chicago <- arrange(chicago, date)
  • Ahora podemos visualizar las primeras y últimas filas de las siguiente manera
head(select(chicago, date, pm25tmean2), 3)
##         date pm25tmean2
## 1 1987-01-01         NA
## 2 1987-01-02         NA
## 3 1987-01-03         NA
tail(select(chicago, date, pm25tmean2), 3)
##            date pm25tmean2
## 6938 2005-12-29    7.45000
## 6939 2005-12-30   15.05714
## 6940 2005-12-31   15.00000
  • Las columnas también se pueden ordenar de forma descendente utilizando el operador especial desc()
chicago <- arrange(chicago, desc(date))
head(chicago)
##   city tmpd dptp       date pm25tmean2 pm10tmean2  o3tmean2 no2tmean2
## 1 chic   35 30.1 2005-12-31   15.00000       23.5  2.531250  13.25000
## 2 chic   36 31.0 2005-12-30   15.05714       19.2  3.034420  22.80556
## 3 chic   35 29.4 2005-12-29    7.45000       23.5  6.794837  19.97222
## 4 chic   37 34.5 2005-12-28   17.75000       27.5  3.260417  19.28563
## 5 chic   40 33.6 2005-12-27   23.56000       27.0  4.468750  23.50000
## 6 chic   35 29.6 2005-12-26    8.40000        8.5 14.041667  16.81944
  • Si se observan las tres primeras y las tres últimas filas, las fechas aparecen en orden descendente
head(select(chicago, date, pm25tmean2), 3)
##         date pm25tmean2
## 1 2005-12-31   15.00000
## 2 2005-12-30   15.05714
## 3 2005-12-29    7.45000
tail(select(chicago, date, pm25tmean2), 3)
##            date pm25tmean2
## 6938 1987-01-03         NA
## 6939 1987-01-02         NA
## 6940 1987-01-01         NA

rename()

  • Renombrar una variable en un dataframe en R es sorprendentemente difícil de hacer. La función rename() está diseñada para facilitar este proceso. Aquí puedes ver los nombres de las cinco primeras variables del dataframe chicago.
head(chicago[, 1:5], 3)
##   city tmpd dptp       date pm25tmean2
## 1 chic   35 30.1 2005-12-31   15.00000
## 2 chic   36 31.0 2005-12-30   15.05714
## 3 chic   35 29.4 2005-12-29    7.45000
  • La columna dptp se supone que representa la temperatura del punto de rocío y la columna pm25tmean2 proporciona los datos PM2.5. Sin embargo, estos nombres son incómodos y probablemente deban ser renombrados a algo más sencillos. Dew point: temperatura más alta a la que empieza a condensarse el vapor de agua contenido en el aire, produciendo rocío, neblina, cualquier tipo de nube o, en caso de que la temperatura sea lo suficientemente baja, escarcha.
chicago <- rename(chicago, dewpoint = dptp, pm25 = pm25tmean2)
head(chicago[, 1:5], 3)
##   city tmpd dewpoint       date     pm25
## 1 chic   35     30.1 2005-12-31 15.00000
## 2 chic   36     31.0 2005-12-30 15.05714
## 3 chic   35     29.4 2005-12-29  7.45000
  • La sintaxis dentro de la función rename() es tener el nuevo nombre en el lado izquierdo del signo = y el nombre antiguo en el lado derecho.

mutate()

  • La función mutate() existe para calcular las transformaciones de las variables de un dataframe. A menudo, se desea crear nuevas variables derivadas de variables existentes y mutate() proporciona una interfaz limpia para hacerlo. Por ejemplo, con los datos de contaminación atmosférica, a menudo queremos eliminar la tendencia de los datos restando la media de los mismos

  • De este modo, podemos ver si el nivel de contaminación atmosférica de un día determinado es superior o inferior a la media (en lugar de observar su nivel absoluto). Aquí creamos una variable pm25detrend que resta la media de la variable pm25.

chicago <- mutate(chicago, pm25detrend = pm25 - mean(pm25, na.rm = TRUE))
head(chicago)
##   city tmpd dewpoint       date     pm25 pm10tmean2  o3tmean2 no2tmean2
## 1 chic   35     30.1 2005-12-31 15.00000       23.5  2.531250  13.25000
## 2 chic   36     31.0 2005-12-30 15.05714       19.2  3.034420  22.80556
## 3 chic   35     29.4 2005-12-29  7.45000       23.5  6.794837  19.97222
## 4 chic   37     34.5 2005-12-28 17.75000       27.5  3.260417  19.28563
## 5 chic   40     33.6 2005-12-27 23.56000       27.0  4.468750  23.50000
## 6 chic   35     29.6 2005-12-26  8.40000        8.5 14.041667  16.81944
##   pm25detrend
## 1   -1.230958
## 2   -1.173815
## 3   -8.780958
## 4    1.519042
## 5    7.329042
## 6   -7.830958
  • También existe la función relacionada llamada transmute(), que hace lo mismo que mutate() pero luego elimina todas las variables no transformadas. En este caso, se remueve la tendencia de las variables PM10 y ozono (O3)
head(transmute(chicago, 
               pm10detrend = pm10tmean2 - mean(pm10tmean2, na.rm = TRUE),
               o3detrend = o3tmean2 - mean(o3tmean2, na.rm = TRUE)))
##   pm10detrend  o3detrend
## 1  -10.395206 -16.904263
## 2  -14.695206 -16.401093
## 3  -10.395206 -12.640676
## 4   -6.395206 -16.175096
## 5   -6.895206 -14.966763
## 6  -25.395206  -5.393846
  • Observe que sólo hay dos columnas en el dataframe transmutado.

group_by()

  • La función group_by() se utiliza para generar estadísticas de resumen del dataframe dentro de los estatus definidos por una variable. Por ejemplo, en este dataset de contaminación atmosférica, podría querer saber cuál es el nivel medio anual de PM2,5. Entonces el estatus es el año, y eso es algo que podemos derivar de la variable fecha. Junto con la función group_by() se suele utilizar la función summarize() (o summarise()` para algunas partes del mundo).

  • La operación general aquí es una combinación de dividir un dataframe en piezas separadas definidas por una variable o grupo de variables (group_by()), y luego aplicar una función de resumen en esos subconjuntos (summarize()). En primer lugar, podemos crear una variable de año utilizando as.POSIXlt(). Las opciones para días y meses respectivamente, están disponibles usando $mday y mon+1 (ver as.POSIXlt). Los valores anuales se almacenan utilizando un valor de índice base de 1900. Así, 2015 se almacena como 115 ($year = 115).

chicago <- mutate(chicago, year = as.POSIXlt(date)$year + 1900)
head(chicago)
##   city tmpd dewpoint       date     pm25 pm10tmean2  o3tmean2 no2tmean2
## 1 chic   35     30.1 2005-12-31 15.00000       23.5  2.531250  13.25000
## 2 chic   36     31.0 2005-12-30 15.05714       19.2  3.034420  22.80556
## 3 chic   35     29.4 2005-12-29  7.45000       23.5  6.794837  19.97222
## 4 chic   37     34.5 2005-12-28 17.75000       27.5  3.260417  19.28563
## 5 chic   40     33.6 2005-12-27 23.56000       27.0  4.468750  23.50000
## 6 chic   35     29.6 2005-12-26  8.40000        8.5 14.041667  16.81944
##   pm25detrend year
## 1   -1.230958 2005
## 2   -1.173815 2005
## 3   -8.780958 2005
## 4    1.519042 2005
## 5    7.329042 2005
## 6   -7.830958 2005
  • Ahora podemos crear un dataframe separado que divida el dataset original por año
years <- group_by(chicago, year)
years
## # A tibble: 6,940 × 10
## # Groups:   year [19]
##    city   tmpd dewpoint date        pm25 pm10tmean2 o3tmean2 no2tmean2
##    <chr> <dbl>    <dbl> <date>     <dbl>      <dbl>    <dbl>     <dbl>
##  1 chic     35     30.1 2005-12-31 15          23.5     2.53      13.2
##  2 chic     36     31   2005-12-30 15.1        19.2     3.03      22.8
##  3 chic     35     29.4 2005-12-29  7.45       23.5     6.79      20.0
##  4 chic     37     34.5 2005-12-28 17.8        27.5     3.26      19.3
##  5 chic     40     33.6 2005-12-27 23.6        27       4.47      23.5
##  6 chic     35     29.6 2005-12-26  8.4         8.5    14.0       16.8
##  7 chic     35     32.1 2005-12-25  6.7         8      14.4       13.8
##  8 chic     37     35.2 2005-12-24 30.8        25.2     1.77      32.0
##  9 chic     41     32.6 2005-12-23 32.9        34.5     6.91      29.1
## 10 chic     22     23.3 2005-12-22 36.6        42.5     5.39      33.7
## # ℹ 6,930 more rows
## # ℹ 2 more variables: pm25detrend <dbl>, year <dbl>
  • Por último, calculamos los estadísticos de resumen para cada año del dataframe con la función summarize(). La función summarize() devuelve un dataframe con year como primera columna, y luego los promedios anuales de pm25, o3 y no2.
summarize(years, pm25 = mean(pm25, na.rm = TRUE),
          o3 = max(o3tmean2, na.rm = TRUE),
          no2 = median(no2tmean2, na.rm = TRUE))
## # A tibble: 19 × 4
##     year  pm25    o3   no2
##    <dbl> <dbl> <dbl> <dbl>
##  1  1987 NaN    63.0  23.5
##  2  1988 NaN    61.7  24.5
##  3  1989 NaN    59.7  26.1
##  4  1990 NaN    52.2  22.6
##  5  1991 NaN    63.1  21.4
##  6  1992 NaN    50.8  24.8
##  7  1993 NaN    44.3  25.8
##  8  1994 NaN    52.2  28.5
##  9  1995 NaN    66.6  27.3
## 10  1996 NaN    58.4  26.4
## 11  1997 NaN    56.5  25.5
## 12  1998  18.3  50.7  24.6
## 13  1999  18.5  57.5  24.7
## 14  2000  16.9  55.8  23.5
## 15  2001  16.9  51.8  25.1
## 16  2002  15.3  54.9  22.7
## 17  2003  15.2  56.2  24.6
## 18  2004  14.6  44.5  23.4
## 19  2005  16.2  58.8  22.6
  • En un ejemplo algo más trabajado, podríamos querer saber cuáles son los niveles medios de de ozono (o3) y dióxido de nitrógeno (no2) dentro de los quintiles de pm25. Una forma más sencilla de de hacerlo sería a través de un modelo de regresión, pero en realidad podemos hacerlo rápidamente con group_by() y summarize().

  • Primero, podemos crear una variable categórica de pm25 dividida en quintiles La función cut divide el rango en intervalos y codifica los valores según el intervalo al que pertenecen.

qq <- quantile(chicago$pm25, seq(0, 1, 0.20), na.rm = TRUE)
qq
##     0%    20%    40%    60%    80%   100% 
##  1.700  8.700 12.375 16.700 22.610 61.500
chicago <- mutate(chicago, pm25.quint = cut(pm25, qq))
head(chicago)
##   city tmpd dewpoint       date     pm25 pm10tmean2  o3tmean2 no2tmean2
## 1 chic   35     30.1 2005-12-31 15.00000       23.5  2.531250  13.25000
## 2 chic   36     31.0 2005-12-30 15.05714       19.2  3.034420  22.80556
## 3 chic   35     29.4 2005-12-29  7.45000       23.5  6.794837  19.97222
## 4 chic   37     34.5 2005-12-28 17.75000       27.5  3.260417  19.28563
## 5 chic   40     33.6 2005-12-27 23.56000       27.0  4.468750  23.50000
## 6 chic   35     29.6 2005-12-26  8.40000        8.5 14.041667  16.81944
##   pm25detrend year  pm25.quint
## 1   -1.230958 2005 (12.4,16.7]
## 2   -1.173815 2005 (12.4,16.7]
## 3   -8.780958 2005   (1.7,8.7]
## 4    1.519042 2005 (16.7,22.6]
## 5    7.329042 2005 (22.6,61.5]
## 6   -7.830958 2005   (1.7,8.7]
  • Ahora podemos agrupar el dataframe por la variable pm25.quint.
quint <- group_by(chicago, pm25.quint)
quint
## # A tibble: 6,940 × 11
## # Groups:   pm25.quint [6]
##    city   tmpd dewpoint date        pm25 pm10tmean2 o3tmean2 no2tmean2
##    <chr> <dbl>    <dbl> <date>     <dbl>      <dbl>    <dbl>     <dbl>
##  1 chic     35     30.1 2005-12-31 15          23.5     2.53      13.2
##  2 chic     36     31   2005-12-30 15.1        19.2     3.03      22.8
##  3 chic     35     29.4 2005-12-29  7.45       23.5     6.79      20.0
##  4 chic     37     34.5 2005-12-28 17.8        27.5     3.26      19.3
##  5 chic     40     33.6 2005-12-27 23.6        27       4.47      23.5
##  6 chic     35     29.6 2005-12-26  8.4         8.5    14.0       16.8
##  7 chic     35     32.1 2005-12-25  6.7         8      14.4       13.8
##  8 chic     37     35.2 2005-12-24 30.8        25.2     1.77      32.0
##  9 chic     41     32.6 2005-12-23 32.9        34.5     6.91      29.1
## 10 chic     22     23.3 2005-12-22 36.6        42.5     5.39      33.7
## # ℹ 6,930 more rows
## # ℹ 3 more variables: pm25detrend <dbl>, year <dbl>, pm25.quint <fct>
  • Por último, podemos calcular la media de o3 y no2 dentro de los quintiles de pm25
na.omit(summarize(quint, o3 = mean(o3tmean2, na.rm = TRUE), no2 = mean(no2tmean2, na.rm = TRUE)))
## # A tibble: 5 × 3
##   pm25.quint     o3   no2
##   <fct>       <dbl> <dbl>
## 1 (1.7,8.7]    21.7  18.0
## 2 (8.7,12.4]   20.4  22.1
## 3 (12.4,16.7]  20.7  24.4
## 4 (16.7,22.6]  19.9  27.3
## 5 (22.6,61.5]  20.3  29.6
  • Con base en la tabla, no hay una relación fuerte entre pm25 y o3, pero parece haber una correlación positiva entre pm25 y no2. Un modelo estadístico más sofisticado puede ayudar a dar respuestas precisas a estas preguntas, pero una simple aplicación de las funciones dplyr puede llevarle a la mayoría de los caminos.

  • Nótese además que si omitimos el uso de la función na.omit, podremos observar una fila cuya clase corresponde a <NA>. Esto está asociado a observaciones para las cuales existen valores de o3 y no2, pero, pm25 es faltante. Estudiaremos mas adelante como realizar imputación de este tipo de datos.

summarize(quint, o3 = mean(o3tmean2, na.rm = TRUE), no2 = mean(no2tmean2, na.rm = TRUE))
## # A tibble: 6 × 3
##   pm25.quint     o3   no2
##   <fct>       <dbl> <dbl>
## 1 (1.7,8.7]    21.7  18.0
## 2 (8.7,12.4]   20.4  22.1
## 3 (12.4,16.7]  20.7  24.4
## 4 (16.7,22.6]  19.9  27.3
## 5 (22.6,61.5]  20.3  29.6
## 6 <NA>         18.8  25.8

%>%

  • El operador pipeline %>% es muy útil para encadenar múltiples funciones dplyr en una secuencia de operaciones. Nótese que hasta ahora, cada vez que se desea aplicar más de una función, la secuencia quedaba dentro de una secuencia de llamadas a funciones anidadas que es difícil de leer, por ejemplo
third(second(first(x)))
  • Este anidamiento no es una forma natural de pensar en una secuencia de operaciones. El operador %>% permite encadenar las operaciones de izquierda a derecha, es decir
first(x) %>% 
  second %>% 
  third
  • Tomemos el ejemplo que acabamos de hacer en la última sección, en el que hemos calculado la media de o3 y no2 dentro de los quintiles de pm25. Allí tuvimos que

    1. Crear una nueva variable pm25.quint
    2. Agrupar el dataframe por esa nueva variable
    3. Calcular la media de o3 y no2 en los subgrupos definidos por pm25.quint
  • Eso se puede hacer con la siguiente secuencia en una sola expresión de R

library(tidyr)

mutate(chicago, pm25.quint = cut(pm25, qq)) %>%
  group_by(pm25.quint) %>%
  summarize(o3 = mean(o3tmean2, na.rm = TRUE), no2 = mean(no2tmean2, na.rm = TRUE)) %>%
  drop_na()
## # A tibble: 5 × 3
##   pm25.quint     o3   no2
##   <fct>       <dbl> <dbl>
## 1 (1.7,8.7]    21.7  18.0
## 2 (8.7,12.4]   20.4  22.1
## 3 (12.4,16.7]  20.7  24.4
## 4 (16.7,22.6]  19.9  27.3
## 5 (22.6,61.5]  20.3  29.6
  • De esta manera no tenemos que crear un conjunto de variables temporales en el camino o crear una secuencia masiva anidada de llamadas a funciones. Observe en el código anterior que el dataframe chicago pasa a la primera llamada a mutate(), pero después no tiene que pasar el primer argumento a group_by() o a summarize(). Una vez que se recorre el pipeline con %>%, el primer argumento se toma como la salida del elemento anterior en la pipeline.

  • Otro ejemplo podría ser el cálculo del nivel medio de contaminantes pm25, o3, no2 por mes. Esto podría ser útil para ver si hay alguna tendencia estacional en los datos.

mutate(chicago, month = as.POSIXlt(date)$mon + 1) %>%
  group_by(month) %>%
  summarize(pm25 = mean(pm25, na.rm = TRUE),
            o3 = max(o3tmean2, na.rm = TRUE),
            no2 = median(no2tmean2, na.rm = TRUE))
## # A tibble: 12 × 4
##    month  pm25    o3   no2
##    <dbl> <dbl> <dbl> <dbl>
##  1     1  17.8  28.2  25.4
##  2     2  20.4  37.4  26.8
##  3     3  17.4  39.0  26.8
##  4     4  13.9  47.9  25.0
##  5     5  14.1  52.8  24.2
##  6     6  15.9  66.6  25.0
##  7     7  16.6  59.5  22.4
##  8     8  16.9  54.0  23.0
##  9     9  15.9  57.5  24.5
## 10    10  14.2  47.1  24.2
## 11    11  15.2  29.5  23.6
## 12    12  17.5  27.7  24.5
  • Aquí podemos ver que el o3 tiende a ser bajo en los meses de invierno (11, 12, 1, 2, 3) y alto en el verano (4,5,…,10) mientras que no2 es más alto en invierno y más bajo en verano.

Graficar un dataset

  • El paquete ggplot2 puede utilizarse para crear varios tipos de gráficos y diagramas a partir de un dataframe. La función ggplot() se utiliza para definir gráficos, y se le pueden pasar una serie de parámetros y unirse a otras funciones para producir finalmente un gráfico de salida. El primer parámetro que se pasa a ggplot() es el dataframe que se quiere representar. Normalmente será un objeto data.frame, pero también puede ser un subconjunto de un dataframe definido con la función subset().
library(readr)
dfCrime = read_csv("C:/Data/Crime_Data.csv", col_names = TRUE)
  • Puede obtener un recuento del número de registros con la función nrow(). La función View() se puede utilizar para ver los datos en un formato tabular.
nrow(dfCrime)
## [1] 481376
head(dfCrime)
## # A tibble: 6 × 11
##   `Report Number` `Occurred Date` `Occurred Time` `Reported Date`
##             <dbl> <chr>                     <dbl> <chr>          
## 1         2.01e13 12/13/1908                 2114 12/13/2008     
## 2         2.01e13 06/15/1964                    0 06/15/2010     
## 3         2.01e12 01/01/1973                    0 01/25/2012     
## 4         2.01e13 06/01/1974                    0 09/09/2013     
## 5         2.02e13 01/01/1975                    0 08/11/2016     
## 6         1.98e12 12/16/1975                  900 12/16/1975     
## # ℹ 7 more variables: `Reported Time` <dbl>, `Crime Subcategory` <chr>,
## #   `Primary Offense Description` <chr>, Precinct <chr>, Sector <chr>,
## #   Beat <chr>, Neighborhood <chr>
  • Un tibble, o tbl_df, es una reimaginación moderna del data.frame, manteniendo lo que el tiempo ha demostrado que es eficaz, y desechando lo que no lo es. En este caso limitaremos las columnas a lo siguiente: Reported Date, Crime Subcategory, Primary Offense Description, Precinct, Sector, Beat y Neighborhood

  • tibble tiene un método de imprimir refinado que muestra sólo las 10 primeras filas y todas las columnas que caben en la pantalla. Esto hace que sea mucho más fácil trabajar con datos de gran tamaño. A diferencia de los data.frame, los tibble no admiten operaciones aritméticas en todas las columnas, mientras que con un data.frame.

dfCrime = select(dfCrime,
                 "Reported Date",
                 "Crime Subcategory",
                 "Primary Offense Description",
                 "Precinct",
                 "Sector",
                 "Beat",
                 "Neighborhood")
  • También es posible que quiera cambiar el nombre de las columnas para que sean más fáciles de leer o simplificar los nombres. La función select() se puede utilizar para hacer esto también.
dfCrime = select(dfCrime, 
                 "CrimeDate" = "Reported Date", 
                 "Category" = "Crime Subcategory", 
                 "Description" = "Primary Offense Description", 
                 "Precinct",
                 "Sector", 
                 "Beat", 
                 "Neighborhood")
head(dfCrime)
## # A tibble: 6 × 7
##   CrimeDate  Category             Description Precinct Sector Beat  Neighborhood
##   <chr>      <chr>                <chr>       <chr>    <chr>  <chr> <chr>       
## 1 12/13/2008 DUI                  DUI-LIQUOR  EAST     G      G2    CENTRAL ARE…
## 2 06/15/2010 FAMILY OFFENSE-NONV… CHILD-OTHER WEST     Q      Q2    QUEEN ANNE  
## 3 01/25/2012 SEX OFFENSE-OTHER    SEXOFF-OTH… NORTH    N      N2    NORTHGATE   
## 4 09/09/2013 SEX OFFENSE-OTHER    SEXOFF-OTH… UNKNOWN  <NA>   <NA>  UNKNOWN     
## 5 08/11/2016 SEX OFFENSE-OTHER    SEXOFF-OTH… UNKNOWN  <NA>   <NA>  UNKNOWN     
## 6 12/16/1975 BURGLARY-RESIDENTIAL BURGLARY-F… SOUTH    R      R3    LAKEWOOD/SE…
  • El filtrado del dataset le permite centrarse en un subconjunto de filas en lugar de en todo el dataset. El paquete dplyr incluye una función filter() que soporta esta capacidad
dfCrime2 = filter(dfCrime, Neighborhood == "QUEEN ANNE")
nrow(dfCrime2)
## [1] 25172
  • También puede incluir varias expresiones en una función filter(), como sigue, vecindario QUEEN ANNE y categoría BURGLARY-RESIDENTIAL. Examinaremos expresiones de filtro más complejas en una sección posterior
dfCrime3 = filter(dfCrime, 
                  Neighborhood == "QUEEN ANNE", 
                  Category == "BURGLARY-RESIDENTIAL")
head(dfCrime3)
## # A tibble: 6 × 7
##   CrimeDate  Category             Description Precinct Sector Beat  Neighborhood
##   <chr>      <chr>                <chr>       <chr>    <chr>  <chr> <chr>       
## 1 04/22/2008 BURGLARY-RESIDENTIAL BURGLARY-N… WEST     Q      Q3    QUEEN ANNE  
## 2 01/02/2008 BURGLARY-RESIDENTIAL BURGLARY-F… WEST     Q      Q3    QUEEN ANNE  
## 3 01/02/2008 BURGLARY-RESIDENTIAL BURGLARY-F… WEST     D      D2    QUEEN ANNE  
## 4 01/07/2008 BURGLARY-RESIDENTIAL BURGLARY-F… WEST     Q      Q3    QUEEN ANNE  
## 5 01/09/2008 BURGLARY-RESIDENTIAL BURGLARY-N… WEST     Q      Q2    QUEEN ANNE  
## 6 01/10/2008 BURGLARY-RESIDENTIAL BURGLARY-F… WEST     Q      Q3    QUEEN ANNE
  • En la ventana de la consola de RStudio añada el código que ve a continuación para agrupar los delitos por ronda policial
dfCrime2 = group_by(dfCrime2, Beat)
  • La función n() se utiliza para obtener un recuento del número de registros de cada grupo. Añada a el código que se ve a continuación. La columna Beat corresponde a el límite del sector policial designado donde se produjeron las infracciones (ver Seattle Police Department)
dfCrime2 = summarise(dfCrime2, n = n())
head(dfCrime2)
## # A tibble: 4 × 2
##   Beat      n
##   <chr> <int>
## 1 D2     4373
## 2 Q1       88
## 3 Q2    10851
## 4 Q3     9860

Gráficos con ggplot()

  • En la consola de RStudio añada el código que ve a continuación. La función ggplot() en este caso recibe el dataframe dfCrime. La función geom_col() se utiliza para definir la geometría del gráfico (diagrama de barras) y se le pasa un parámetro de asignación que se define llamando a la función aes() y pasando las columnas para el eje x (Beat), y el eje y (n = count)
library("ggplot2")

ggplot(data=dfCrime2) + 
  geom_col(mapping = aes(x = Beat, y = n), fill = "red")

Gráficos de robos por mes y año

  • En este ejercicio crearemos un par de gráficos de barras que muestren el número de robos por año y por mes para el barrio de Queen Anne. Además de los paquetes dplyr y ggplot2 que que usamos anteriormente, también usaremos el paquete lubridate para manipular la información de las fechas.

  • En el panel de paquetes de RStudio, cargue el paquete lubridate. El paquete lubridate forma parte de tidyverse y se utiliza para trabajar con fechas y horas. Además, asegúrese de que los paquetes readr, dplyr y ggplot2 están cargados

library(readr)
dfCrime = read_csv("C:/Data/Crime_Data.csv", col_names = TRUE)
  • Especifique las columnas y los nombres de las columnas
dfCrime = select(dfCrime, 
                 "CrimeDate" = "Reported Date",
                 "Category" = "Crime Subcategory",
                 "Description" = "Primary Offense Description", 
                 "Precinct",
                 "Sector", 
                 "Beat", 
                 "Neighborhood")
  • Filtrar los registros para que sólo se conserven los robos residenciales en el barrio de Queen Anne
dfCrime2 = filter(dfCrime, 
                  Neighborhood == "QUEEN ANNE", 
                  Category == "BURGLARY-RESIDENTIAL")
  • El paquete dplyr incluye la capacidad de crear dinámicamente nuevas columnas en un dataframe mediante la manipulación de los datos de las columnas existentes en el dataframe. La función mutate() se utiliza para crear las nuevas columnas. Aquí la función mutate() se utilizará para extraer el año de la columna CrimeDate.

  • Agregue el siguiente código para ver esto en acción. El segundo parámetro crea una nueva columna llamada YEAR y la rellena utilizando la función year() del paquete lubridate. Dentro de la función year() la columna CrimeDate, que es una columna de caracteres, se convierte en una fecha con base en el formato format="%m/%d/%Y".

library(lubridate)
dfCrime3 = mutate(dfCrime2, YEAR = year(as.Date(dfCrime2$CrimeDate,format="%m/%d/%Y")))
  • Observe la columna YEAR al final del dataframe. La función mutate() siempre añade nuevas columnas al final del dataframe
head(dfCrime3)
## # A tibble: 6 × 8
##   CrimeDate  Category       Description Precinct Sector Beat  Neighborhood  YEAR
##   <chr>      <chr>          <chr>       <chr>    <chr>  <chr> <chr>        <dbl>
## 1 04/22/2008 BURGLARY-RESI… BURGLARY-N… WEST     Q      Q3    QUEEN ANNE    2008
## 2 01/02/2008 BURGLARY-RESI… BURGLARY-F… WEST     Q      Q3    QUEEN ANNE    2008
## 3 01/02/2008 BURGLARY-RESI… BURGLARY-F… WEST     D      D2    QUEEN ANNE    2008
## 4 01/07/2008 BURGLARY-RESI… BURGLARY-F… WEST     Q      Q3    QUEEN ANNE    2008
## 5 01/09/2008 BURGLARY-RESI… BURGLARY-N… WEST     Q      Q2    QUEEN ANNE    2008
## 6 01/10/2008 BURGLARY-RESI… BURGLARY-F… WEST     Q      Q3    QUEEN ANNE    2008
  • Ahora agruparemos los datos por año y resumiremos obteniendo un recuento del número de delitos por año
dfCrime4 = group_by(dfCrime3, YEAR)
dfCrime4 = summarise(dfCrime4, n = n())
head(dfCrime4)
## # A tibble: 6 × 2
##    YEAR     n
##   <dbl> <int>
## 1  2008   205
## 2  2009   194
## 3  2010   181
## 4  2011   203
## 5  2012   170
## 6  2013   209
  • Cree un gráfico de barras llamando a las funciones ggplot() y geom_col() como se ve abajo. Defina YEAR como columna para el eje x y el número de delitos para el eje el eje y. Esto debería producir el gráfico que se ve a continuación
ggplot(data=dfCrime4) + geom_col(mapping = aes(x=YEAR, y=n), fill="red")

  • Ahora crearemos otro gráfico de barras que muestre el número de delitos por por mes en lugar de por año. En primer lugar, cree una columna MONTH utilizando la función mutate()
dfCrime3 = mutate(dfCrime2, MONTH = month(as.Date(dfCrime2$CrimeDate, format="%m/%d/%Y")))
  • Agrupamos y resumimos los datos por mes
dfCrime4 = group_by(dfCrime3, MONTH) 
dfCrime4 = summarise(dfCrime4, n = n())
dfCrime4
## # A tibble: 12 × 2
##    MONTH     n
##    <dbl> <int>
##  1     1   211
##  2     2   162
##  3     3   173
##  4     4   188
##  5     5   206
##  6     6   203
##  7     7   196
##  8     8   144
##  9     9   187
## 10    10   154
## 11    11   168
## 12    12   201
  • Para obtener una mejor visualización, podemos por ejemplo utilizar la función month.abb[] para cambiar los id de los meses a las iniciales correspondientes
dfCrime4 = mutate(dfCrime4, MONTH_S = month.abb[MONTH])
dfCrime4
## # A tibble: 12 × 3
##    MONTH     n MONTH_S
##    <dbl> <int> <chr>  
##  1     1   211 Jan    
##  2     2   162 Feb    
##  3     3   173 Mar    
##  4     4   188 Apr    
##  5     5   206 May    
##  6     6   203 Jun    
##  7     7   196 Jul    
##  8     8   144 Aug    
##  9     9   187 Sep    
## 10    10   154 Oct    
## 11    11   168 Nov    
## 12    12   201 Dec
  • A continuación, se crea el gráfico de barras
library(scales)

dfCrime4$MONTH_S <- factor(dfCrime4$MONTH_S, levels=unique(dfCrime4$MONTH_S))

ggplot(data=dfCrime4) + geom_col(mapping = aes(x=MONTH_S, y=n), fill="blue") +
  xlab("Month") + ylab("Frequency") +
  ggtitle("Number of crimes per month")

Conclusiones

  • En este capítulo se han abordado algunas técnicas básicas para la exploración y visualización utilizando el paquete tidyverse y su ecosistema de subpaquetes. Después de instalar y cargar el paquete mediante RStudio, se realizaron una serie de tareas usando el lenguaje de programación R con una serie de subpaquetes tidyverse. Se cargó un dataset desde un archivo CSV utilizando readr.
  • Después, se manipuló los datos de varias maneras utilizando el paquete dplyr. Se utilizó la función select() para incluir y renombrar columnas. El contenido del dataframe se filtró con la función filter(). A continuación, los datos se agruparon y finalmente se produjeron varios gráficos utilizando ggplot2. En la siguiente sección se estudiará más sobre cómo usar el paquete readr para cargar datos de fuentes de datos externas.

Lectura de datos en R

  • Los objetos de datos grandes, normalmente almacenados como dataframe en R, suelen leerse de archivos externos. En esta sección, examinaremos una serie de funciones que se pueden utilizar para leer datos. Hay un gran número de formatos de datos comunes que pueden ser leídos dentro y fuera de R. Esto incluye archivos de texto en formatos como csv, txt, html y json. También incluye archivos de salida de aplicaciones estadísticas como SAS y SPSS. Los recursos en línea Los recursos en línea, incluidos los servicios web y las páginas HTML, también pueden leerse en R. Por último, también se pueden leer tablas de bases de datos relacionales y no relacionales. Hay una serie de funciones proporcionadas por R y Tidyverse que le permitirán leer estas diversas fuentes.

  • En este capítulo cubriremos los siguientes temas:

    • Carga de un archivo csv con read.table()
    • Carga de un archivo csv con read.csv()
    • Carga de un archivo delimitado por tabulaciones con read.table()
    • Uso de readr para cargar datos

Lectura de archivos csv con read.table()

  • La primera función que examinaremos es read.table(). La función read.table() es una función función integrada en R que puede utilizarse para leer varios formatos de archivo en un dataframe, esta es probablemente la función interna más común utilizada para leer archivos simples en R. Sin embargo, como veremos más adelante en esta sección, tidyverse incluye funciones similares que son más eficientes en la lectura de datos externos en R. La sintaxis de read.table() es aceptar un nombre de archivo, que será la ruta junto con un indicador TRUE|FALSE para la cabecera. Si se establece como TRUE, se asume que los nombres de las columnas están en la línea de cabecera del archivo. La ruta no es no es necesaria si ya se ha establecido el directorio de trabajo. La salida de la función read.table() es un objeto dataframe. La función file.choose() es una función práctica que puede utilizar para seleccionar interactivamente el archivo que desea importar en lugar de tener que codificar la ruta del dataset. En este ejercicio aprenderá a utilizar la función read.table() para cargar un archivo con formato csv

  • Abra RStudio y busque el panel de la consola. Si es necesario, establezca el directorio de trabajo escribiendo el código que ve a continuación en el panel de la consola o desde las opcciones Sesión | Establecer directorio de trabajo | Elegir Directorio en el menú de RStudio.

setwd(<installation directory for exercise data>)


  • La carpeta RDataSets contiene un archivo llamado StudyArea.csv, que es un archivo separado por comas que contiene datos de incendios forestales de los años 1980-2016 para los estados de California, Oregón, Washington, Idaho, Montana, Wyoming, Colorado, Utah, Nevada, Arizona y Nuevo México. Hay un poco más de 439.000 registros en este archivo y hay 37 columnas de información que describen cada incendio durante este periodo.

  • Utilice la función read.table() para cargar estos datos en un nuevo objeto dataframe

df = read.table("C:/Data/StudyArea.csv", header = TRUE)
  • Recibirá un mensaje de error cuando intente ejecutar esta línea de código. El mensaje de error mensaje de error debería aparecer como se ve a continuación


  • La razón por la que se generó un mensaje de error en este caso es que la función read.table() utiliza espacios como delimitador entre registros y nuestro archivo utiliza comas como delimitador

  • Actualice su llamada a read.table() como se ve a continuación para incluir el argumento sep, que debería ser una coma

df = read.table("C:/Data/StudyArea.csv", sep=",", header = TRUE)
  • Cuando ejecute esta línea de código verá un nuevo error


  • La función read.table() NO rellenará automáticamente los valores que falten con un valor por defecto como NA, así que como algunas de las columnas están vacías en nuestras filas obtenemos un mensaje de error que indica que una línea en particular no tiene las 14 columnas de información. Podemos arreglar esto añadiendo el parámetro fill y poniéndolo igual a TRUE. Actualice su código como se ve a continuación para añadir el parámetro de fill
df = read.table("C:/Data/StudyArea.csv", header=TRUE, fill=TRUE, sep=",")
nrow(df)
## [1] 153095
  • Cuando ejecute esta línea de código, importará el contenido del archivo a un objeto objeto dataframe. Sin embargo, si mira la pestaña Global Enviroment en RStudio verá que sólo se han cargado 153.095 registros y sin embargo sabemos que hay más de 400.000.

  • Las comillas (simples o dobles) en un archivo csv pueden hacer que no se carguen los registros. Agreguemos un parámetro más para manejar los registros que fueron descartados debido a comillas.

df = read.table("C:/Data/StudyArea.csv", header=TRUE, fill=TRUE, quote="\"", sep=",")
nrow(df)
## [1] 439362
  • Al ejecutar esta línea de código, se deberían importar 440.476 registros. Los datos se cargan en un objeto R data.frame que es una estructura que se asemeja a una tabla. Por ahora, puede pensar en ellos como tablas que contienen columnas y filas.

  • La función read.table() se utiliza normalmente para cargar archivos de texto delimitados por tabulaciones, pero muchos intentarán utilizar la función read.table() con archivos de formato csv sin entender todos los parámetros que pueden necesitar ser incluidos. En su lugar, también puede utilizar read.csv() como haremos en el siguiente paso.

Lectura de archivos csv con read.csv()

  • La función read.csv() también es una función incorporada en R que es casi idéntica a read.table(), con la excepción de que los argumentos de cabecera y relleno se establecen en TRUE por defecto. En este paso se observa que es mucho más fácil cargar un archivo csv usando read.csv().

  • La función read.csv() maneja automáticamente la mayoría de las situaciones que deben identificarse cuando se utiliza read.table() para cargar un archivo csv. Introduzca y ejecute el código que se ve a continuación para ver cuánto más fácil es esto con read.csv(). Esto cargará correctamente los más de 400.000 registros del archivo csv. En general esta función es mucho más fácil de usar que read.table().

df = read.csv("C:/Data/StudyArea.csv")
nrow(df)
## [1] 439362

Lectura de archivo delimitado por tabulaciones con read.table()

  • La función read.table() se utiliza más a menudo para leer el contenido de un archivo delimitado por tabulaciones. En este paso aprenderás a hacerlo

  • La carpeta RDataSets incluye un archivo llamado all_genes_pombase.txt, que está delimitado por texto. Abra este archivo con Excel o alguna otra aplicación para ver la estructura de los campos y los delimitadores.

  • En la ventana de la consola de R introduzca y ejecute el código que ve a continuación para importar el archivo.

df2 = read.table("C:/Data/all_genes_pombase.txt", header=TRUE, sep="\t", quote="\"")
nrow(df2)
## [1] 7019
head(df2)
##     ensembl_id        name chromosome
## 1  SPAC1002.01 SPAC1002.01          I
## 2  SPAC1002.02       pom34          I
## 3 SPAC1002.03c        gls2          I
## 4 SPAC1002.04c       taf11          I
## 5 SPAC1002.05c        jmj2          I
## 6 SPAC1002.06c        bqt2          I
##                                                     description   feature_type
## 1                                     conserved fungal protein  protein_coding
## 2                                            nucleoporin Pom34  protein_coding
## 3                            glucosidase II alpha subunit Gls2  protein_coding
## 4 transcription factor TFIID complex subunit Taf11 (predicted)  protein_coding
## 5                                     histone demethylase Jmj2  protein_coding
## 6                               bouquet formation protein Bqt2  protein_coding
##   strand   start     end
## 1      1 1798347 1799015
## 2      1 1799061 1800053
## 3     -1 1799915 1803141
## 4     -1 1803624 1804491
## 5     -1 1804548 1806797
## 6     -1 1807270 1807781
  • Esto debería cargar 7019 registros en el data.frame. Notará que muchos de los parámetros todavía tienen que ser utilizados cuando se carga el dataset, por lo que no es tan fácil de como se podría esperar incluso en este caso.

Uso de readr para cargar datos

  • Hasta ahora en este capítulo hemos visto varias funciones incorporadas en R para leer archivos externos como dataframe. El paquete tidyverse incluye un subpaquete llamado readr que también puede utilizarse para cargar datos externos. El paquete readr incluye una función read_csv() que carga los datos mucho más rápido que la función interna read.csv().

  • En general, la función read_csv() del paquete readr es preferible a las funciones que se encuentran en la instalación básica de R. El paquete readr también incluye algunas otras funciones para cargar varios formatos de archivo, incluyendo read_delim(), read_csv2(), y read_tsv(). Cada una estas funciones aceptan los mismos parámetros, por lo que una vez que haya aprendido a utilizar cualquiera de las funciones de R para cargar datos, puede fácilmente utilizar cualquiera de las otras.

  • En este paso vas a utilizar la función read_csv() que se encuentra en el paquete readr para cargar los datos en un dataframe.

  1. Cargar la librería readr
library(readr)
  1. La función read_csv() del paquete readr puede utilizarse para cargar archivos csv. En comparación con las funciones de carga base que vimos anteriormente, las funciones readr son significativamente más rápidas (10 veces), incluyen una útil barra de progreso para proporcionar información sobre el progreso de la carga de archivos grandes, y todas las funciones trabajan exactamente de la misma manera.
  • Añada y ejecute el código a continuación. Observe que los datos se cargan mucho más rápido en el objeto dataframe. El argumento col_types fue utilizado en este caso para cargar todas las columnas como un tipo de datos de caracteres para simplificar. De lo contrario, tendríamos que hacer un preprocesamiento adicional de los datos para tener en cuenta los distintos tipos de datos de las columnas.
dfReadr = read_csv("C:/Data/StudyArea.csv", col_types = cols(.default = "c"), col_names = TRUE)
  1. Ahora vamos a ejecutar esta función de nuevo, pero esta vez quitando el argumento col_types para poder ver un ejemplo de algunos de los posibles errores de carga que pueden ocurrir. Lo primero que verá es una lista de las columnas que se importarán junto con el tipo de datos de la columna. A continuación aparecerá un mensaje de advertencia indicando que hubo errores de análisis en la carga.
dfReadr = read_csv("C:/Data/StudyArea.csv", col_names = TRUE)
  1. Puede utilizar la función problems() para obtener una lista de los errores de análisis. Añada y ejecute el código que ves a continuación.
problems(dfReadr)
## # A tibble: 0 × 5
## # ℹ 5 variables: row <int>, col <int>, expected <chr>, actual <chr>, file <chr>
  1. Por el aspecto de los mensajes de error parece que hay un problema con la columna UNIT. Si vuelve a mirar la lista de columnas y tipos de datos, observará que la columna UNIT fue creada como un tipo de datos entero. Sin embargo, si abre el archivo StudyArea.csv en Excel o en otra aplicación, verá rápidamente verá que no todos los valores son numéricos, algunos incluyen letras. Esto explica los errores de análisis en el dataset.
  • Actualice su código como se ve a continuación y ejecútelo de nuevo. Esto establece la columna UNIT a un tipo de datos de carácter (texto).
dfReadr = read_csv("C:/Data/StudyArea.csv", col_types = list(UNIT = col_character()),
col_names = TRUE)
  • Esta vez debería obtener una carga limpia del dataset. Eso no significa que los datos no necesiten alguna preparación y limpieza adicional. Por ejemplo, hay algunos campos de fecha, como STARTDATED, que se cargaron como caracteres pero es mejor que se carguen como campos de fecha. Podemos dejar este trabajo de preparación adicional para un ejercicio posterior.
  1. Puede examinar las primeras líneas del dataframe introduciendo la función head() como se ve a continuación.
head(dfReadr)
## # A tibble: 6 × 14
##     FID ORGANIZATI UNIT  SUBUNIT SUBUNIT2        FIRENAME CAUSE YEAR_ STARTDATED
##   <dbl> <chr>      <chr> <chr>   <chr>           <chr>    <chr> <dbl> <chr>     
## 1     0 FWS        81682 USCADBR San Diego Bay … PUMP HO… Human  2001 1/1/01 0:…
## 2     1 FWS        81682 USCADBR San Diego Bay … I5       Human  2002 5/3/02 0:…
## 3     2 FWS        81682 USCADBR San Diego Bay … SOUTHBAY Human  2002 6/1/02 0:…
## 4     3 FWS        81682 USCADBR San Diego Bay … MARINA   Human  2001 7/12/01 0…
## 5     4 FWS        81682 USCADBR San Diego Bay … HILL     Human  1994 9/13/94 0…
## 6     5 FWS        81682 USCADBR San Diego Bay … IRRIGAT… Human  1994 4/22/94 0…
## # ℹ 5 more variables: CONTRDATED <chr>, OUTDATED <chr>, STATE <chr>,
## #   STATE_FIPS <dbl>, TOTALACRES <dbl>
  • En esta sección se estudiaron varias funciones para cargar un archivo de datos externo incluyendo las funciones incorporadas en R read.table() y read.csv(). Aunque estas funciones funciones pueden hacer el trabajo, la función read_csv() que se encuentra en el paquete readr es una función mucho más eficiente para cargar datos externos. En la siguiente sección se aprenderá a transformar datasets utilizando el paquete dplyr. Se estudiarán técnicas para filtrar el contenido de un dataframe, seleccionar las columnas específicas que se utilizarán, organizar las filas en orden ascendente o descendente y resumir y agrupar un dataset.

Transformación de datos

  • Antes de poder analizar un conjunto de datos en R, a menudo es necesario manipularlo o transformarlo de diversas maneras. El paquete dplyr, que forma parte del paquete más amplio tidyverse, proporciona un conjunto de funciones que permiten transformar un conjunto de datos de diversas maneras.

  • En esta sección trataremos los siguientes temas:

    • Filtrar registros para crear un subconjunto
    • Limitar la lista de columnas
    • Organizar las filas en orden ascendente o descendente
    • Añadir filas
    • Resumir y agrupar
    • Canalización para la eficiencia del código

Filtrar registros para crear un subconjunto

  • La primera función de dplyr que examinaremos es filter(). La función filter() se utiliza para crear un subconjunto de registros basado en algún valor. Por ejemplo, puede querer crear un dataframe de incendios forestales que contenga incidentes que hayan quemado más de 25.000 acres. Siempre que tenga un dataframe existente que incluya una columna que mida el número de acres quemadas, puede crear este subconjunto utilizando la función filter().

  • Como será el caso de todas las funciones de dplyr que examinemos, el primer argumento que se pasa a la función filter() es un objeto de dataframe. Cada parámetro adicional pasado a la función es una expresión condicional utilizada para filtrar el dataframe.

  • Por ejemplo, observe la siguiente línea de código. Esta declaración llama a la función filter() para crear una nueva variable llamada df25k, que contendrá sólo las filas en las que la columna ACRES contenga un valor superior a 25000.

df25k = filter(df, ACRES >= 25000)
  • En el siguiente ejemplo de código, se pasan dos expresiones condicionales. La primera se utiliza para filtrar los registros de manera que el número de ACRES sea mayor mayor o igual a 25.000, y la segunda filtra los registros para los que la columna YEAR contenga un valor de 2016.
df25k = filter(df, ACRES >= 25000, YEAR == 2016)
  • En este caso, la variable df25k incluirá los registros en los que coincidan ambas condiciones: la superficie quemada es superior a 25000 y el año del incendio fue 2016. Este también puede reescribirse como un único parámetro que utiliza el operador & para combinar como se ve a continuación.
df25k = filter(df, ACRES >= 25000 & YEAR == 2016)
  • En este ejercicio aprenderá a utilizar la función filter() para crear un subconjunto de registros basado en algún valor. Los ejercicios de este capítulo requieren los siguientes paquetes: readr, dplyr, ggplot2. Pueden cargarse desde el panel de paquetes, el panel de la consola o un script. Si es necesario, establezca el directorio de trabajo escribiendo el código: setwd().

  • El repositorio RDataSets contiene un archivo llamado StudyArea.csv, que es un archivo separado por comas que contiene datos de incendios forestales de los años 1980-2016 para los estados de California, Oregón, Washington, Idaho, Montana, Wyoming, Colorado, Utah, Nevada, Arizona y Nuevo México. Hay algo más de 439.000 registros en este archivo y hay 37 columnas de información que describen cada incendio durante este periodo.

  • Utilice la función read_csv() para cargar el conjunto de datos en un dataframe.

dfFires = read_csv("C:/Data/StudyArea.csv", col_types = list(UNIT = col_character()), col_names = TRUE)
  • Utilice la función nrow() para asegurarse de que los aproximadamente 439.000 registros se hayan cargado.
nrow(dfFires)
## [1] 439362
  • Inicialmente utilizaremos una única expresión condicional con la función filter() para crear un subconjunto de registros que contenga sólo los incendios forestales de más de 25.000 acres.
df25k = filter(dfFires, TOTALACRES >= 25000)
  • Obtenga un recuento del número de registros que coinciden con el filtro. Debería haber 655 filas. También puede utilizar la función View(df25k) para ver los datos en un formato tabular.
nrow(df25k)
## [1] 655
head(df25k)
## # A tibble: 6 × 14
##     FID ORGANIZATI UNIT  SUBUNIT SUBUNIT2        FIRENAME CAUSE YEAR_ STARTDATED
##   <dbl> <chr>      <chr> <chr>   <chr>           <chr>    <chr> <dbl> <chr>     
## 1  1875 FWS        22570 USAZKGR Kofa National … KING VA… Human  2005 10/1/05 0…
## 2  1968 FWS        81674 USCAHPR Hopper Mountai… PIRU     Human  2003 10/23/03 …
## 3  2476 FWS        81720 USCATNR San Diego Nati… CEDAR    Human  2003 10/25/03 …
## 4  2477 FWS        81720 USCATNR San Diego Nati… MINE     Human  2003 10/26/03 …
## 5  6929 FWS        61520 USMTCMR Charles M. Rus… SODA CR… Natu…  2006 7/16/06 0…
## 6  9161 FWS        14621 USORSLR Sheldon Nation… BADGER … Natu…  1999 8/24/99 0…
## # ℹ 5 more variables: CONTRDATED <chr>, OUTDATED <chr>, STATE <chr>,
## #   STATE_FIPS <dbl>, TOTALACRES <dbl>
  • También puede incluir múltiples expresiones condicionales como parte del filtro.
df1k = filter(dfFires, TOTALACRES >= 1000, YEAR_ == 2016)
nrow(df1k)
## [1] 152
  • También puede combinar las expresiones en una sola expresión con varias condiciones, como se ve a continuación. El carácter & es el operador “y”. Tendrías que usar el carácter | para incluir un operador “o”.
df1k = filter(dfFires, TOTALACRES >= 1000 & YEAR_ == 2016)
  • Por último, cuando se tiene una lista de valores potenciales que se desea incluir por el filtro se puede utilizar la sentencia %in%. Esta línea de código en particular crearía un dataframe que contiene los incendios ocurridos en los años 2010, 2011 o 2012.
dfYear = filter(dfFires, YEAR_ %in% c(2010, 2011, 2012))
head(dfYear)
## # A tibble: 6 × 14
##     FID ORGANIZATI UNIT  SUBUNIT SUBUNIT2        FIRENAME CAUSE YEAR_ STARTDATED
##   <dbl> <chr>      <chr> <chr>   <chr>           <chr>    <chr> <dbl> <chr>     
## 1 12831 FWS        81627 USCASRR Sacramento Riv… SWR-SR-… Human  2010 5/10/10 0…
## 2 12839 FWS        84555 USNVDSR Desert Nationa… MORMON … Human  2010 6/28/10 0…
## 3 12840 FWS        13561 USWALPR Little Pend Or… WINSLOW… Natu…  2010 7/31/10 0…
## 4 12841 FWS        13700 USWAHFR Hanford Reach … SADDLE … Natu…  2010 7/31/10 0…
## 5 12847 FWS        84555 USNVDSR Desert Nationa… DEAD HO… Natu…  2010 7/25/10 0…
## 6 12921 FWS        22530 USAZBAR Buenos Aires N… M M 14.… Unde…  2010 11/2/10 0…
## # ℹ 5 more variables: CONTRDATED <chr>, OUTDATED <chr>, STATE <chr>,
## #   STATE_FIPS <dbl>, TOTALACRES <dbl>

Acotar la lista de columnas con select()

  • El archivo StudyArea.csv con el que ha estado trabajando en los ejercicios incluye 37 columnas de información. En la mayoría de los casos no necesitará todas las columnas. La función select() puede utilizarse para reducir la lista de columnas para incluir sólo las necesarias para una tarea.

  • Utilice la función read_csv() para cargar el conjunto de datos en un dataframe. Nota: En aras de la exhaustividad, se cargarán los datos externos del archivo StudyArea.csv en el dataframe dfFires, pero este paso no es absolutamente necesario si está haciendo los ejercicios en secuencia en la misma sesión de R Studio.

dfFires = read_csv("C:/Data/StudyArea.csv", col_types = list(UNIT = col_character()), col_names = TRUE)
  • En una nueva línea, añada una llamada a la función select() como se ve a continuación para limitar las columnas que se devuelven.
dfFires2 = select(dfFires, FIRENAME, TOTALACRES, YEAR_)
  • Visualice las primeras filas y observe que ahora sólo tenemos tres columnas.
head(dfFires2)
## # A tibble: 6 × 3
##   FIRENAME   TOTALACRES YEAR_
##   <chr>           <dbl> <dbl>
## 1 PUMP HOUSE        0.1  2001
## 2 I5                3    2002
## 3 SOUTHBAY          0.5  2002
## 4 MARINA            0.1  2001
## 5 HILL              1    1994
## 6 IRRIGATION        0.1  1994
  • Muchos de los nombres de las columnas que importes no serán muy fáciles de leer, por lo que no es raro que quiera renombrar las columnas también. Esto puede lograrse usando la función select().
dfFires2 = select(dfFires, "FIRE" = "FIRENAME", "ACRES" ="TOTALACRES", "YR" = "YEAR_")
head(dfFires2)
## # A tibble: 6 × 3
##   FIRE       ACRES    YR
##   <chr>      <dbl> <dbl>
## 1 PUMP HOUSE   0.1  2001
## 2 I5           3    2002
## 3 SOUTHBAY     0.5  2002
## 4 MARINA       0.1  2001
## 5 HILL         1    1994
## 6 IRRIGATION   0.1  1994
  • También hay una serie de útiles funciones de ayuda que puede utilizar con la función select() para filtrar las columnas devueltas. Estas incluyen starts_with(), ends_with(), contains(), matches() y num_range().
dfFires3 = select(dfFires, contains("DATE"))
head(dfFires3)
## # A tibble: 6 × 3
##   STARTDATED   CONTRDATED   OUTDATED
##   <chr>        <chr>        <chr>   
## 1 1/1/01 0:00  1/1/01 0:00  <NA>    
## 2 5/3/02 0:00  5/3/02 0:00  <NA>    
## 3 6/1/02 0:00  6/1/02 0:00  <NA>    
## 4 7/12/01 0:00 7/12/01 0:00 <NA>    
## 5 9/13/94 0:00 9/13/94 0:00 <NA>    
## 6 4/22/94 0:00 4/22/94 0:00 <NA>
  • También puede hacer varias llamadas a estas funciones de ayuda
dfFires3 = select(dfFires, contains("DATE"), starts_with("TOTAL"))
head(dfFires3)
## # A tibble: 6 × 4
##   STARTDATED   CONTRDATED   OUTDATED TOTALACRES
##   <chr>        <chr>        <chr>         <dbl>
## 1 1/1/01 0:00  1/1/01 0:00  <NA>            0.1
## 2 5/3/02 0:00  5/3/02 0:00  <NA>            3  
## 3 6/1/02 0:00  6/1/02 0:00  <NA>            0.5
## 4 7/12/01 0:00 7/12/01 0:00 <NA>            0.1
## 5 9/13/94 0:00 9/13/94 0:00 <NA>            1  
## 6 4/22/94 0:00 4/22/94 0:00 <NA>            0.1

Organizar las filas

  • La función arrange() del paquete dplyr puede utilizarse para ordenar las filas de un dataframe. Esta función acepta un conjunto de columnas para ordenarlas, siendo el orden por defecto ordenación de las filas en orden ascendente. Sin embargo, puede pasar la función desc() helper para ordenar las filas en orden descendente.

  • Utilice la función read_csv() para cargar el conjunto de datos en un dataframe.

dfFires = read_csv("C:/Data/StudyArea.csv", col_types = list(UNIT = col_character()), col_names = TRUE)
  • Filtrar el conjunto de datos para que contenga solo los incendios de más de 1.000 acres quemados del año 2016.
df1k = filter(dfFires, TOTALACRES >= 1000, YEAR_ == 2016)
  • Añade y ejecuta el código que ves a continuación para crear un subconjunto de columnas y renombrarlas
df1k = select(df1k, "NAME" = "FIRENAME", "ACRES" = "TOTALACRES", "YR" = "YEAR_")
  • Ordena las filas para que estén en orden ascendente
arrange(df1k, ACRES)
## # A tibble: 152 × 3
##    NAME        ACRES    YR
##    <chr>       <dbl> <dbl>
##  1 Crackerbox  1000   2016
##  2 Lakes       1000   2016
##  3 Choulic 2   1008   2016
##  4 Amigo Wash  1020.  2016
##  5 Granite     1030   2016
##  6 Tie         1031   2016
##  7 Black       1040   2016
##  8 Bybee Creek 1072   2016
##  9 MARSHES     1080   2016
## 10 Bug Creek   1089   2016
## # ℹ 142 more rows
  • Utilice la función de ayuda desc() para ordenar las filas en orden descendente.
arrange(df1k, desc(ACRES))
## # A tibble: 152 × 3
##    NAME        ACRES    YR
##    <chr>       <dbl> <dbl>
##  1 PIONEER    188404  2016
##  2 Junkins    181320  2016
##  3 Range 12   171915  2016
##  4 Erskine     48007  2016
##  5 Cedar       45977  2016
##  6 Maple       45425  2016
##  7 Rail        43799  2016
##  8 North Fire  42102  2016
##  9 Laidlaw     39813  2016
## 10 BLUE CUT    36274  2016
## # ℹ 142 more rows

Añadir filas con mutate()

  • La función mutate() se utiliza para añadir nuevas columnas a un dataframe que son resultado de operaciones entre otras columnas del dataframe. Las nuevas columnas creadas con la función mutate() se añadirán al final del dataframe. Para este ejercicio usará el paquete lubridate. El paquete lubridate forma parte de tidyverse y se utiliza para trabajar con fechas y horas.

  • Utilice la función read_csv() para cargar el conjunto de datos en un dataframe

dfFires = read_csv("C:/Data/StudyArea.csv", 
                   col_types = list(UNIT = col_character()),
                   col_names = TRUE)
  • Utilice la función select() para definir un conjunto de columnas para el dataframe.
df = select(dfFires, ORGANIZATI, STATE, YEAR_, TOTALACRES, CAUSE, STARTDATED)
  • Realice un filtrado básico de los datos para que sólo se incluyan los incendios de más de 1.000 acres quemados y que tengan una causa humana o natural
df = filter(df, TOTALACRES >= 1000 & CAUSE %in% c("Human", "Natural"))
  • Utilice la función mutate() para crear una nueva columna DOY que contenga el día del año en que se inició el incendio. La función yday() del paquete lubridate se utiliza para devolver el día del año utilizando una fecha de entrada formateada de la columna STARTDATED.
df = mutate(df, DOY = yday(as.Date(df$STARTDATED, format="%m/%d/%y%H:%M")))
knitr::kable(head(df))
ORGANIZATI STATE YEAR_ TOTALACRES CAUSE STARTDATED DOY
FWS Arizona 1988 1500 Human 3/26/88 0:00 86
FWS Arizona 1986 10390 Human 5/15/86 0:00 135
FWS Montana 1986 1400 Human 6/27/86 0:00 178
FWS Arizona 2002 1035 Human 2/28/02 0:00 59
FWS Arizona 2000 5700 Human 4/9/00 0:00 100
FWS Arizona 2000 2750 Human 5/14/00 0:00 135

Resumir y agrupar

  • Las estadísticas de resumen de un dataframe pueden producirse con la función summarize(). La agrupación de datos en un dataframe facilita el paradigma de dividir-aplicar-combinar. Este paradigma primero divide los datos en grupos, utilizando la función group_by() en dplyr, luego aplica el análisis al grupo y, finalmente, combina los resultados.

  • En este ejercicio utilizará las funciones mutate(), summarize() y group_by() para agrupar los incendios forestales por década y producir un resumen del tamaño medio de los incendios forestales para cada década.

  • Utilice la función read_csv() para cargar el conjunto de datos en un dataframe.

dfFires = read_csv("C:/Data/StudyArea.csv", 
                   col_types = list(UNIT = col_character()), 
                   col_names = TRUE)
  • Seleccione las columnas que se utilizarán en el ejercicio.
df = select(dfFires, ORGANIZATI, STATE, YEAR_, TOTALACRES, CAUSE)
  • Filtrar los registros
df = filter(df, TOTALACRES >= 1000)
  • Utilice la función mutate() para crear una nueva columna llamada DECADE que define la década en la que se produjo cada incendio. En este caso se llama a la función ifelse() para producir los valores de cada década.
df <- mutate(df, DECADE = ifelse(YEAR_ %in% 1980:1989, "1980-1989", 
                          ifelse(YEAR_ %in% 1990:1999, "1990-1999",
                          ifelse(YEAR_ %in% 2000:2009, "2000-2009", 
                          ifelse(YEAR_ %in% 2010:2016, "2010-2016", "-99")))))
knitr::kable(head(df))
ORGANIZATI STATE YEAR_ TOTALACRES CAUSE DECADE
FWS Arizona 1988 1500 Human 1980-1989
FWS Arizona 1986 10390 Human 1980-1989
FWS Montana 1986 1400 Human 1980-1989
FWS Arizona 2002 1035 Human 2000-2009
FWS Arizona 2000 5700 Human 2000-2009
FWS Arizona 2000 2750 Human 2000-2009
  • Utilice la función group_by() para agrupar el dataframe por década.
grp = group_by(df, DECADE)
  • Resuma el tamaño medio de los incendios forestales por década utilizando la función summarize()
sm = summarize(grp, mean(TOTALACRES))
knitr::kable(sm)
DECADE mean(TOTALACRES)
1980-1989 8128.645
1990-1999 8333.036
2000-2009 12329.181
2010-2016 14443.197
  • Pongamos en orden las cosas cambiando el nombre de la nueva columna producida por la función summarize()
names(sm) <- c("DECADE", "MEAN_ACRES_BURNED")
  • Por último, vamos a crear un gráfico de barras con los resultados. Discutiremos la creación de diferentes tipos de tablas y gráficos a medida que avancemos en los capítulos posteriores
library(ggplot2)

ggplot(data=sm) + 
  geom_col(mapping = aes(x=DECADE, y=MEAN_ACRES_BURNED), fill="red")

Tuberías

  • La canalización es una forma eficiente de enviar la salida de una función a otra función sin crear un conjunto de datos intermedio y es más útil cuando se tiene una serie de funciones para ejecutar. La sintaxis para la canalización consiste en utilizar los caracteres %>% al final de cada sentencia que desee canalizar. En este ejercicio aprenderá a utilizar la canalización para encadenar dataframe de entrada y salida. Resolveremos el ejercicio anterior usando las tuberias.
library(lubridate)

df = read_csv("C:/Data/StudyArea.csv", col_types = list(UNIT = col_character()), col_names = TRUE) %>%
  select(ORGANIZATI, STATE, YEAR_, TOTALACRES, CAUSE, STARTDATED) %>%
  filter(TOTALACRES >= 1000 & CAUSE %in% c("Human", "Natural")) %>%
  mutate(DOY = yday(as.Date(STARTDATED, format="%m/%d/%y %H:%M")))

knitr::kable(head(df))
ORGANIZATI STATE YEAR_ TOTALACRES CAUSE STARTDATED DOY
FWS Arizona 1988 1500 Human 3/26/88 0:00 86
FWS Arizona 1986 10390 Human 5/15/86 0:00 135
FWS Montana 1986 1400 Human 6/27/86 0:00 178
FWS Arizona 2002 1035 Human 2/28/02 0:00 59
FWS Arizona 2000 5700 Human 4/9/00 0:00 100
FWS Arizona 2000 2750 Human 5/14/00 0:00 135
  • La primera línea de código lee el contenido del archivo externo StudyArea.csv en una variable de dataframe df como hemos hecho en todos los demás ejercicios de este capítulo. Sin embargo, observará la inclusión de la sentencia de canalización (%>%>) al final de la línea. Esto asegura que el contenido de la variable df será automáticamente automáticamente a la función select().

  • Observe que la función select() no crea una variable como hemos hecho en ejercicios anteriores, y que hemos omitido el primer parámetro, que normalmente habría sido la variable del dataframe. Se da a entender que la variable df será pasada a la función select().

  • Este mismo proceso de incluir la sentencia piping al final de cada línea y dejando fuera el primer parámetro se repite para todas las líneas de código adicionales en las que queramos pasar automáticamente la variable df a la siguiente función de dplyr.

  • En esta sección se utilizó el paquete dplyr para realizar varias funciones de transformación de datos. Se aprendió a limitar columnas con la función select() filtrar un marco de datos basado en una o más expresiones, añadir columnas con mutate(), y a resumir y agrupar datos. Por último, ha aprendido a utilizar la canalización para hacer su código más eficiente. En el próximo capítulo se aprenderá a crear conjuntos de datos ordenados con el paquete tidyr.

Ejercicio para entregar

  • Crear un nuevo dataframe que sea un subconjunto del dataframe original de dfFires. El subconjunto debe contener todos los incendios del Estado de Idaho y las columnas deben ser limitadas para que sólo estén presentes las columnas YEAR_, CAUSE y TOTALACRES. Cambie el nombre de las columnas. Agrupe los datos por CAUSE y YEAR_ y luego resuma por el total de acres quemados. Trazar los resultados.

  • Resuelva la Tarea 1.1 de la Sección 1 de Python utilizando R.

Creación de datos ordenados

  • La ordenación de datos es una forma coherente de organizar los datos en R y puede facilitarse a través del paquete tidyr que se encuentra en el ecosistema ecosistema tidyverse. Hay tres reglas que podemos seguir para hacer un conjunto de datos ordenado.

  • En primer lugar, cada variable debe tener su propia columna. En segundo lugar, cada observación debe tener su propia fila y, por último, cada valor debe tener su propia celda. Esto se ilustrado por el diagrama siguiente.

  • En primer lugar, tener una estructura de datos consistente es muy importante. Los paquetes que forman parte de tidyverse incluyendo dplyr y ggplot2 están diseñados para trabajar con datos ordenados, así que asegurar que que tus datos sean uniformes facilita el procesamiento eficiente de tus datos. Además, colocar las variables en columnas permite facilitar la vectorización en R.

  • Muchos de los conjuntos de datos que encuentre no estarán ordenados y requerirán algo de trabajo por su parte. Puede haber muchas razones por las que un conjunto de datos no esté ordenado. A menudo, las personas que crearon el conjunto de datos no están familiarizadas con los principios de los datos ordenados.

  • Otra razón común por la que los conjuntos de datos no están ordenados es que los datos se organizan a menudo para facilitar algo más que el análisis. Para que la introducción de datos sea lo más fácil posible en ocasiones se suelen organizar los datos de forma poco ordenada. Así, muchos conjuntos de datos requieren algún tipo de ordenación antes de poder empezar el análisis.

  • El primer paso es averiguar cuáles son las variables y observaciones del conjunto de datos. Esto le facilitará la comprensión de lo que deben ser las columnas y las. Además, también tendrá que resolver uno o dos problemas comunes. Tendrá que averiguar si una variable está repartida en varias columnas, y si una observación está dispersa en varias filas. Estos conceptos se conocen como reunión y dispersión. Examinaremos examinaremos más a fondo estos conceptos en los ejercicios de este capítulo.

  • En este capítulo trataremos los siguientes temas:

    • Recopilación
    • Distribución
    • Separación
    • Unión

Recopilación

  • Un problema común en muchos conjuntos de datos es que los nombres de las columnas no son variables sino valores de una variable. En la figura siguiente, las columnas 1999 y 2000 son en realidad valores de la variable YEAR. Cada fila de la tabla existente representa en realidad dos observaciones. El paquete tidyr puede utilizarse para reunir estas columnas existentes en una nueva variable. En este caso, tenemos que crear una nueva columna columna llamada YEAR y luego reunir en esta los valores existentes en las columnas 1999 y 2000.
  • La función gather() del paquete tidyr puede utilizarse para realizar la recopilación de datos.
gather("1999", "2000", key = "year", value = "cases")
  • Hay tres parámetros de la función gather(). El primero es el conjunto de columnas que representan lo que deberían ser valores y no variables. Estas serían las columnas 1999 y 2000 en el ejemplo que hemos seguido.
  • A continuación, tendrá que nombrar la variable de la nueva columna. Esto también se llama la clave key, y en este caso será la variable del año. Por último, tendrás que proporcionar el valor value, que es el nombre de la variable cuyos valores se reparten por las celdas.

  • En este ejercicio aprenderás a utilizar la función gather() para resolver este tipo de problemas:
  1. Descargue el archivo CountryPopulation.csv localizado en RDataSets. Abra este archivo, por ejemplo en Microsoft Excel, o algún otro tipo de software de hoja de cálculo. El archivo debería tener un aspecto similar al de la pantalla que se muestra a continuación. Esta hoja de cálculo incluye el año 2017.
  • Las columnas de cada año representan valores, no variables. Estas columnas deben reunirse en un nuevo par de variables que representen el año y la población. En este ejercicio utilizarás la función gather() para realizar esta tarea de ordenación de datos.

  1. Si es necesario, cargue los paquetes readr y tidyr haciendo clic en las casillas de verificación en el panel de paquetes o incluyendo la siguiente línea de código.
library(readr)
library(tidyr)
  1. Cargue el archivo CountryPopulation.csv en RStudio escribiendo el código que ve abajo en el panel de la consola
dfPop = read_csv("C:/Data/CountryPopulation.csv", col_names = TRUE)
str(dfPop)
## spc_tbl_ [264 × 10] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ Country Name: chr [1:264] "Aruba" "Afghanistan" "Angola" "Albania" ...
##  $ Country Code: chr [1:264] "ABW" "AFG" "AGO" "ALB" ...
##  $ 2010        : num [1:264] 101669 28803167 23369131 2913021 84449 ...
##  $ 2011        : num [1:264] 102053 29708599 24218565 2905195 83751 ...
##  $ 2012        : num [1:264] 102577 30696958 25096150 2900401 82431 ...
##  $ 2013        : num [1:264] 103187 31731688 25998340 2895092 80788 ...
##  $ 2014        : num [1:264] 103795 32758020 26920466 2889104 79223 ...
##  $ 2015        : num [1:264] 104341 33736494 27859305 2880703 78014 ...
##  $ 2016        : num [1:264] 104822 34656032 28813463 2876101 77281 ...
##  $ 2017        : num [1:264] 105264 35530081 29784193 2873457 76965 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   `Country Name` = col_character(),
##   ..   `Country Code` = col_character(),
##   ..   `2010` = col_double(),
##   ..   `2011` = col_double(),
##   ..   `2012` = col_double(),
##   ..   `2013` = col_double(),
##   ..   `2014` = col_double(),
##   ..   `2015` = col_double(),
##   ..   `2016` = col_double(),
##   ..   `2017` = col_double()
##   .. )
##  - attr(*, "problems")=<externalptr>
  1. Utilice la función View() para mostrar los datos en una estructura tabular.
View(dfPop)
  1. Utilice la función gather() como se ve a continuación
dfPop2 = gather(dfPop, 
                "2010", "2011", "2012", "2013", "2014", "2015", "2016", "2017", 
                key = "YEAR", 
                value = "POPULATION")
knitr::kable(head(dfPop2, 10))
Country Name Country Code YEAR POPULATION
Aruba ABW 2010 101669
Afghanistan AFG 2010 28803167
Angola AGO 2010 23369131
Albania ALB 2010 2913021
Andorra AND 2010 84449
Arab World ARB 2010 356508908
United Arab Emirates ARE 2010 8270684
Argentina ARG 2010 41223889
Armenia ARM 2010 2877311
American Samoa ASM 2010 55637
  • Otra opción también sería la siguiente, cuando contamos con un gran número de columnas de este tipo
years <- colnames(dfPop)[grep("^\\d{4}$", colnames(dfPop))]

dfPop2 <- dfPop %>%
  gather(key = "YEAR", value = "POPULATION", all_of(years))

knitr::kable(head(dfPop2, 10))
Country Name Country Code YEAR POPULATION
Aruba ABW 2010 101669
Afghanistan AFG 2010 28803167
Angola AGO 2010 23369131
Albania ALB 2010 2913021
Andorra AND 2010 84449
Arab World ARB 2010 356508908
United Arab Emirates ARE 2010 8270684
Argentina ARG 2010 41223889
Armenia ARM 2010 2877311
American Samoa ASM 2010 55637
  • Donde, en expresiones regulares, "^\\d{4}$" tiene el siguiente significado:

    • ^: Representa el inicio de una cadena.
    • \\d: Representa un dígito.
    • {4}: Indica que el elemento anterior (\\d, en este caso) debe aparecer exactamente 4 veces.
    • $: Representa el final de una cadena.
  • En resumen, "^\\d{4}$" coincide con cualquier cadena que contenga exactamente 4 dígitos y no contenga ningún otro carácter adicional antes o después de los dígitos.

Distribución

  • La distribución es lo contrario de la reunión y se utiliza cuando una observación se distribuye en varias filas. En el diagrama siguiente, la tabla debe definir una observación de un país por año. Sin embargo, observará que está repartida en dos filas. Una fila para cases y otra para population.

  • Podemos utilizar la función spread() para solucionar este problema. La función spread() toma dos parámetros: la columna que contiene los nombres de las variables repetidas, conocida como key y una columna que contiene valores de múltiples variables, que llamamos value

  • Instale el paquete devtools y los conjuntos de datos DSR utilizando el código que ve a continuación escribiendo en el panel de la consola. Alternativamente, puede utilizar el panel de paquetes para instalar los librerías
install.packages("devtools")
devtools::install_github("garrettgman/DSR")
knitr::kable(head(table2))
country year type count
Afghanistan 1999 cases 745
Afghanistan 1999 population 19987071
Afghanistan 2000 cases 2666
Afghanistan 2000 population 20595360
Brazil 1999 cases 37737
Brazil 1999 population 172006362
  • Utilice la función spread() para corregir este problema.
table2b = spread(table2, key = type, value = count)
knitr::kable(head(table2b))
country year cases population
Afghanistan 1999 745 19987071
Afghanistan 2000 2666 20595360
Brazil 1999 37737 172006362
Brazil 2000 80488 174504898
China 1999 212258 1272915272
China 2000 213766 1280428583

Separación

  • Otro caso común es el de dos variables que se colocan en la misma columna. Por ejemplo, la hoja de cálculo siguiente tiene una columna State-County Name que en realidad contiene dos variables separadas por una barra.

  • La función separate() puede utilizarse para dividir una columna en varias columnas dividiendo por un separador. Por defecto, la función separate() buscará automáticamente cualquier carácter no alfanumérico o se puede definir un carácter específico (ver separate()).
  1. En la carpeta de RDataSets hay un archivo llamado usco2005.csv. Abra este archivo, por ejemplo con Microsoft Excel, o algún otro tipo de software de hoja de cálculo. El archivo debería tener un aspecto similar al de la captura de pantalla de abajo.

  1. Cargue el archivo usco2005.csv en RStudio escribiendo el código que ve a continuación en el panel de la consola
df = read_csv("C:/Data/usco2005.csv", col_names = TRUE)
knitr::kable(head(df, 10))
STATE STATEFIPS COUNTYFIPS FIPS State-County Name TP-TotPop
AL 1 1 1001 Alabama-Autauga County 48.612
AL 1 3 1003 Alabama-Baldwin County 162.586
AL 1 5 1005 Alabama-Barbour County 28.414
AL 1 7 1007 Alabama-Bibb County 21.516
AL 1 9 1009 Alabama-Blount County 55.725
AL 1 11 1011 Alabama-Bullock County 11.055
AL 1 13 1013 Alabama-Butler County 20.766
AL 1 15 1015 Alabama-Calhoun County 112.141
AL 1 17 1017 Alabama-Chambers County 35.460
AL 1 19 1019 Alabama-Cherokee County 24.522
  1. Utilice la función separate() para separar el contenido de la columna State-County Name en las columnas StateAbbrev y CountyName
df2 = separate(df, "State-County Name",into = c("StateAbbrev", "CountyName"))
knitr::kable(head(df2, 10))
STATE STATEFIPS COUNTYFIPS FIPS StateAbbrev CountyName TP-TotPop
AL 1 1 1001 Alabama Autauga 48.612
AL 1 3 1003 Alabama Baldwin 162.586
AL 1 5 1005 Alabama Barbour 28.414
AL 1 7 1007 Alabama Bibb 21.516
AL 1 9 1009 Alabama Blount 55.725
AL 1 11 1011 Alabama Bullock 11.055
AL 1 13 1013 Alabama Butler 20.766
AL 1 15 1015 Alabama Calhoun 112.141
AL 1 17 1017 Alabama Chambers 35.460
AL 1 19 1019 Alabama Cherokee 24.522

Unión

  • La función unite() es exactamente lo contrario de separate(), ya que combina varias columnas en una sola. Aunque no se utiliza tan a menudo como separate(), puede haber ocasiones en las que necesite la funcionalidad proporcionada por unite(). En este ejercicio unirás el marco de datos que fue separado en el último ejercicio.

  • En el panel de la consola, añada el código que ve a continuación para unir las columnas StateAbbrev y CountyName en una sola columna. Por defecto unite() realiza unión con base en la expresión regular "_", "unite(data, col, ..., sep = "_", remove = TRUE)"

df3 = unite(df2, State_County_Name, StateAbbrev, CountyName)
knitr::kable(head(df3, 10))
STATE STATEFIPS COUNTYFIPS FIPS State_County_Name TP-TotPop
AL 1 1 1001 Alabama_Autauga 48.612
AL 1 3 1003 Alabama_Baldwin 162.586
AL 1 5 1005 Alabama_Barbour 28.414
AL 1 7 1007 Alabama_Bibb 21.516
AL 1 9 1009 Alabama_Blount 55.725
AL 1 11 1011 Alabama_Bullock 11.055
AL 1 13 1013 Alabama_Butler 20.766
AL 1 15 1015 Alabama_Calhoun 112.141
AL 1 17 1017 Alabama_Chambers 35.460
AL 1 19 1019 Alabama_Cherokee 24.522

Conclusión

  • En este capítulo se ha presentado el paquete tidyr y su conjunto de funciones para crear conjuntos de datos ordenados. El próximo capítulo le enseñará los fundamentos de la exploración de datos datos utilizando R y tidyverse.

Ejercicio para entregar

  • Considere el conjunto de datos us_state_population.tsv utilizado en la sección de Python, para la creación del mapa coroplético de Estados Unidos. Repita el procedimiento planteado en cada ítem de esta sección para obtener el nuevo dataframe con las nuevas columnas Year y Population. Realice unión y separación utilizando las columnas State y Code.

Técnicas básicas de exploración y visualización de datos en R

Técnicas básicas de exploración

  • El Análisis Exploratorio de Datos (EDA) es un flujo de trabajo diseñado para obtener una mejor comprensión de sus datos. El flujo de trabajo consta de tres pasos.

    1. El primero consiste en generar preguntas sobre los datos. En este paso se quiere ser lo más amplio posible, ya que en este momento no se tiene una buena idea de los datos.
    2. A continuación, se buscan las respuestas a estas preguntas mediante la visualización, la transformación y el modelado de los datos.
    3. Por último, refine sus preguntas o genere nuevas preguntas.
  • En R hay dos herramientas principales que apoyan el proceso de exploración de datos: los gráficos y los estadísticos de resumen. Los datos pueden dividirse generalmente en tipos categóricos o continuos.

  • Las variables categóricas consisten en un pequeño conjunto de valores, mientras que las continuas tienen un un conjunto potencialmente infinito de valores ordenados.

  • Las variables categóricas suelen visualizarse con gráficos de barras, y las continuas con histogramas. Tanto datos categóricos y continuos pueden representarse a través de varios gráficos creados con R.

  • Al realizar la visualización básica de las variables, tendemos a medir la variación o la covariación. La variación es la tendencia de los valores de una variable a cambiar de una medición a otra. La covariación es la tendencia de los valores de dos o más variables a variar de una manera relacionada.

    • Medir la variación categórica con un gráfico de barras
    • Medición de la variación continua con un histograma
    • Medición de la covariación con gráficos de caja
    • Medición de la covariación con el tamaño de los símbolos
    • Creación de gráficos 2D y hexadecimales
    • Generación de estadísticas de resumen

Medición de la variación categórica con un gráfico de barras

  • Un gráfico de barras es una buena manera de visualizar datos categóricos. Se separa cada categoría en una barra separada y la altura de cada barra se define por el número de ocurrencias en esa categoría.
  1. Los ejercicios de esta sección requieren los siguientes paquetes: readr, dplyr, ggplot2. Pueden cargarse desde el panel de paquetes, el panel de la consola o un script

  2. Utilice la función read_csv() para cargar el conjunto de datos en un marco dataframe.

library(dplyr)
library(readr)
library(ggplot2)

dfFires <- read_csv("C:/Data/StudyArea.csv", col_types = list(UNIT = col_character()), col_names = TRUE)
str(dfFires)
## spc_tbl_ [439,362 × 14] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ FID       : num [1:439362] 0 1 2 3 4 5 6 18 20 21 ...
##  $ ORGANIZATI: chr [1:439362] "FWS" "FWS" "FWS" "FWS" ...
##  $ UNIT      : chr [1:439362] "81682" "81682" "81682" "81682" ...
##  $ SUBUNIT   : chr [1:439362] "USCADBR" "USCADBR" "USCADBR" "USCADBR" ...
##  $ SUBUNIT2  : chr [1:439362] "San Diego Bay National Wildlife Refuge" "San Diego Bay National Wildlife Refuge" "San Diego Bay National Wildlife Refuge" "San Diego Bay National Wildlife Refuge" ...
##  $ FIRENAME  : chr [1:439362] "PUMP HOUSE" "I5" "SOUTHBAY" "MARINA" ...
##  $ CAUSE     : chr [1:439362] "Human" "Human" "Human" "Human" ...
##  $ YEAR_     : num [1:439362] 2001 2002 2002 2001 1994 ...
##  $ STARTDATED: chr [1:439362] "1/1/01 0:00" "5/3/02 0:00" "6/1/02 0:00" "7/12/01 0:00" ...
##  $ CONTRDATED: chr [1:439362] "1/1/01 0:00" "5/3/02 0:00" "6/1/02 0:00" "7/12/01 0:00" ...
##  $ OUTDATED  : chr [1:439362] NA NA NA NA ...
##  $ STATE     : chr [1:439362] "California" "California" "California" "California" ...
##  $ STATE_FIPS: num [1:439362] 6 6 6 6 6 6 6 6 6 6 ...
##  $ TOTALACRES: num [1:439362] 0.1 3 0.5 0.1 1 0.1 3 0.1 0.1 0.1 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   FID = col_double(),
##   ..   ORGANIZATI = col_character(),
##   ..   UNIT = col_character(),
##   ..   SUBUNIT = col_character(),
##   ..   SUBUNIT2 = col_character(),
##   ..   FIRENAME = col_character(),
##   ..   CAUSE = col_character(),
##   ..   YEAR_ = col_double(),
##   ..   STARTDATED = col_character(),
##   ..   CONTRDATED = col_character(),
##   ..   OUTDATED = col_character(),
##   ..   STATE = col_character(),
##   ..   STATE_FIPS = col_double(),
##   ..   TOTALACRES = col_double()
##   .. )
##  - attr(*, "problems")=<externalptr>
  1. Para este análisis, filtraremos los datos de modo que solo estén representados los incendios que quemaron más de 1.000 acres (4046.86 metros cuadrados o 0.4047 hectáreas) en los años 2010 a 2016. Añada el código que que se ve a continuación para filtrar los datos y enviar los resultados a un gráfico de barras.
df <- filter(dfFires, TOTALACRES >= 1000, 
             YEAR_ %in% c(2010, 2011, 2012, 2013, 2014, 2015, 2016))
ggplot(data = df) + 
  geom_bar(mapping = aes(x = YEAR_)) +
  scale_x_continuous(breaks = c(2010, 2011, 2012, 2013, 2014, 2015, 2016)) +
  labs(x = "Year", y = "Count")

  1. Utilice la función count() para obtener el recuento real de cada categoría.
count(df, YEAR_)
## # A tibble: 7 × 2
##   YEAR_     n
##   <dbl> <int>
## 1  2010   127
## 2  2011   240
## 3  2012   352
## 4  2013   170
## 5  2014   131
## 6  2015   205
## 7  2016   152

Medición de la variación continua con un histograma

  • La distribución de una variable continua se puede medir con el uso de un histograma. En este ejercicio crearás un histograma de los acres quemados en incendios forestales
  1. En una nueva línea, utilice la función read_csv() para cargar el archivo StudyArea.csv
dfFires <- read_csv("C:/Data/StudyArea.csv", col_types = list(UNIT = col_character()), col_names = TRUE)
  1. Introduzca el marco de datos y utilice la función select() para limitar las columnas y filtrar las filas para que sólo se incluyan los incendios superiores a 1.000 acres. Dado que tenemos un gran número de incendios forestales que quemaron sólo un pequeño número de acres, nos centraremos en los incendios que son un poco más grandes en este caso.
df %>%
  select(ORGANIZATI, STATE, YEAR_, TOTALACRES, CAUSE) %>%
  filter(TOTALACRES >= 1000)
## # A tibble: 1,377 × 5
##    ORGANIZATI STATE      YEAR_ TOTALACRES CAUSE       
##    <chr>      <chr>      <dbl>      <dbl> <chr>       
##  1 FWS        Washington  2010       2755 Natural     
##  2 FWS        Idaho       2010       1801 Undetermined
##  3 FWS        Montana     2010      27898 Natural     
##  4 FWS        Montana     2012       9468 Natural     
##  5 FWS        Washington  2012       1141 Human       
##  6 FWS        Oregon      2010       3083 Human       
##  7 FWS        Oregon      2013      51015 Undetermined
##  8 FWS        Washington  2016     171915 Human       
##  9 FWS        Arizona     2015       6000 Natural     
## 10 FWS        Arizona     2016       1624 Natural     
## # ℹ 1,367 more rows
  1. Cree el histograma utilizando ggplot() con geom_hist() y un tamaño de la celda de 500. Los datos están claramente sesgados hacia el extremo inferior del número de acres quemados.
df %>%
  select(ORGANIZATI, STATE, YEAR_, TOTALACRES, CAUSE) %>%
  filter(TOTALACRES >= 1000) %>%
  slice(1:300) %>%
  ggplot() + 
  geom_histogram(mapping = aes(x=TOTALACRES), binwidth=1000) +
  labs(x = "Total Acres", y = "Count")

  1. También se puede obtener un recuento físico del número de incendios que han caído en cada casilla. Al ver el histograma y el recuento, se puede oservar que la gran mayoría de incendios son pequeños.
df %>%
  count(cut_width(TOTALACRES, 100))
## # A tibble: 338 × 2
##    `cut_width(TOTALACRES, 100)`     n
##    <fct>                        <int>
##  1 [950,1050]                      43
##  2 (1050,1150]                     73
##  3 (1150,1250]                     38
##  4 (1250,1350]                     34
##  5 (1350,1450]                     39
##  6 (1450,1550]                     36
##  7 (1550,1650]                     26
##  8 (1650,1750]                     43
##  9 (1750,1850]                     37
## 10 (1850,1950]                     24
## # ℹ 328 more rows
  1. Reconstruya el histograma utilizando un tamaño de celda de 5000. ¿Cuál es el efecto en la salida?

Medición de la covariación con gráficos de caja

  • Los gráficos de caja proporcionan una representación visual de la dispersión de los datos de una variable. Estos gráficos muestran el rango de valores de una variable junto con la mediana y los cuartiles. Siga las instrucciones proporcionadas a continuación para crear un gráfico de caja que mide la covariación entre la organización y la superficie total quemada.
  1. Utilice la función read_csv() para cargar el archivo StudyArea.csv en un dataframe
dfFires <- read_csv("C:/Data/StudyArea.csv", 
                    col_types = list(UNIT = col_character()), 
                    col_names = TRUE)
  1. Extraiga el marco de datos y filtre las filas para que sólo se incluyan los incendios de entre 5000 y 10000 acres. A continuación, agrupe los datos por organización. La columna ORGANIZATI del conjunto de datos contiene datos categóricos de los organismos del gobierno federal de EE.UU. que han tenido tierras afectadas por incendios forestales. Por último utilice ggplot() con geom_boxplot() para crear un boxplot que muestre la distribución de incendios forestales por organización
df %>%
  filter(TOTALACRES >= 5000 & TOTALACRES <= 10000) %>%
  group_by(ORGANIZATI) %>%
  ggplot(mapping = aes(x = ORGANIZATI, y = TOTALACRES)) + geom_boxplot() +
  labs(x = "Organization", y = "Total Acres")

  1. Puede crear también un nuevo boxplot que mapee la covariación de CAUSE y TOTALACRES

Medición de la covariación con el tamaño del símbolo

  • La función geom_count() puede utilizarse con ggplot() para medir la covariación entre variables utilizando diferentes tamaños de símbolos. Siga las instrucciones proporcionadas a continuación para medir la covariación entre la organización y la causa del incendio forestal utilizando el tamaño del símbolo
  1. Use la función read_csv() para cargar el conjunto de datos en un dataframe
dfFires <- read_csv("C:/Data/StudyArea.csv", 
                    col_types = list(UNIT = col_character()), 
                    col_names = TRUE)
  1. Extraiga el marco de datos y filtre las filas para que sólo se incluyan los incendios forestales que se originaron por causas naturales o humanas. Esto eliminará cualquier registro que son desconocidos o tienen valores perdidos. A continuación, utilice geom_count() para crear un gráfico de símbolos graduados basado en el número de incendios por organización.
df %>%
  filter(CAUSE == "Natural" | CAUSE == "Human") %>%
  group_by(ORGANIZATI) %>%
  ggplot() + geom_count(mapping = aes(x = ORGANIZATI, y = CAUSE)) +
  labs(x = "Organization", y = "Cause")

  1. También puede obtener un recuento exacto del número de incendios por organización y causa
df %>%
  count(ORGANIZATI, CAUSE)
## # A tibble: 11 × 3
##    ORGANIZATI CAUSE            n
##    <chr>      <chr>        <int>
##  1 BIA        Human           49
##  2 BIA        Natural         91
##  3 BLM        Human          187
##  4 BLM        Natural        386
##  5 FS         Human          158
##  6 FS         Natural        431
##  7 FWS        Human           10
##  8 FWS        Natural          7
##  9 FWS        Undetermined     6
## 10 NPS        Human            6
## 11 NPS        Natural         46

Gráficos 2D de contenedores y hexágonos

  • También puede utilizar gráficos 2D de contenedores y hexadecimales como una forma alternativa de ver la distribución de dos variables. Siga las instrucciones que se proporcionan a continuación para crear gráficos 2D de cubos y hexágonos que visualicen la relación entre el año y la superficie total quemada
  1. Utilice la función read_csv() para cargar el conjunto de datos en un marco de datos
dfFires <- read_csv("C:/Data/StudyArea.csv", 
                    col_types = list(UNIT = col_character()),
                    col_names = TRUE)
  1. Cree un mapa 2D con YEAR_ en el eje \(x\) y TOTALACRES en el eje \(y\)
ggplot(data = dfFires) + 
  geom_bin2d(mapping = aes(x=YEAR_, y=TOTALACRES)) +
  labs(x = "Year", y = "Total Acres")

Generación de estadísticas de resumen

  • Otra técnica básica para realizar un análisis exploratorio de datos es generar varias estadísticas de resumen sobre un conjunto de datos. R incluye una serie de funciones individuales para generar estadísticas de resumen específicas o puede utilizar la función summary() para generar un conjunto de estadísticas de resumen
  1. Recargar el archivo StudyArea.csv en un marco de datos
df <- read_csv("C:/Data/StudyArea.csv", 
               col_types = list(UNIT = col_character()),
               col_names = TRUE)
  1. Restringir la lista de columnas
df <- select(df, ORGANIZATI, STATE, YEAR_, TOTALACRES, CAUSE)
  1. Filtra la lista para incluir sólo los incendios forestales de más de 1000 acres
df <- filter(df, TOTALACRES >= 1000)
  1. Llame a la función mean(), pasando una referencia al marco de datos y a la columna columna TOTALACRES.
mean(df$TOTALACRES)
## [1] 10813.06
  1. Llama a la función median()
median(df$TOTALACRES)
## [1] 3240
  1. En lugar de llamar a las funciones individuales de estadísticas de resumen, puede simplemente utilizar la función summary() para devolver una lista de estadísticas de resumen.
summary(df$TOTALACRES)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    1000    1670    3240   10813    8282  590620

Conclusiones

  • En esta sección ha aprendido algunas técnicas básicas de exploración de datos utilizando R. Ha aprendido a medir la variación categórica y continua con gráficos de barras e histogramas, y la covariación con gráficos de caja y diferentes tamaños de símbolos. Por último, finalmente, aprendió a generar estadísticas de resumen y a crear gráficos 2D y hexadecimales.

Técnicas básicas de visualización de datos

  • El paquete ggplot2 es una biblioteca que permite la creación de muchos tipos de visualización de datos, incluyendo varios tipos de gráficos y diagramas. Esta biblioteca fue creada creada por Hadley Wickham en 2005 y es una implementación en R de la Wilkinson’s Grammar of Graphics. La idea de este paquete es especificar bloques de construcción de gráficos y luego combinarlos para crear una pantalla gráfica.

  • En este capítulo cubriremos los siguientes temas:

    • Creación de un gráfico de dispersión
    • Añadir una línea de regresión a un gráfico de dispersión
    • Trazado de categorías
    • Etiquetado del gráfico
    • Diseños de leyendas
    • Creación de una faceta
    • Tematización
    • Creación de gráficos de barras
    • Creación de gráficos de violines
    • Creación de gráficos de densidad

Creación de un gráfico de dispersión

  • Un diagrama de dispersión es un gráfico en el que los valores de dos variables se trazan a lo largo de dos ejes, y el patrón de los puntos resultantes revela cualquier correlación presente
  1. Los ejercicios de este capítulo requieren los siguientes paquetes: readr, dplyr, ggplot2. Pueden cargarse desde el panel de paquetes, el panel de la consola o un script

  2. Abra RStudio y busque el panel de la consola

  3. Cargar el contenido del archivo StudyArea.csv en un marco de datos

dfWildfires <- read_csv("C:/Data/StudyArea.csv", 
                        col_types = list(UNIT = col_character()), 
                        col_names = TRUE)
  1. Crear un subconjunto de columnas
df <- select(dfWildfires, ORGANIZATI, STATE, YEAR_, TOTALACRES, CAUSE)
  1. Agrupa los registros por año
grp <- group_by(df, YEAR_)
  1. Resuma los datos según el número total de hectáreas quemadas
sm <- summarize(grp, totalacres = sum(TOTALACRES))
  1. Utilice ggplot() para crear un gráfico de dispersión con el año en el eje x y el total de acres quemados en el eje Y.
ggplot(data=sm) + geom_point(mapping = aes(x=YEAR_, y=totalacres))

  1. Hay ocasiones en las que tiene sentido utilizar las escalas logarítmicas en los cuadros y gráficos. Una de las razones es responder a la asimetría hacia los valores grandes, es decir, los casos en los que uno o unos pocos puntos son mucho más grandes que el grueso de los datos. En el gráfico que acabamos de crear hay un par de puntos que entran en esta categoría en en el eje Y. Vuelva a crear el gráfico, pero esta vez utilice la función log() en la columna totalacres
ggplot(data=sm) + geom_point(mapping = aes(x=YEAR_, y=log(totalacres)))

Añadir una línea de regresión al gráfico de dispersión

  • Los gráficos construidos con ggplot() pueden tener más de una geometría. Es habitual añadir una línea de predicción (regresión) al gráfico.
  1. Hay varias maneras de añadir una línea de regresión al gráfico de dispersión, una de ellas es utilizar la función geom_smooth() con el método establecido como lm (línea recta) y el parámetro se establecido en FALSE. Añade la línea de código que ves abajo en la ventana de la consola
ggplot(data=sm, aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() +
  geom_smooth(method=lm, se=FALSE)

  1. Cambia el método a loess el efecto en la línea de regresión
ggplot(data=sm, aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() +
  geom_smooth(method=loess, se=FALSE)

  1. Puede añadir un intervalo de confianza alrededor de la línea de regresión estableciendo se = TRUE
ggplot(data=sm, aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() +
  geom_smooth(method=loess, se=TRUE)

Trazado de categorías

  • En lugar de representar gráficamente todo el conjunto de incendios forestales, es posible que se quiera comprender mejor las tendencias por estado. En este paso creará un nuevo gráfico de dispersión que visualice las tendencias de los incendios forestales a lo largo del tiempo por estado
  1. Reagrupar el marco de datos de los incendios forestales por estado y año
grp <- group_by(df, STATE, YEAR_)
  1. Resuma los grupos por el total de hectáreas quemadas
sm <- summarize(grp, totalacres = sum(TOTALACRES))
  1. Añade un parámetro de parámetro de color a la función aes() para que los puntos y la línea de regresión se se mapeen de acuerdo con el estado en el que se produjeron
ggplot(data=sm, aes(x=YEAR_, y=totalacres, colour=STATE)) + 
  geom_point(aes(colour = STATE)) + 
  stat_smooth(method=lm, se=FALSE)

Etiquetar el gráfico

  • Puede añadir etiquetas a su gráfico mediante la función geom_text() o la función geom_ label()
  1. Etiquete cada uno de los puntos del gráfico de dispersión utilizando geom_text() con un tamaño de etiqueta de 3
ggplot(data=sm, aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() +
  geom_smooth(method=loess, se=TRUE) + 
  geom_text(aes(label=STATE), size=3)

La pantalla está extremadamente desordenada así que vamos a ajustar algunos parámetros para hacer esto más fácil de leer.

  1. Puede utilizar el parámetro check_overlap para eliminar cualquier etiqueta superpuesta. Actualice su código como se ve a continuación
ggplot(data=sm, aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() +
  geom_smooth(method=loess, se=TRUE) + 
  geom_text(aes(label=STATE), size=3, check_overlap = TRUE)

  1. Esto se ve un poco mejor, pero si se cambia el tamaño de la etiqueta a 2 se reducirá aún más reducir el desorden y la superposición, mientras que esperamos que siga siendo legible

  2. Habrás observado que las etiquetas se sitúan directamente sobre los temas. Puede utilizar los parámetros nudge_x y nudge_y para mover las etiquetas en relación con el punto. Utilice nudge_x como se ve a continuación para ver cómo esto mueve las etiquetas horizontalmente

ggplot(data=sm, aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() +
  geom_smooth(method=loess, se=TRUE) + 
  geom_text(aes(label=STATE), size=2, check_overlap = TRUE, nudge_x = 1.0)

  1. También puede colorear las etiquetas por categoría añadiendo el parámetro de color a la función aes() para geom_text()
ggplot(data=sm, aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() +
  geom_smooth(method=loess, se=TRUE) + 
  geom_text(aes(label=STATE, color=STATE), size=2, check_overlap = TRUE, nudge_x = 1.0)

  1. También puedes añadir un subtítulo y un pie de foto con el código que ves a continuación
ggplot(data=sm, aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() +
  geom_smooth(method=loess, se=TRUE) + 
  labs(title=paste("Acreage Burned by Wildfires Has Increased In the Past Few Decades"), subtitle=paste("1980-2016"), caption="Data from USGS")

  1. También puede actualizar las etiquetas X e Y del gráfico. Actualice estas etiquetas en su gráfico usando el código que ve abajo
ggplot(data=sm, aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() +
  geom_smooth(method=loess, se=TRUE) + 
  labs(title=paste("Acreage Burned by Wildfires Has Increased In the Past Few Decades"), subtitle=paste("1980-2016"), caption="Data from USGS") +
  scale_y_continuous(name="Log of Total Acres Burned") + 
  scale_x_continuous(name="Burn Year")

Diseños de leyendas

  • La función theme() puede utilizarse para controlar la ubicación de la leyenda y la función guide() puede utilizarse para proporcionar un control adicional de la leyenda
  1. La función theme() junto con el argumento legend.postion se utiliza para controlar la ubicación de la leyenda en el gráfico. Por defecto, la leyenda que hemos visto hasta ahora se ha colocado en el lado derecho del gráfico con una orientación vertical. Reposicione la leyenda en la parte inferior con el código siguiente.
ggplot(data=sm, aes(x=YEAR_, y=log(totalacres), color=STATE)) +
  geom_point() + 
  labs(title=paste("Acreage Burned by Wildfires Has Increased In the Past Few Decades"),
  subtitle=paste("1980-2016"), caption="Data from USGS") + 
  scale_y_continuous(name="Log of Total Acres Burned") +
  scale_x_continuous(name="Burn Year") + 
  theme(legend.position="bottom")

  1. También puede eliminar explícitamente una leyenda estableciendo legend.position = "none". Pruebe eso ahora si lo desea

  2. Otros aspectos de la leyenda, como el número de filas en la leyenda, así como el tamaño del símbolo pueden ser controlados a través de la función guides(). Utilice el código que se ve a continuación para actualizar la leyenda a dos filas y con cada símbolo de tamaño 4

ggplot(data=sm, aes(x=YEAR_, y=log(totalacres), color=STATE)) +
  geom_point() +
  labs(title=paste("Acreage Burned by Wildfires Has Increased In the Past Few
Decades"), subtitle=paste("1980-2016"), caption="Data from USGS") +
  scale_y_continuous(name="Log of Total Acres Burned") +
  scale_x_continuous(name="Burn Year") +
  theme(legend.position = "bottom") +
  guides(color=guide_legend(nrow=2,override.aes=list(size=4)))

Crear una faceta

  • Una forma particularmente buena de graficar variables categóricas es dividir el gráfico en facetas, que son subparcelas que muestran cada una un subconjunto de los datos. Las funciones facet_wrap() y facet_grid() pueden utilizarse para crear facetas
  1. Utilice la función facet_wrap() que se muestra en el código siguiente para crear un mapa de facetas que muestre el total de acres quemados por estado
ggplot(data=sm, mapping = aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() + facet_wrap(~STATE) + geom_smooth(method=loess, se=TRUE)

Temática

  • Existen ocho temas incorporados que pueden utilizarse para personalizar el estilo de ggplot2 de los elementos que no son datos de su gráfico.
  1. Los ocho temas incluidos en ggplot2 son theme_bw, theme_ classic, theme_dark, theme_gray, theme_light, theme_ linedraw, theme_minimal, theme_void. Añade el código que ves a continuación para cambiar la faceta a theme_dark
ggplot(data=sm, mapping = aes(x=YEAR_, y=log(totalacres))) + 
  geom_point() + facet_wrap(~STATE) + geom_smooth(method=loess, se=TRUE) +
  theme_dark()

  1. Experimenta con los temas para ver las diferencias de estilo.

Creación de gráficos de barras

  • Puede utilizar geom_bar() o geom_chart() para crear gráficos de barras con ggplot2. Sin embargo, hay una diferencia significativa entre ambos. La función geom_bar() generará un recuento del número de instancias de una variable. En otras palabras, cambia la estadística que ya se ha generado para el grupo. La función geom_col() mantiene la variable ya generada para el grupo. Para ver la diferencia, complete los siguientes pasos
  1. Cargue el archivo StudyArea.csv y obtenga un subconjunto de columnas
dfWildfires <- read_csv("C:/Data/StudyArea.csv", 
                        col_types = list(UNIT = col_character()), col_names = TRUE)
df <- select(dfWildfires, ORGANIZATI, STATE, YEAR_, TOTALACRES, CAUSE)
  1. Filtrar el marco de datos para que sólo se incluyan los incendios forestales de California
df <- filter(df, STATE == "California")
  1. Agrupar el marco de datos por YEAR_
grp <- group_by(df, YEAR_)
  1. Trace los datos utilizando geom_bar() como se ve a continuación. Observe que el gráfico de barras que se producido es un recuento del número de incendios para cada año
ggplot(data=grp) + geom_bar(mapping = aes(x=YEAR_), fill="red")

  1. Ahora utilice geom_col() para ver la diferencia. La variable TOTALACRES se mantiene en este caso
ggplot(data=grp) + geom_col(mapping = aes(x=YEAR_, y=TOTALACRES), fill="red")

Creación de tramas de violín

  • Los gráficos de violín, que son similares a los gráficos de caja, también muestran la densidad de probabilidad en varios valores. Las áreas más gruesas del gráfico de violín indican una mayor probabilidad en ese valor. Normalmente, los gráficos de violín también incluyen un marcador para la mediana junto con el rango intercuartil (IQR). La función geom_violin() se utiliza para crear gráficos violín en ggplot2
  1. Cargue el archivo StudyArea.csv y obtenga un subconjunto de columnas
dfWildfires <- read_csv("C:/Data/StudyArea.csv", 
                        col_types = list(UNIT = col_character()), col_names = TRUE)
df <- select(dfWildfires, ORGANIZATI, STATE, YEAR_, TOTALACRES, CAUSE)
  1. Filtrar el marco de datos para que sólo se incluyan los incendios forestales de más de 5.000 acres se incluyan.
dfWildfires <- filter(dfWildfires, TOTALACRES >= 5000)
  1. Agrupe los incendios forestales por organización
grpWildfires <- group_by(dfWildfires, ORGANIZATI)
  1. Crear una gráfica básica de violín.
ggplot(data=grpWildfires, mapping = aes(x=ORGANIZATI, y=log(TOTALACRES))) +
  geom_violin()

  1. Puede añadir las observaciones individuales utilizando geom_jitter(). Si necesita mantener los puntos y dispersarlos horizontalmente, puede utilizar geom_jitter(height = 0)
ggplot(data=grpWildfires, mapping = aes(x=ORGANIZATI, y=log(TOTALACRES))) +
  geom_violin() + 
  geom_jitter(height = 0, width = 0.1)

  1. La media puede añadirse utilizando stat_summary() como se ve a continuación
ggplot(data=grpWildfires, mapping = aes(x=ORGANIZATI, y=log(TOTALACRES))) +
  geom_violin() + geom_jitter(height = 0, width = 0.1) + 
  stat_summary(fun.y=median, geom="point", size=3, color="red") +
  stat_summary(fun.y=mean, geom="point", size=3, color="green")

  1. La función box_plot() puede utilizarse para añadir la mediana y el IQR
ggplot(data=grpWildfires, mapping = aes(x=ORGANIZATI, y=log(TOTALACRES))) +
  geom_violin() + geom_boxplot(width=0.2)

Creación de gráficos de densidad

  • Los gráficos de densidad, creados con geom_density() calculan una estimación de la densidad, que es una versión suavizada de un histograma y se utiliza con datos continuos. ggplot2 también puede calcular versiones 2D de la densidad, incluyendo contornos y gráficos de densidad con estilo de polígono
  1. En esta primera parte del ejercicio crearás un gráfico de densidad básico. Cargue el archivo StudyArea.csv y obtenga un subconjunto de columnas
dfWildfires <- read_csv("C:/Data/StudyArea.csv", 
                        col_types = list(UNIT = col_character()), col_names = TRUE)
df <- select(dfWildfires, ORGANIZATI, STATE, YEAR_, TOTALACRES, CAUSE)
  1. Filtrar el marco de datos para que sólo se incluyan los incendios forestales de más de 1.000 acres
dfWildfires <- filter(dfWildfires, TOTALACRES >= 1000)
  1. Cree un gráfico de densidad con la función geom_density()
ggplot(dfWildfires, aes(TOTALACRES)) + geom_density()

  1. También puede crear el mismo gráfico de densidad con una versión registrada de los datos
ggplot(dfWildfires, aes(log(TOTALACRES))) + geom_density()

  1. A continuación, crearás gráficos 2D de los datos empezando por los contornos. Añade el código que ves a continuación
ggplot(dfWildfires, aes(x=YEAR_, y=log(TOTALACRES))) + 
  geom_point() +
  geom_density_2d()

  1. Por último, cree una superficie de densidad 2D utilizando stat_density_2d()
ggplot(dfWildfires, aes(x=YEAR_, y=log(TOTALACRES))) +
  geom_density_2d() + 
  stat_density_2d(geom="raster", aes(fill=..density..), contour=FALSE)

Conclusiones

  • En este capítulo has aprendido varias técnicas de visualización de datos utilizando ggplot2. Comenzamos con gráficos de dispersión básicos, añadimos líneas de regresión, etiquetamos los gráficos de varias formas y creamos una leyenda. Además, se aprendió a crear gráficos de facetas y a trabajar con ggplot2 y las opciones de tematización. También aprendió a crear gráficos de barras, gráficos de violín y gráficos de densidad.

Ejercicio para entregar

  • Considere los datos asociados a precios de casas house_prices.csv que aparece en el directorio RDataSets.zip en Github. Nótese que el archivo posee su respectiva descripción house_prices_description.txt que puede ser de gran utilidad. Realice un análisis exploratorio de los datos relacionados con los precios de casas, teniendo en cuenta cada ítem estudiado en esta sección. Debería agregar todas las visualizaciones extras, que sean necesarias y le permitan generar y refinar preguntas sobre los datos.

  • Considere los datos asociados a precios de casas house_prices.csv que aparece en el directorio RDataSets.zip en Github. A manera de aplicación, utilice las técnicas básicas de visualización de datos estudiadas en cada ítem de esta sección, ahora aplicadas al conjunto de datos relacionado con la predicción del precio de casas, el cual puede ser influenciado por los distintos factores, presentados en las columnas del archivo house_prices.csv, cuyas descripciones puede encontrar en el archivo house_prices_description.txt. Puede agregar visualizaciones extras, que le permitan generar y refinar preguntas sobre los datos.

Visualización de datos geográficos con ggmap

  • El paquete ggmap permite visualizar datos espaciales y estadísticas espaciales en formato de mapa utilizando el enfoque por capas de ggplot2. Este paquete también incluye mapas base que dan contexto a sus visualizaciones, incluyendo Google Maps, Open Street Map, Stamen Maps y CloudMade. Además, se proporcionan funciones para acceder a varios servicios de Google, como Geocoding, Distance Matrix, and Directions.

  • El paquete ggmap se basa en ggplot2, lo que significa que adoptará un enfoque por capas y constará de los mismos cinco componentes que se encuentran en ggplot2. Estos incluyen un conjunto de datos por defecto con mapeos estéticos donde \(x\) es longitud, \(y\) es latitud, y el sistema de coordenadas se fija en Mercator. Otros componentes incluyen una o más capas definidas con un objeto geométrico y una transformación para cada mapa estético, el sistema de coordenadas y la especificación de las facetas. Debido a que ggmap está construido sobre ggplot2 tiene acceso a toda la gama de ggplot2 ya aprendidas.

  • En esta sección cubriremos los siguientes temas:

    • Creación de un mapa base
    • Añadir capas operativas
    • Añadir capas desde un shapefile

Creación de un mapa base

  • Hay dos pasos básicos para crear un mapa con ggmap. En general sólo hay que descargar el mapa rasterizado (basemap) y luego trazar los datos operativos en el mapa base. El primer paso se consigue con la función get_map(), que puede utilizarse para crear un mapa base desde Google, Stamen, Open Street Map o CloudMade. Aprenderás cómo hacerlo en este paso. En un próximo paso aprenderás a añadir y estilizar los datos operativos de varias maneras
  1. Cargue el paquete ggmap yendo al panel de paquetes en RStudio y haciendo clic en la casilla junto al nombre del paquete. También puede cargarlo desde la consola escribiendo
library(ggmap)
  1. Crea una variable llamada myLocation y defínela como California, ubicación que usaremos más adelante
myLocation <- "California"
  1. Llama a la función get_map() y pasa la variable de localización junto con un nivel de Zoom de 6. Necesitaras primero crear una API key en Google Cloud Platform. Si descargas la versión de desarrollo de ggmap desde GitHub, hay una función que guarda tu clave de la API de Google y la utiliza en las siguientes llamadas a la API a través de ggmap. Para instalarla desde la consola escribir
devtools::install_github("dkahle/ggmap")
  1. Utilice las siguientes instrucciones para descargar el mapa. Cargue primero su API key y luego use la función get_googlemap para obtener el mapa de interés. Para que las siguientes instrucciones funcionen, debe realizar los siguientes pasos en Google Cloud Platform
  • Debe registrarse en Google Cloud Platform. Desafortunadamente para el registro deberá usar una Tarjeta de credito. Obtendrá $300 gratis para usarlos por 90 días. Luego de esto se cargarán a su cuenta $5 mensuales, dependiendo del uso que haga de la API. Recomiendo usar una tarjeta crédito prepago como por ejemplo Daviplata o similiares

  • Debe habilitar los siguientes servicios en la Biblioteca

    • Maps Static API
    • Geocoding API
    • Geolocation API
    • Maps Embed API
  • Para finalizar genere una API restringida a cada unos de los servicios de localización habilitados anteriormente. Las siguientes imágenes muestran como quedaría la configuración requerida

  • Para obtener mas información sobre el uso de la función get_googlemap() ver ggmap. Por favor, no use la API key mykey = "AIzaSyABAXSsDR-f9qby3LK2o6bh0BypFWlKrm4", pues está será cambiada por otra, en este caso es usada a manera de ejemplo. Cree su propia API Google Cloud Platform
library(ggmap)
mykey = "AIzaSyABAXSsDR-f9qby3LK2o6bh0BypFWlKrm4"
register_google(key = mykey)

get_googlemap("waco, texas") %>% ggmap()

  1. Lo siguientes tipos de mapas también están disponibles para vista satelital en los mapas. Para ver más opciones visitar la documentación de get_googlemap
get_googlemap("Colombia", 
              maptype = "hybrid",
              scale = 2,
              zoom = 5) %>% ggmap()

get_googlemap("Universidad del Norte", 
              maptype = "roadmap",
              scale = 2,
              zoom = 12) %>% ggmap()

get_googlemap("Universidad del Norte", 
              maptype = "terrain",
              scale = 2,
              zoom = 16) %>% ggmap()

get_googlemap("Universidad del Norte", 
              maptype = "hybrid",
              scale = 2,
              zoom = 18) %>% ggmap()

Añadir capas de datos operativos

  • ggmap() devuelve un objeto ggplot, lo que significa que actúa como capa base en el ggplot2. Esto permite la gama completa de capacidades de ggplot2 lo que significa que puede trazar puntos en el mapa, añadir contornos, mapas de calor 2D y mucho más. Examinaremos algunas de estas capacidades en esta sección.
  1. El siguiente código es usado para producir un mapa de California que muestre los incendios forestales entre los años 1980-2016 que quemaron más de 1,000 hectáreas.
myLocation <- "California"
myMap <- get_map(location = myLocation, zoom = 6)
library(readr)
dfWildfires <- read_csv("C:/Data/StudyArea_SmallFile.csv", col_names = TRUE)
library(dplyr)
df <- select(dfWildfires, STATE, YEAR_, TOTALACRES, DLATITUDE, DLONGITUDE)
knitr::kable(head(df))
STATE YEAR_ TOTALACRES DLATITUDE DLONGITUDE
Arizona 1988 1500 31.58333 -111.5500
Arizona 1986 10390 32.50000 -111.5167
Montana 1986 1400 47.50000 -111.4333
Arizona 2002 1035 31.70000 -111.4830
Arizona 2000 5700 31.51600 -111.5170
Arizona 2000 2750 31.64900 -111.4910
df <- filter(df, TOTALACRES >= 1000 & STATE == "California")
ggmap(myMap) + geom_point(data=df, aes(x = DLONGITUDE, y = DLATITUDE))

  1. Ahora vamos a hacer algo un poco más interesante. En primer lugar, utilizaremos la función de dplyr mutate() para agrupar los incendios por década
df <- mutate(df, 
             DECADE = ifelse(YEAR_ %in% 1980:1989, "1980-1989", 
                      ifelse(YEAR_ %in% 1990:1999, "1990-1999", 
                      ifelse(YEAR_ %in% 2000:2009, "2000-2009", 
                      ifelse(YEAR_ %in% 2010:2016, "2010-2016", "-99")))))
  1. A continuación, codifica por colores los incendios forestales por DECADE y crea un mapa de símbolos graduados en función del tamaño de cada incendio. La propiedad de color define la columna a utilizar para la agrupación, y la propiedad de tamaño define la columna a utilizar para el tamaño de cada símbolo.
  • Nótese que además es posible hacer dinámico el mapa usando ggplotly. Para esto utilizamos la función ggplot_build para convertir el plot de ggmap a un objeto ggplot.
last_plot <- ggmap(myMap) + 
  geom_point(data=df, aes(x = DLONGITUDE, y = DLATITUDE, colour= DECADE, size = TOTALACRES))
library(plotly)

gg <- ggplot_build(last_plot)$plot
plotly_plot <- ggplotly(gg)
plotly_plot
  1. Vamos a cambiar la vista del mapa para centrarnos más en el sur de California, y en en particular el área al norte de Los Ángeles.
myMap <- get_map(location = "Santa Clarita, California", zoom = 10)

ggmap(myMap) + 
  geom_point(data=df, 
             aes(x = DLONGITUDE, y = DLATITUDE, colour= DECADE, size = TOTALACRES))

  1. A continuación, añadiremos capas de contorno y de calor. La función geom_density2d() se utiliza para crear los contornos mientras que la función stat_density2d() crea el mapa de calor. Puedes experimentar con los colores usando las propiedades scale_fill_gradient(low and high).
  • Aquí los hemos establecido en verde y rojo respectivamente, pero puede cambiar el esquema de colores según su preferencia. En este ejemplo, la sintaxis fill = ..level.. se utiliza para rellenar los contornos en el gráfico basado en los niveles de densidad de los puntos de datos.
myMap <- get_map(location = "California", zoom = 6)
ggmap(myMap, extent = "device") + 
  geom_density2d(data = df, aes(x =DLONGITUDE, y = DLATITUDE), size = 0.3) +
  stat_density2d(data = df, 
                 aes(x = DLONGITUDE, y = DLATITUDE, fill = ..level.., 
                     alpha =..level..), 
                 size = 0.01, bins = 16, geom = "polygon") + 
  scale_fill_gradient(low = "green", high ="red") + 
  scale_alpha(range = c(0, 0.3), guide = FALSE)

  1. Si prefiere ver el mapa de calor sin contornos, el código puede simplificarse como sigue
ggmap(myMap, extent = "device") + 
  stat_density2d(data = df, 
                 aes(x =DLONGITUDE, y = DLATITUDE, fill = ..level.., 
                     alpha = ..level..), 
                 size = 0.01,bins = 16, geom = "polygon") + 
  scale_fill_gradient(low = "green", high ="red") + 
  scale_alpha(range = c(0, 0.3), guide = FALSE)

  1. Por último, vamos a crear un mapa de facetas que represente los puntos críticos de cada año de la década actual. El conjunto de datos contiene información hasta el año 2016. facet_wrap() es una función útil en ggplot2 para crear múltiples mapas mostrados juntos en un diseño de cuadrícula, cada uno representando un subconjunto de los datos.
df <- filter(df, YEAR_ %in% c(2010, 2011, 2012, 2013, 2014, 2015, 2016))

gg <- ggmap(myMap, extent = "device") + 
  stat_density2d(data = df, aes(x = DLONGITUDE, y = DLATITUDE, fill = ..level.., alpha = ..level..),
                 size = 0.01, bins = 16, geom = "polygon") +
  scale_fill_gradient(low = "green", high ="red") + 
  scale_alpha(range = c(0, 0.3), guide = FALSE) + 
  facet_wrap(~YEAR_, ncol = 4)
  theme(plot.title = element_text(size = 20)) +
  theme(plot.margin = margin(1, 1, 1, 1, "cm"))
## List of 2
##  $ plot.title :List of 11
##   ..$ family       : NULL
##   ..$ face         : NULL
##   ..$ colour       : NULL
##   ..$ size         : num 20
##   ..$ hjust        : NULL
##   ..$ vjust        : NULL
##   ..$ angle        : NULL
##   ..$ lineheight   : NULL
##   ..$ margin       : NULL
##   ..$ debug        : NULL
##   ..$ inherit.blank: logi FALSE
##   ..- attr(*, "class")= chr [1:2] "element_text" "element"
##  $ plot.margin: 'margin' num [1:4] 1cm 1cm 1cm 1cm
##   ..- attr(*, "unit")= int 1
##  - attr(*, "class")= chr [1:2] "theme" "gg"
##  - attr(*, "complete")= logi FALSE
##  - attr(*, "validate")= logi TRUE
print(gg)

Añadir capas a partir de Shapefiles

  • Aunque se trata de un formato de datos SIG algo más antiguo, los shapefiles siguen siendo utilizados comúnmente para representar características geográficas. Con un poco de manipulación, puede obtener datos de trazado de shapefiles en ggmap.
  1. Para este ejercicio necesitarás instalar un paquete adicional llamado rgdal. Utilice el panel de paquetes para encontrar e instalar rgdal o introduzca el código que ve a continuación
install.packages("rgdal")
  1. Cargue el paquete rgdal a través del panel de paquetes o introduzca el código que ve a continuación
library(rgdal)
  1. La carpeta RDataSets que contiene los datos del ejercicio para este curso, contiene un archivo shape llamado S_USA.Wilderness.
  • Estos archivos se combinan para crear lo que se llama un shapefile. Este archivo contiene los límites de las áreas silvestres designadas en los Estados Unidos. Utilice la función readOGR() de rgdal para cargar los datos en una variable.
wild <- readOGR("C:/Data", "S_USA.Wilderness")
## OGR data source with driver: ESRI Shapefile 
## Source: "C:\Data", layer: "S_USA.Wilderness"
## with 447 features
## It has 8 fields
## Integer64 fields read as strings:  WID
  1. La función fortify(), que forma parte de ggplot2, convierte todos los puntos individuales que definen cada límite en un marco de datos que se puede utilizar para trazar los límites del polígono.
wild <- fortify(wild)
  1. Utilice la función ggmap qmap() (qmap significa mapa rápido) para crear el mapa base que se utilizará como referencia para los límites de los espacios naturales. Centra el mapa en Montana.
montana <- qmap("Montana", zoom=6)
  1. Antes de trazar los límites de los espacios naturales como polígonos en el mapa, observe el marco de datos creado por la función fortify() para entender mejor la estructura creada por esta función
knitr::kable(head(wild)) 
long lat order hole piece id group
-70.94738 44.27397 1 FALSE 1 0 0.1
-70.94757 44.27229 2 FALSE 1 0 0.1
-70.94924 44.27145 3 FALSE 1 0 0.1
-70.95507 44.27034 4 FALSE 1 0 0.1
-70.95729 44.26868 5 FALSE 1 0 0.1
-70.95862 44.26679 6 FALSE 1 0 0.1
  • Fíjese en la columna group. Esta columna identifica de forma exclusiva límites de los espacios naturales. Los límites de los espacios naturales son polígonos, y los polígonos están definidos por un conjunto de puntos que definen la estructura del polígono. Es algo así como como jugar a conectar los puntos, donde cada punto es un par de coordenadas de latitud/longitud definidas por las columnas long y lat en el marco de datos.

  • Por ejemplo, mire el grupo 0.1. Observe que hay varias filas que contiene el valor 0.1, y que cada fila tiene valores únicos de longitud y lat. Estos son todos los puntos utilizados para definir los límites de ese polígono.

  1. Ahora traza los límites de los espacios naturales en el mapa base. Observe el uso de la columna para agrupar los polígonos. El trazado de los límites en el mapa lleva algo de tiempo, así que sea paciente con este paso. Al final debería ver un mapa similar a la captura de pantalla de abajo.
montana + 
  geom_polygon(aes(x=long,y=lat, group=group, alpha=0.25), data=wild, fill="white") + 
  geom_polygon(aes(x=long, y=lat, group=group), data=wild, color="black", fill=NA)

Conclusión

  • En este capítulo ha aprendido a utilizar el paquete ggmap para crear atractivas visualizaciones de datos en formato de mapa.

  • Ha aprendido a *crear mapas base utilizando Google como fuente de datos, añadir capas de datos operativos, crear varios tipos de visualizaciones de mapas utilizando fuentes de datos externas, y cargar archivos de forma.

Ejercicio para entregar

  • Considere el conjunto de datos violent_crimes.csv en RDataSets.zip el cual abarca una gran variedad de delitos, y realice visualizaciones geográficas para cada tipo de crimen.

  • Considere cada ítem estudiado en esta sección para realizar las visualizaciones respectivas con este dataset.

  • Verifique si existe algún patrón dentro de los delitos violentos que pueda explorarse visualmente. Los datos incluyen dentro de sus columnas la longitud y latitud, la categoría en la que se clasifica el delito, la fecha y hora.

Análisis con datos faltantes

Introducción

  • Los datos faltantes es uno de los temas que se ignoran en la mayoría de los textos introductorios. Probablemente, parte de la razón por la que esto es así es que todavía abundan muchos mitos sobre el análisis con datos faltantes. Además, algunas de las investigaciones sobre técnicas de vanguardia es aún relativamente nueva. Una razón más legítima para su ausencia en los textos introductorios es que la mayoría de las metodologías son bastante complicadas, desde el punto de vista matemático. Sin embargo, la increíble ubicuidad de los problemas relacionados con los datos faltantes en el análisis de datos de la vida real requiere que se aborde el tema.

  • Esta sección sirve como una suave introducción al tema y a una de las técnicas más eficaces para tratarla. Un refrán común sobre el tema es algo así como que, la mejor manera de tratar con los datos que faltan es no tenerlos. Es cierto que los datos que faltan son un tema complicado, y hay muchas maneras de hacerlo, de las cuales algunas pueden estar mal. Es importante no llevar este consejo al extremo, sin embargo, para eludir los problemas de los datos faltantes, algunos han impedido participantes en una encuesta, que por ejemplo, siguen sin responder a todas las preguntas de un formulario.

  • Hay tratamientos para los datos que faltan, pero no hay tratamientos para los datos malos o incorrectos. El tratamiento estándar para el problema de los datos que faltan es reemplazar los datos que faltan por valores no ausentes. Este proceso se denomina imputación. En la mayoría de los casos, el objetivo de la imputación no es recrear el conjunto de datos faltantes completo, sino permitir que se realicen estimaciones o inferencias estadísticas válidas a partir de los datos faltantes.

  • Por ello, la eficacia de las diferentes técnicas de imputación no puede evaluarse por su capacidad de recrear los datos con la mayor exactitud posible a partir de un conjunto de datos faltantes simulado, sino que deben juzgarse por su capacidad de apoyar las mismas inferencias estadísticas que se obtendrían del análisis de los datos completos que se extraen del análisis.

  • De este modo, rellenar los datos que faltan es sólo un paso hacia el verdadero objetivo: el análisis. El conjunto de datos imputados rara vez se considera el objetivo final de la imputación. En la práctica, hay muchas formas diferentes de tratar los datos que faltan, algunas son buenas y otras no tanto. Algunas están bien en determinadas circunstancias, pero no en otras. Algunas implican la eliminación de datos faltantes, mientras que otras implican la imputación. Vamos a mencionar brevemente algunos de los métodos más comunes. Sin embargo, el objetivo final de esta sección, es iniciarle en lo que a menudo se describe como el estándar de oro de las técnicas de imputación: la imputación múltiple.

Visualización de los datos que faltan

  • Para demostrar la visualización de patrones de datos ausentes, primero tenemos que crear algunos datos ausentes. Para mostrar cómo utilizar la imputación múltiple en un escenario semirealista, vamos a crear una versión del conjunto de datos mtcars con algunos valores faltantes. Configuramos seed (para la aleatoriedad determinista), y creamos una variable para mantener nuestro nuevo conjunto de datos.

  • Los datos se extrajeron de la revista Motor Trend US de 1974 y comprenden el consumo de combustible y 10 aspectos del diseño y el rendimiento de 32 automóviles (modelos de 1973 a 1974) (ver MTCARS).

set.seed(2)
miss_mtcars <- mtcars
  • En primer lugar, vamos a crear siete valores faltantes en drat (alrededor del 20 por ciento), cinco valores faltantes en la columna mpg (alrededor del 15 por ciento), cinco valores faltantes en la columna cyl, tres valores faltantes en wt (alrededor del 10 por ciento), y tres valores faltantes en vs
some_rows <- sample(1:nrow(miss_mtcars), 7)
miss_mtcars$drat[some_rows] <- NA

some_rows <- sample(1:nrow(miss_mtcars), 5)
miss_mtcars$mpg[some_rows] <- NA

some_rows <- sample(1:nrow(miss_mtcars), 5)
miss_mtcars$cyl[some_rows] <- NA

some_rows <- sample(1:nrow(miss_mtcars), 3)
miss_mtcars$wt[some_rows] <- NA

some_rows <- sample(1:nrow(miss_mtcars), 3)
miss_mtcars$vs[some_rows] <- NA
  • Ahora, vamos a crear cuatro valores faltantes en qsec, pero sólo para los coches automáticos
only_automatic <- which(miss_mtcars$am==0)
some_rows <- sample(only_automatic, 4)
miss_mtcars$qsec[some_rows] <- NA
  • Ahora, observemos las primeras filas del nuevo conjunto de datos:
head(miss_mtcars)
##                    mpg cyl disp  hp drat    wt  qsec vs am gear carb
## Mazda RX4         21.0  NA  160 110 3.90 2.620 16.46 NA  1    4    4
## Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
## Datsun 710        22.8  NA  108  93 3.85 2.320 18.61  1  1    4    1
## Hornet 4 Drive    21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
## Hornet Sportabout 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
## Valiant           18.1   6  225 105   NA 3.460 20.22  1  0    3    1
str(miss_mtcars)
## 'data.frame':    32 obs. of  11 variables:
##  $ mpg : num  21 21 22.8 21.4 18.7 18.1 14.3 24.4 NA 19.2 ...
##  $ cyl : num  NA 6 NA 6 8 6 8 4 4 6 ...
##  $ disp: num  160 160 108 258 360 ...
##  $ hp  : num  110 110 93 110 175 105 245 62 95 123 ...
##  $ drat: num  3.9 3.9 3.85 3.08 3.15 NA 3.21 NA 3.92 3.92 ...
##  $ wt  : num  2.62 2.88 2.32 3.21 3.44 ...
##  $ qsec: num  16.5 17 18.6 19.4 17 ...
##  $ vs  : num  NA 0 1 1 0 1 NA 1 1 1 ...
##  $ am  : num  1 1 1 0 0 0 0 0 0 0 ...
##  $ gear: num  4 4 4 3 3 3 3 4 4 4 ...
##  $ carb: num  4 4 1 1 2 1 4 2 2 4 ...
  • Ahora vamos a visualizar los datos faltantes. La primera forma en que vamos a visualizar el patrón de los datos faltantes es utilizando la función md.pattern del paquete mice (que es también el paquete que vamos a utilizar para imputar nuestros datos faltantes). Si no tiene el paquete instálelo antes
library(mice)
md.pattern(miss_mtcars, plot = TRUE, rotate.names = TRUE)

##    disp hp am gear carb wt vs qsec mpg cyl drat   
## 12    1  1  1    1    1  1  1    1   1   1    1  0
## 6     1  1  1    1    1  1  1    1   1   1    0  1
## 2     1  1  1    1    1  1  1    1   1   0    1  1
## 3     1  1  1    1    1  1  1    1   0   1    1  1
## 1     1  1  1    1    1  1  1    1   0   0    1  2
## 1     1  1  1    1    1  1  1    0   1   1    1  1
## 1     1  1  1    1    1  1  1    0   0   1    1  2
## 1     1  1  1    1    1  1  0    1   1   0    1  2
## 1     1  1  1    1    1  1  0    0   1   1    1  2
## 1     1  1  1    1    1  1  0    0   1   0    1  3
## 2     1  1  1    1    1  0  1    1   1   1    1  1
## 1     1  1  1    1    1  0  1    1   1   1    0  2
##       0  0  0    0    0  3  3    4   5   5    7 27
  • Un patrón de datos faltantes por fila se refiere a las columnas que faltan en cada fila. Esta función agrega y contabiliza el número de filas con el mismo patrón de datos faltantes. Esta función produce una matriz binaria (0 y 1). Las celdas con un 1 representan datos no faltantes; los 0 representan datos que faltan. Como las filas están ordenadas en un orden creciente de ausencia, la primera fila siempre se refiere al patrón de datos ausentes que contiene la menor cantidad de datos faltantes.

  • La columna de la izquierda es un conteo del número de filas que muestran el patrón de datos faltantes, y la columna más a la derecha es un recuento del número de puntos faltantes en ese patrón. La última fila contiene un recuento del número de puntos faltantes en cada columna. Como puede ver, 12 de las filas no contienen datos faltantes.

  • Sólo hay seis filas que contienen más de un valor perdido. Sólo una de estas filas contiene más de dos valores faltantes (como se muestra en la penúltima fila). En cuanto a los conjuntos de datos con datos faltantes, este en particular no contiene mucho. No es raro que en algunos conjuntos de datos falte más del 30% de los datos. Este conjunto de datos no llega ni al 3%. Ahora vamos a visualizar el patrón de datos faltantes gráficamente utilizando el paquete VIM, el cual debe instalar en caso de no contar con este.

library(VIM)
aggr(miss_mtcars, numbers=TRUE)

  • A simple vista, esta representación nos muestra, sin esfuerzo, que la columna drat representa la mayor proporción de datos faltantes, por columnas, seguida de mpg, cyl, qsec, vs y wt. El gráfico de la derecha nos muestra información similar a la de la salida de md.pattern. Esta representación, sin embargo, hace que sea más fácil saber si hay algún patrón sistemático de omisión. Las celdas azules representan los datos no ausentes, y las rojas representan los datos que faltan. Los números de la derecha del gráfico representan la proporción de filas que muestran ese patrón de datos faltantes. El 37.5% de las filas no contienen ningún tipo de dato faltante.

Marginplots

  • Parallel boxplots: El método VIM permite comparar las distribuciones condicionales de una variable continua según un conjunto de variables, con valores recodificados como faltantes o no faltantes, mediante múltiples diagramas de caja paralelos.

  • Estos diagramas son útiles para explorar si una variable continua explica la distribución de valores faltantes en otra variable. En la siguiente Figura se muestra un ejemplo con la variable age en los datos de EU-SILC User Guide.

  • Además del diagrama de caja estándar, se muestran diagramas de caja agrupados por valores observados (gris claro) y faltantes (gris oscuro) en los diferentes componentes de ingresos. El ancho de las cajas puede ser proporcional al tamaño del grupo o de tamaño igual. Sin embargo, en este ejemplo no es posible utilizar la primera opción porque la proporción de valores faltantes es cercana a 0 para algunos componentes de ingresos.

  • En muchos componentes, la presencia de valores faltantes claramente depende de la magnitud de los valores de la edad, por ejemplo, los valores faltantes en la variable py090n (beneficios por desempleo) ocurren predominantemente para individuos más jóvenes. Esto indica situaciones MAR para los valores faltantes en estas variables, lo cual es información útil para los especialistas en la materia.

  • Scatterplots: La implementación en VIM también incluye boxplots para datos disponibles y faltantes en los márgenes del gráfico. Las frecuencias de valores faltantes en una o ambas variables se representan mediante números en la esquina inferior izquierda. Este gráfico será denominado en adelante marginplot.

marginplot(miss_mtcars[c(1,2)])

  • El diagrama de caja rojo de la izquierda muestra la distribución del número de cilindros cyl, agrupados por datos faltantes para mpg, mientras que el diagrama de caja azul muestra esta distribución para datos observados de mpg. Lo mismo ocurre con los gráficos de caja de cyl en la parte inferior del gráfico. Si los datos tienen mecanismo MCAR se espera que los gráficos de caja rojos y azules sean muy similares. Para mas información ver el artículo asociado Exploring incomplete data using visualization techniques.

Tipos de datos faltantes

  • El paquete VIM nos permitió visualizar los patrones de datos faltantes. Un término relacionado, al mecanismo de datos faltantes, describe el proceso que determina la probabilidad de que cada punto de datos sea faltante. Hay tres categorías principales de mecanismos de datos faltantes: Missing Completely At Random (MCAR), Missing At Random (MAR), y Missing Not At Random (MNAR). La discriminación basada en el mecanismo de datos faltantes es crucial, ya que nos informa sobre las opciones para manejar los datos faltantes.

    • Faltantes completamente al azar (MCAR)

      • El primer mecanismo, MCAR, se produce cuando la falta de datos no está relacionada con la datos. Esto ocurriría, por ejemplo, si se borraran filas de una base de datos al azar, o si una ráfaga de viento se llevara una muestra aleatoria de los formularios de encuesta de un investigador. El mecanismo que rige la ausencia de drat, mpg, cyl, wt, y vs es MCAR, porque seleccionamos al azar los elementos que faltan. Este mecanismo, aunque es el más fácil de trabajar, rara vez es sostenible en la práctica.
    • Faltantes no al azar (MNAR)

      • El mecanismo MNAR se produce cuando la ausencia de una variable está relacionada con la variable en sí misma. Por ejemplo, supongamos que la báscula que pesa cada coche tiene una capacidad de sólo 3.700 libras y, por ello, los ocho coches que pesaban más se registraron como NA. Este es un ejemplo clásico del mecanismo MNAR, es el propio peso de la observación la causa de que falte.
      • Otro ejemplo sería si durante el transcurso del ensayo de un fármaco antidepresivo los participantes a los que no les ayudaba el fármaco se deprimían demasiado para continuar con el ensayo. Al final del ensayo, cuando se accede y se registra el nivel de depresión de todos los participantes, habrá valores que falten para los participantes cuyo motivo de ausencia está relacionado con su nivel de depresión.
    • Faltantes al azar (MAR)

      • El mecanismo, faltante al azar, tiene un nombre un tanto desafortunado, al contrario de lo que pueda parecer, significa que existe una relación sistemática entre la ausencia de una variable resultado y otras variables observadas, pero no la variable de resultado en sí misma.

      • Un ejemplo es que los hombres son menos propensos a completar una encuesta sobre depresión, pero esto no tiene nada que ver con su nivel de depresión, después de tener en cuenta la masculinidad.

      • Por ejemplo, puede que todas las observaciones de nuestro conjunto de datos con un valor ausente de fiebre no se hayan registrado porque se asumió que todos los pacientes con escalofríos y dolores tenían fiebre, por lo que nunca se les tomó la temperatura. Si es cierto, podríamos predecir fácilmente que cada observación que falta con escalofríos y dolores también tiene fiebre y utilizar esta información para imputar nuestros datos que faltan.

  • Observación

    • Quizá haya observado que el lugar que ocupa un conjunto de datos concreto en la taxonomía del mecanismo de datos faltantes depende de las variables que incluye. Por ejemplo, sabemos que el mecanismo detrás de qsec es MAR, pero si el conjunto de datos no incluyera am, sería MNAR. Como somos nosotros los que creamos los datos, sabemos el procedimiento que dio lugar a los valores faltantes de qsec. Si no fuéramos nosotros los que creamos los datos, como ocurre en el mundo real, y el conjunto de datos no contuviera la columna am simplemente veríamos una cantidad de valores qsec que faltan arbitrariamente.
    • Esto podría llevarnos a creer que los datos son MCAR. Sin embargo, no lo es; sólo porque la variable con la cual otra variable faltante está sistemáticamente relacionada no se observa, no quiere decir que esta no exista. Esto plantea una cuestión crítica: ¿Podemos estar seguros de que nuestros datos no son MCAR? La respuesta desafortunada es no. Dado que los datos que necesitamos para demostrar o refutar el MNAR faltan ipso facto, la suposición de MNAR nunca puede ser desconfirmada de forma concluyente. Es nuestro trabajo, como analistas de datos con pensamiento crítico, preguntar si es probable que haya un mecanismo MNAR o no.

Visualización de datos faltantes

  • Tomando el conjunto de datos airquality, un conjunto de datos de mediciones diarias de la calidad del aire en Nueva York de mayo a septiembre de 1973, que tiene valores NA dentro de sus variables (ver airquality). Las filas del conjunto de datos representan 154 días consecutivos. Cualquier eliminación de estas filas afectará a la continuidad del tiempo, lo que puede afectar a cualquier análisis de series temporales que se realice. Veamos con más detalle el conjunto de datos de la calidad del aire

  • Iniciamos visualizando los datos faltantes. Eliminaremos algunos puntos de datos del conjunto de datos para este ejemplo. En lo que respecta a las variables categóricas, la sustitución de las mismas no suele ser aconsejable. Algunas prácticas comunes incluyen la sustitución de las variables categóricas que faltan por la moda de las observadas, sin embargo, es cuestionable si es una buena opción. Aunque en este caso no faltan puntos de datos de las variables categóricas, las eliminamos de nuestro conjunto de datos (podemos volver a añadirlas más tarde si es necesario) y echamos un vistazo a los datos utilizando summary()

data <- airquality
knitr::kable(head(data))
Ozone Solar.R Wind Temp Month Day
41 190 7.4 67 5 1
36 118 8.0 72 5 2
12 149 12.6 74 5 3
18 313 11.5 62 5 4
NA NA 14.3 56 5 5
28 NA 14.9 66 5 6
# data <- airquality
data[4:10, 3] <- rep(NA, 7)
data[1:5, 4] <- NA
data <- data[-c(5,6)]
summary(data)
##      Ozone           Solar.R           Wind             Temp      
##  Min.   :  1.00   Min.   :  7.0   Min.   : 1.700   Min.   :57.00  
##  1st Qu.: 18.00   1st Qu.:115.8   1st Qu.: 7.400   1st Qu.:73.00  
##  Median : 31.50   Median :205.0   Median : 9.700   Median :79.00  
##  Mean   : 42.13   Mean   :185.9   Mean   : 9.806   Mean   :78.28  
##  3rd Qu.: 63.25   3rd Qu.:258.8   3rd Qu.:11.500   3rd Qu.:85.00  
##  Max.   :168.00   Max.   :334.0   Max.   :20.700   Max.   :97.00  
##  NA's   :37       NA's   :7       NA's   :7        NA's   :5
knitr::kable(head(data))
Ozone Solar.R Wind Temp
41 190 7.4 NA
36 118 8.0 NA
12 149 12.6 NA
18 313 NA NA
NA NA NA NA
28 NA NA 66
  • Nótese que, Ozone es la variable con más puntos de datos faltantes. A continuación vamos a profundizar en los patrones de datos que faltan. Suponiendo que los datos sean MCAR, demasiados datos faltantes también pueden ser un problema. Por lo general, un umbral máximo seguro es el 5% del total para conjuntos de datos grandes. Si los datos que faltan para una determinada característica o muestra son superiores al 5%, probablemente deba dejar de lado esa característica o muestra. Por lo tanto, comprobamos las características (columnas) y las muestras (filas) en las que falta más del 5% de los datos mediante una sencilla función
pMiss <- function(x){sum(is.na(x))/length(x)*100}
apply(data,2,pMiss)
##     Ozone   Solar.R      Wind      Temp 
## 24.183007  4.575163  4.575163  3.267974
  • Vemos que al ozono le falta casi el 25% de los puntos de datos, por lo que podríamos considerar la posibilidad de eliminarlo del análisis o de reunir más mediciones. Las demás variables están por debajo del umbral del 5%, por lo que podemos mantenerlas.

  • Usemos md.pattern() para conocer mejor el patrón de los datos que faltan

library(mice)
md.pattern(data, rotate.names = TRUE)

##     Temp Solar.R Wind Ozone   
## 104    1       1    1     1  0
## 34     1       1    1     0  1
## 3      1       1    0     1  1
## 1      1       1    0     0  2
## 4      1       0    1     1  1
## 1      1       0    1     0  2
## 1      1       0    0     1  2
## 3      0       1    1     1  1
## 1      0       1    0     1  2
## 1      0       0    0     0  4
##        5       7    7    37 56
  • La salida nos dice que 104 muestras están completas, que a 34 muestras les falta sólo la medición de Ozone, que a 4 muestras les falta sólo el valor de Solar.R y así sucesivamente. Una representación visual quizás más útil puede obtenerse utilizando el paquete VIM de la siguiente manera
library(VIM)

aggr_plot <- aggr(data, col=c('navyblue','red'), numbers=TRUE, sortVars=TRUE, labels=names(data),
                  cex.axis=.7, gap=3, ylab=c("Histogram of missing data","Pattern"))

## 
##  Variables sorted by number of missings: 
##  Variable      Count
##     Ozone 0.24183007
##   Solar.R 0.04575163
##      Wind 0.04575163
##      Temp 0.03267974
  • El gráfico nos ayuda a entender que a casi el 70% de las muestras no les falta ninguna información, al 22% le falta el valor de Ozono y las restantes muestran otros patrones de ausencia. A través de este enfoque, la situación parece un poco más clara. Otra aproximación visual (esperemos) útil es un gráfico de caja especial
marginplot(data[c(1,2)])

  • Aquí estamos limitados a trazar sólo 2 variables a la vez, pero sin embargo podemos obtener algunas ideas interesantes. El diagrama de caja rojo de la izquierda muestra la distribución de datos faltantes para Ozono, mientras que, el diagrama de caja azul muestra la distribución de los puntos de datos restantes. Lo mismo ocurre con los gráficos de caja de Solar.R en la parte inferior del gráfico. Si nuestra suposición de los datos MCAR es correcta, entonces esperamos que los gráficos de caja rojos y azules sean muy similares.

Borrado de la lista

  • El método más utilizado por los científicos de datos para tratar los datos que faltan es simplemente omitir los casos con datos que faltan, analizando únicamente el resto del conjunto de datos. Este método se conoce como eliminación por lista o análisis de casos completos. La función na.omit() en R elimina todos los casos con uno o más valores de datos faltantes en un conjunto de datos.

  • La mayor ventaja de este método es su comodidad. Sin embargo, si la naturaleza de los datos es MCAR, la eliminación de la lista dará lugar a errores estándar que son significativos para los datos reducidos, pero no para todo el conjunto de datos, que tenía los datos que faltaban. Este método de tratar los datos que faltan es posiblemente un desperdicio. Si se eliminan los casos de esta manera, hay que ser consciente de la disminución de la capacidad para detectar el verdadero efecto de las variables de interés.

  • Sin embargo, si los datos no son MCAR, el análisis de casos completos puede influir mucho en las estimaciones de la media, los coeficientes de regresión y las correlaciones. La supresión de la lista puede causar submuestras sin sentido.

  • A continuación, realizamos una simple función na.omit() para eliminar los casos que tienen NAs. Vemos que todas las filas que contenían algún NA en cualquier variable son eliminadas del dataframe

airquality_omit <- na.omit(data)
head(airquality_omit)
##    Ozone Solar.R Wind Temp
## 12    16     256  9.7   69
## 13    11     290  9.2   66
## 14    14     274 10.9   68
## 15    18      65 13.2   58
## 16    14     334 11.5   64
## 17    34     307 12.0   66
  • Dibujemos los histogramas antes y después de la imputación/eliminación usando ggplot
library(ggplot2)
library(tidyverse)
library(hrbrthemes)
library(gridExtra)
ggp1 <- ggplot(data.frame(value=data$Ozone), aes(x=value)) +
  geom_histogram(fill="#FBD000", color="#E52521", alpha=0.9) +
  ggtitle("Original data") +
  xlab('Ozone') + ylab('Frequency') +
  theme_ipsum() +
  theme(plot.title = element_text(size=15))

ggp2 <- ggplot(data.frame(value=airquality_omit$Ozone), aes(x=value)) +
  geom_histogram(fill="#43B047", color="#049CD8", alpha=0.9) +
  ggtitle("Listwise Deletion") +
  xlab('Ozone') + ylab('Frequency') +
  theme_ipsum() +
  theme(plot.title = element_text(size=15))

grid.arrange(ggp1, ggp2, ncol = 2)

Imputación por emparejamiento predictivo medio

  • Algunos científicos de datos o estadísticos pueden buscar una solución rápida sustituyendo los datos que faltan por la media. La media se utiliza a menudo para imputar datos no categóricos. Por ejemplo, en el conjunto de datos de calidad del aire, supongamos que queremos imputar la media de sus valores faltantes. En este caso, utilizamos el paquete R mice. Cambiando el argumento method = mean, se especifica la imputación de la media, el argumento m = 1 cambia las iteraciones a 1, lo que significa no (iteración).

  • El fundamento teórico de utilizar la media para imputar los datos faltantes es que la media es una buena estimación para seleccionar aleatoriamente una observación de una distribución normal. Ahora, intentaremos imputar la media para las variables Ozono y Solar.R del conjunto de datos airquality. En primer lugar, vamos a cargar los paquetes mice y mipackages (instale el paquete de CRAN primero usando install.packages("mice") y install.packages("mi")). Para mas información sobre el paquete utilizado (ver Mice). Podemos recuperar el conjunto de datos completo utilizando la función complete().

library(mice)
library(foreign)
  • Los datos faltantes para Ozono y Solar.R pueden ser imputados por la función mice().
imp <- mice(data, m=5, maxit=50, method ='pmm', seed=500, printFlag = FALSE)
  • El parámetro m se refiere al número de conjuntos de datos imputados que se generarán. Es decir, m representa la cantidad de veces que se imputarán los datos para crear múltiples conjuntos de datos imputados. Cada conjunto de datos imputados contendrá valores imputados diferentes para los datos faltantes, lo que permite capturar la incertidumbre en el proceso de imputación o falta de certeza sobre los valores reales de los datos faltantes.

  • El parámetro maxit en el paquete mice de R controla cuántas veces se repetirá el proceso de imputación en cada conjunto de datos para alcanzar una convergencia o un límite máximo de iteraciones. Durante cada iteración, el algoritmo intenta mejorar la imputación de los valores faltantes. Si el número de iteraciones alcanza el valor especificado en maxit y el proceso no converge, la imputación se detendrá y se generará un mensaje de advertencia. Ajustar adecuadamente el valor de maxit puede ayudar a controlar el tiempo de ejecución y garantizar una convergencia adecuada del algoritmo de imputación.

  • El valor por defecto es m=5. method='pmm' (ver mice.impute.pmm) se refiere al método de imputación. En este caso, utilizamos el método de imputación por emparejamiento predictivo medio (PMM). Se pueden utilizar otros métodos de imputación, escriba methods(mice) para obtener una lista de los métodos de imputación disponibles.

methods(mice)
##  [1] mice.impute.2l.bin              mice.impute.2l.lmer            
##  [3] mice.impute.2l.norm             mice.impute.2l.pan             
##  [5] mice.impute.2lonly.mean         mice.impute.2lonly.norm        
##  [7] mice.impute.2lonly.pmm          mice.impute.cart               
##  [9] mice.impute.jomoImpute          mice.impute.lasso.logreg       
## [11] mice.impute.lasso.norm          mice.impute.lasso.select.logreg
## [13] mice.impute.lasso.select.norm   mice.impute.lda                
## [15] mice.impute.logreg              mice.impute.logreg.boot        
## [17] mice.impute.mean                mice.impute.midastouch         
## [19] mice.impute.mnar.logreg         mice.impute.mnar.norm          
## [21] mice.impute.mpmm                mice.impute.norm               
## [23] mice.impute.norm.boot           mice.impute.norm.nob           
## [25] mice.impute.norm.predict        mice.impute.panImpute          
## [27] mice.impute.passive             mice.impute.pmm                
## [29] mice.impute.polr                mice.impute.polyreg            
## [31] mice.impute.quadratic           mice.impute.rf                 
## [33] mice.impute.ri                  mice.impute.sample             
## [35] mice.mids                       mice.theme                     
## see '?methods' for accessing help and source code
imp_df <- complete(imp)
head(imp_df)
##   Ozone Solar.R Wind Temp
## 1    41     190  7.4   87
## 2    36     118  8.0   80
## 3    12     149 12.6   74
## 4    18     313 10.9   66
## 5    13      81 16.6   57
## 6    28      78  7.4   66
  • La siguiente imagen ilustra el funcionamiento del método de imputación múltiple por ecuaciones encadenadas
  • Además, la siguiente figura ilustra el funcionamiento del método de imputación por emparejamiento predictivo medio usado en el ejemplo para airquality


  • Observación: El método PMM utiliza aleatoriedad en la imputación para introducir variabilidad en las estimaciones de los valores faltantes. Esto ayuda a evitar la sobreestimación o subestimación sistemática de los valores imputados y a capturar la incertidumbre asociada con la imputación de datos faltantes. La introducción de aleatoriedad también puede ayudar a mejorar la robustez y generalización del modelo de imputación, especialmente en situaciones donde los datos faltantes siguen patrones complejos o no aleatorios.

  • Tracemos histograma y diagrama de dispersión del aspecto del conjunto de datos de la calidad del aire tras la imputación mediante método PMM. Los siguientes son los histogramas asociados

ggp1 <- ggplot(data.frame(value=data$Ozone), aes(x=value)) +
  geom_histogram(fill="#FBD000", color="#E52521", alpha=0.9) +
  ggtitle("Original data") +
  xlab('Ozone') + ylab('Frequency') +
  theme_ipsum() +
  theme(plot.title = element_text(size=15))

ggp2 <- ggplot(data.frame(value=imp_df$Ozone), aes(x=value)) +
  geom_histogram(fill="#43B047", color="#049CD8", alpha=0.9) +
  ggtitle("PMM imputation") +
  xlab('Ozone') + ylab('Frequency') +
  theme_ipsum() +
  theme(plot.title = element_text(size=15))

grid.arrange(ggp1, ggp2, ncol = 2)

  • Vamos a comparar las distribuciones de los datos originales e imputados utilizando algunos gráficos útiles. En primer lugar, podemos utilizar un gráfico de dispersión y comparar Ozone con todas las demás variables.
library(lattice)
xyplot(imp, Ozone ~ Wind + Temp + Solar.R, pch=18, cex=1)

  • Los valores de pch están asociados con los símbolos gráficos desplegados. cex controla el tamaño de los símbolos, por defecto el tamaño es 1, podemos poner valores menores o superiores.


  • Lo que queremos ver es que la forma de los puntos rojos (imputados) coincida con la de los azules (observados). La coincidencia de la forma nos indica que los valores imputados son realmente valores plausibles. Otro gráfico útil es el de la densidad:
densityplot(imp)

  • La densidad de los datos imputados para cada conjunto de datos imputados se muestra en color rojo, mientras que la densidad de los datos observados se muestra en azul. De nuevo, según nuestros supuestos anteriores, esperamos que las distribuciones sean similares.

  • Es muy recomendable comprobar visualmente la convergencia. Lo comprobamos cuando llamamos a la función de trazado a la que asignamos la salida de mice, para mostrar gráficos de seguimiento de la media y desviación estándar de todas las variables implicadas en las imputaciones.

plot(imp);

  • Cada línea es una de las m conjuntos de datos imputados, a saber m=5 por defecto. Como se puede ver en el gráfico de trazado anterior sobre imp, no hay tendencias claras y las variables se superponen de una iteración a la siguiente. Dicho de otro modo, la varianza dentro de una cadena (hay m cadenas) debería ser aproximadamente igual a la varianza entre las cadenas. Esto indica que se ha logrado la convergencia. Si no se logra la convergencia, se puede aumentar el número de iteraciones que mice emplea especificando explícitamente el parámetro maxit a la función mice.

Otras técnicas de imputación

  • La función mice entrega distintos métodos de imputación los cuales puede estudiar y aplicar, de acuerdo a lo que requieran sus datos. Puede aplicar la imputación por regresión en R con la configuración del método method = "norm.predict" en la función mice. Puede aplicar la imputación de regresión estocástica en R con la función mice utilizando el método method = "norm.nob".

  • El paquete mice también incluye un procedimiento de imputación de regresión estocástica Bayesiana. Puede aplicar este procedimiento de imputación con la función mice y utilizar como método method = "norm". Para estos métodos debe seleccionar primero dos columna del conjunto de datos de interés para ajustar constantes del modelo.

data <- data[, c("Solar.R", "Wind")]
imp.regress <- mice(data, method="norm.predict", m=1, maxit=1)
## 
##  iter imp variable
##   1   1  Solar.R  Wind
imp.regress$imp$Wind
##           1
## 4  9.736111
## 5  9.806622
## 6  9.805925
## 7  9.743832
## 8  9.854140
## 9  9.898263
## 10 9.801744
  • En la actualidad, hay un número limitado de análisis que pueden ser automáticamente agrupados por mice el más importante es el de lm/glm. Sin embargo, si se recuerda, el modelo lineal generalizado es extremadamente flexible, y puede utilizarse para expresar una amplia gama de análisis diferentes. Por extensión, podríamos utilizar la imputación múltiple no sólo para regresión lineal, sino para la regresión logística, la regresión de Poisson, las pruebas \(t\), el ANOVA ANCOVA, etc. Cada una de estas temáticas pueden ser abordadas como proyectos finales en este curso.

Detección de valores atípicos

  • Un valor atípico es un valor o una observación que se aleja de otras observaciones, es decir, un punto de datos que difiere significativamente de otros puntos de datos. Enderlein (1987) va incluso más allá, ya que el autor considera que los valores atípicos son aquellos que se desvían tanto de otras observaciones que se podría suponer un mecanismo de muestreo subyacente diferente.

  • Una observación debe compararse siempre con otras observaciones realizadas sobre el mismo fenómeno antes de calificarla realmente de atípica. En efecto, una persona de 200 cm de altura (1,90 m en EE.UU.) se considerará probablemente un valor atípico en comparación con la población general, pero esa misma persona podría no considerarse un valor atípico si midiéramos la altura de los jugadores de baloncesto.

  • En esta sección, presentamos varios enfoques para detectar valores atípicos en R, desde técnicas sencillas como la estadística descriptiva (que incluye el mínimo, el máximo, el histograma, el boxplot y los percentiles) hasta técnicas más formales como el filtro de Hampel, el Grubbs, el Dixon y las pruebas de Rosner para detectar valores atípicos. Algunas pruebas estadísticas exigen la ausencia de valores atípicos para sacar conclusiones sólidas, pero la eliminación de valores atípicos no se recomienda en todos los casos y debe hacerse con precaución.

Estadísticas descriptivas

  • El primer paso para detectar los valores atípicos en R es comenzar con algunos estadísticos descriptivos, y en particular con el mínimo y el máximo. En R, esto se puede hacer fácilmente con la función summary(). El conjunto de datos a utilizar es mpg asociado a consumo de combustible de 1999 a 2008 de 38 modelos populares de coches (ver ggplot2::mpg).
dat <- ggplot2::mpg
head(dat)
## # A tibble: 6 × 11
##   manufacturer model displ  year   cyl trans      drv     cty   hwy fl    class 
##   <chr>        <chr> <dbl> <int> <int> <chr>      <chr> <int> <int> <chr> <chr> 
## 1 audi         a4      1.8  1999     4 auto(l5)   f        18    29 p     compa…
## 2 audi         a4      1.8  1999     4 manual(m5) f        21    29 p     compa…
## 3 audi         a4      2    2008     4 manual(m6) f        20    31 p     compa…
## 4 audi         a4      2    2008     4 auto(av)   f        21    30 p     compa…
## 5 audi         a4      2.8  1999     6 auto(l5)   f        16    26 p     compa…
## 6 audi         a4      2.8  1999     6 manual(m5) f        18    26 p     compa…
summary(dat$hwy)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   12.00   18.00   24.00   23.44   27.00   44.00
  • El mínimo y el máximo son, respectivamente, el primer y el último valor de hwy. Como alternativa, también pueden calcularse con las funciones min() y max()
min(dat$hwy)
## [1] 12
max(dat$hwy)
## [1] 44
  • Un claro error de codificación, como un peso de 786 kg (1733 libras) para un humano, ya se detectará fácilmente con esta técnica tan sencilla.

Histogramas

  • Otra forma básica de detectar valores atípicos es dibujar un histograma de los datos. Utilizando la base de R (con el número de bins correspondiente a la raíz cuadrada del número de observaciones para tener más bins que la opción por defecto), o usando ggplot2. Considere la columna hwy: highway mileage (miles per gallon)
hist(dat$hwy,
  xlab = "hwy",
  main = "Histogram of hwy",
  breaks = sqrt(nrow(dat))
)

library(ggplot2)

ggplot(dat) +
  aes(x = hwy) +
  geom_histogram(bins = 30L, fill = "#0c4c8a") +
  theme_minimal()

  • Con base en este histograma, se evidencia que hay un par de observaciones más altas que todas las demás

Boxplot

  • Además de los histogramas, los boxplots también son útiles para detectar posibles valores atípicos. Considere nuevamente a manera de ejemplo la columna hwy
boxplot(dat$hwy,
  ylab = "hwy"
)

ggplot(dat) +
  aes(x = "", y = hwy) +
  geom_boxplot(fill = "#0c4c8a") +
  theme_minimal()

  • Un diagrama de caja ayuda a visualizar una variable cuantitativa mostrando cinco resúmenes de localización comunes (mínimo, mediana, primer y tercer cuartil y máximo) y cualquier observación que se haya clasificado como presunto valor atípico utilizando el criterio del rango intercuartílico (IQR).

  • Las observaciones consideradas como posibles valores atípicos según el criterio IQR se muestran como puntos en el diagrama de caja. Según este criterio, hay 2 valores atípicos potenciales (véanse los 2 puntos por encima de la línea vertical, en la parte superior del boxplot).

  • Recuerde que no por el hecho de que una observación sea considerada como un valor atípico potencial por el criterio IQR debe eliminarla. Eliminar o mantener un valor atípico depende de: (i) el contexto de su análisis, (ii) si las pruebas que va a realizar en el conjunto de datos son robustas a los valores atípicos o no, y (iii) a qué distancia está el valor atípico de otras observaciones.

  • También es posible extraer los valores de los posibles valores atípicos basándose en el criterio IQR gracias a la función boxplot.stats()$out:

boxplot.stats(dat$hwy)$out
## [1] 44 44 41
  • Como puede ver, en realidad hay 3 puntos considerados como posibles valores atípicos: 2 observaciones con un valor de 44 y 1 observación con un valor de 41. Gracias a la función which() es posible extraer el número de fila correspondiente a estos valores atípicos:
out <- boxplot.stats(dat$hwy)$out
out_ind <- which(dat$hwy %in% c(out))
out_ind
## [1] 213 222 223
  • Con esta información, ahora puede volver fácilmente a las filas específicas del conjunto de datos para verificarlas, o imprimir todas las variables para estos valores atípicos:
dat[out_ind, ]
## # A tibble: 3 × 11
##   manufacturer model      displ  year   cyl trans  drv     cty   hwy fl    class
##   <chr>        <chr>      <dbl> <int> <int> <chr>  <chr> <int> <int> <chr> <chr>
## 1 volkswagen   jetta        1.9  1999     4 manua… f        33    44 d     comp…
## 2 volkswagen   new beetle   1.9  1999     4 manua… f        35    44 d     subc…
## 3 volkswagen   new beetle   1.9  1999     4 auto(… f        29    41 d     subc…
  • También es posible imprimir los valores de los valores atípicos directamente en el boxplot con la función mtext()
boxplot(dat$hwy,
        ylab = "hwy",
        main = "Boxplot of highway miles per gallon"
)
mtext(paste("Outliers: ", paste(out, collapse = ", ")))

Percentiles

  • Este método de detección de valores atípicos se basa en los percentiles. Con el método de los percentiles, todas las observaciones que se encuentren fuera del intervalo formado por los percentiles 2.5 y 97.5 se considerarán como posibles valores atípicos

\[ I_{\text{out}}=[q_{0.025}; q_{0.975}]^{C} \]

  • También pueden considerarse otros percentiles, como el 1 y el 99, o el 5 y el 95, para construir el intervalo. Los valores de los percentiles inferior y superior (y, por tanto, los límites inferior y superior del intervalo) pueden calcularse con la función quantile()
lower_bound <- quantile(dat$hwy, 0.025)
lower_bound
## 2.5% 
##   14
upper_bound <- quantile(dat$hwy, 0.975)
upper_bound
##  97.5% 
## 35.175
  • Según este método, todas las observaciones por debajo de 14 y por encima de 35.175 se considerarán posibles valores atípicos. Los números de fila de las observaciones fuera del intervalo pueden extraerse entonces con la función which()
outlier_ind <- which(dat$hwy < lower_bound | dat$hwy > upper_bound)
outlier_ind
##  [1]  55  60  66  70 106 107 127 197 213 222 223
dat[outlier_ind, "hwy"]
## # A tibble: 11 × 1
##      hwy
##    <int>
##  1    12
##  2    12
##  3    12
##  4    12
##  5    36
##  6    36
##  7    12
##  8    37
##  9    44
## 10    44
## 11    41
dat[outlier_ind, ]
## # A tibble: 11 × 11
##    manufacturer model      displ  year   cyl trans drv     cty   hwy fl    class
##    <chr>        <chr>      <dbl> <int> <int> <chr> <chr> <int> <int> <chr> <chr>
##  1 dodge        dakota pi…   4.7  2008     8 auto… 4         9    12 e     pick…
##  2 dodge        durango 4…   4.7  2008     8 auto… 4         9    12 e     suv  
##  3 dodge        ram 1500 …   4.7  2008     8 auto… 4         9    12 e     pick…
##  4 dodge        ram 1500 …   4.7  2008     8 manu… 4         9    12 e     pick…
##  5 honda        civic        1.8  2008     4 auto… f        25    36 r     subc…
##  6 honda        civic        1.8  2008     4 auto… f        24    36 c     subc…
##  7 jeep         grand che…   4.7  2008     8 auto… 4         9    12 e     suv  
##  8 toyota       corolla      1.8  2008     4 manu… f        28    37 r     comp…
##  9 volkswagen   jetta        1.9  1999     4 manu… f        33    44 d     comp…
## 10 volkswagen   new beetle   1.9  1999     4 manu… f        35    44 d     subc…
## 11 volkswagen   new beetle   1.9  1999     4 auto… f        29    41 d     subc…
  • Hay 11 valores atípicos potenciales según el método de los percentiles. Para reducir este número, puede establecer los percentiles en 1 y 99:
lower_bound <- quantile(dat$hwy, 0.01)
upper_bound <- quantile(dat$hwy, 0.99)

outlier_ind <- which(dat$hwy < lower_bound | dat$hwy > upper_bound)

dat[outlier_ind, ]
## # A tibble: 3 × 11
##   manufacturer model      displ  year   cyl trans  drv     cty   hwy fl    class
##   <chr>        <chr>      <dbl> <int> <int> <chr>  <chr> <int> <int> <chr> <chr>
## 1 volkswagen   jetta        1.9  1999     4 manua… f        33    44 d     comp…
## 2 volkswagen   new beetle   1.9  1999     4 manua… f        35    44 d     subc…
## 3 volkswagen   new beetle   1.9  1999     4 auto(… f        29    41 d     subc…

Filtro de Hampel

  • Otro método, conocido como filtro de Hampel, consiste en considerar como valores atípicos los valores fuera del intervalo formado por la mediana, más o menos 3 desviaciones absolutas de la mediana

    \[ I=[\tilde{X}-3\cdot\textsf{MAD}, \tilde{X}+3\cdot\textsf{MAD}], \] donde MAD es la desviación absoluta de la mediana y se define como la mediana de las desviaciones absolutas de la mediana \(\tilde{X}=\textsf{median}(X)\) de los datos

    \[ \textsf{MAD}=k\times\textsf{median}(|X_{i}-\tilde{X}|) \]

  • Para este método, primero establecemos los límites del intervalo, gracias a las funciones median() y mad(). En este caso, k representa el factor de escala, por defecto, R lo considera como 1.

lower_bound <- median(dat$hwy) - 3 * mad(dat$hwy, constant = 1)
lower_bound
## [1] 9
upper_bound <- median(dat$hwy) + 3 * mad(dat$hwy, constant = 1)
upper_bound
## [1] 39
  • Según este método, todas las observaciones por debajo de 9 y por encima de 39 se considerarán como posibles valores atípicos. Los números de fila de las observaciones que están fuera del intervalo pueden entonces extraerse con la función which(). Según el filtro de Hampel, hay 3 valores atípicos para la variable hwy.
outlier_ind <- which(dat$hwy < lower_bound | dat$hwy > upper_bound)
outlier_ind
## [1] 213 222 223
dat[outlier_ind, ]
## # A tibble: 3 × 11
##   manufacturer model      displ  year   cyl trans  drv     cty   hwy fl    class
##   <chr>        <chr>      <dbl> <int> <int> <chr>  <chr> <int> <int> <chr> <chr>
## 1 volkswagen   jetta        1.9  1999     4 manua… f        33    44 d     comp…
## 2 volkswagen   new beetle   1.9  1999     4 manua… f        35    44 d     subc…
## 3 volkswagen   new beetle   1.9  1999     4 auto(… f        29    41 d     subc…

Prueba de Grubbs

  • La prueba de Grubbs permite detectar si el valor más alto o más bajo de un conjunto de datos es un valor atípico (ver Grubbs). La prueba de Grubbs detecta un valor atípico cada vez (valor más alto o más bajo), por lo que las hipótesis nula y alternativa son las siguientes

    • \(H_{0}\): El valor más alto/bajo no es un valor atípico
    • \(H_{1}\): El valor más alto/bajo es un valor atípico


  • Como para cualquier prueba estadística, si el valor \(p\) es inferior al umbral de significación elegido (generalmente \(\alpha=0.05\)), se rechaza la hipótesis nula y se concluye que el valor más bajo/más alto es un valor atípico. Por el contrario, si el valor \(p\) es mayor o igual que el nivel de significación, no se rechaza la hipótesis nula y concluiremos que, basándonos en los datos, no rechazamos la hipótesis de que el valor más bajo/más alto no es un valor atípico. Tenga en cuenta que la prueba de Grubbs no es apropiada para un tamaño de muestra \(n\leq6\).

  • Para realizar la prueba de Grubbs en R, utilizamos la función grubbs.test() del paquete outliers. La función recibe los siguientes argumentos: grubbs.test(x, type = 10, opposite = FALSE, two.sided = FALSE). En este caso, type: 10 = prueba si el valor máximo es un valor atípico, 11 = prueba si tanto el valor mínimo como el máximo son valores atípicos, 20 = prueba si hay dos valores atípicos en una cola.

library(outliers)
test <- grubbs.test(dat$hwy)
test
## 
##  Grubbs test for one outlier
## 
## data:  dat$hwy
## G = 3.45274, U = 0.94862, p-value = 0.05555
## alternative hypothesis: highest value 44 is an outlier
  • El valor \(p\) es de 0.056 approx. Al nivel de significación del 5%, no rechazamos la hipótesis de que el valor más alto 44 no es un valor atípico. No tenemos evidencia suficiente para decir que 44 es un valor atípico. Por defecto, la prueba se realiza sobre el valor más alto (como se muestra en la salida de R: hipótesis alternativa: el valor más alto 44 es un valor atípico). Si desea realizar la prueba para el valor más bajo, simplemente añada el argumento opposite = TRUE en la función grubbs.test()
test <- grubbs.test(dat$hwy, opposite = TRUE)
test
## 
##  Grubbs test for one outlier
## 
## data:  dat$hwy
## G = 1.92122, U = 0.98409, p-value = 1
## alternative hypothesis: lowest value 12 is an outlier
  • La salida de R indica que la prueba se realiza ahora sobre el valor más bajo (véase la hipótesis alternativa: el valor más bajo 12 es un valor atípico). El valor \(p\) es 1. Al nivel de significación del 5%, no rechazamos la hipótesis de que el valor más bajo 12 no es un valor atípico.

  • A modo de ilustración, sustituiremos ahora una observación por un valor más extremo y realizaremos la prueba de Grubbs en este nuevo conjunto de datos. Reemplacemos el \(34^{\text{th}}\) por un valor de 212

dat[34, "hwy"] <- 212
  • Y ahora aplicamos la prueba de Grubbs para comprobar si el valor más alto es un valor atípico
test <- grubbs.test(dat$hwy)
test
## 
##  Grubbs test for one outlier
## 
## data:  dat$hwy
## G = 13.72240, U = 0.18836, p-value < 2.2e-16
## alternative hypothesis: highest value 212 is an outlier
  • El valor \(p\) es menor que la significancia. Al nivel de significancia del 5%, concluimos que el valor más alto 212 es un valor atípico.

Prueba de Dixon

  • Al igual que la prueba de Grubbs, la prueba de Dixon se utiliza para comprobar si un único valor bajo o alto es un valor atípico. Por lo tanto, si se sospecha que hay más de un valor atípico, la prueba tiene que realizarse en estos valores atípicos sospechosos individualmente. Tenga en cuenta que la prueba de Dixon es más útil para muestras de pequeño tamaño (normalmente \(n\leq 25\)).

  • Para realizar la prueba de Dixon en R, utilizamos la función dixon.test() del paquete outliers. Sin embargo, restringimos nuestro conjunto de datos a las 20 primeras observaciones, ya que la prueba de Dixon sólo se puede realizar en muestras de pequeño tamaño (R arrojará un error y sólo acepta conjuntos de datos de 3 a 30 observaciones)

subdat <- dat[1:20, ]
test <- dixon.test(subdat$hwy)
test
## 
##  Dixon test for outliers
## 
## data:  subdat$hwy
## Q = 0.57143, p-value = 0.006508
## alternative hypothesis: lowest value 15 is an outlier
  • Los resultados muestran que el valor más bajo, 15, es un valor atípico, con valor \(p\) menor que la significancia del 5%. Para comprobar el valor más alto, basta con añadir el argumento opuesto = TRUE a la función dixon.test()
test <- dixon.test(subdat$hwy, opposite = TRUE)
test
## 
##  Dixon test for outliers
## 
## data:  subdat$hwy
## Q = 0.25, p-value = 0.8582
## alternative hypothesis: highest value 31 is an outlier
  • Los resultados muestran que el valor más alto 31 no es un valor atípico (valor \(p = 0.8582\)). Es una buena práctica comprobar siempre los resultados de la prueba estadística de valores atípicos con el diagrama de caja para asegurarse de que hemos comprobado todos los valores atípicos potenciales
out <- boxplot.stats(subdat$hwy)$out
boxplot(subdat$hwy, ylab = "hwy")
mtext(paste("Outliers: ", paste(out, collapse = ", ")))

  • A partir del boxplot, vemos que también podríamos aplicar la prueba de Dixon sobre el valor 20 además del valor 15 realizado anteriormente. Esto puede hacerse encontrando el número de fila del valor mínimo, excluyendo este número de fila del conjunto de datos y aplicando finalmente la prueba de Dixon a este nuevo conjunto de datos
remove_ind <- which.min(subdat$hwy)
subsubdat <- subdat[-remove_ind, ]

tail(subsubdat)
## # A tibble: 6 × 11
##   manufacturer model       displ  year   cyl trans drv     cty   hwy fl    class
##   <chr>        <chr>       <dbl> <int> <int> <chr> <chr> <int> <int> <chr> <chr>
## 1 audi         a4 quattro    3.1  2008     6 auto… 4        17    25 p     comp…
## 2 audi         a4 quattro    3.1  2008     6 manu… 4        15    25 p     comp…
## 3 audi         a6 quattro    2.8  1999     6 auto… 4        15    24 p     mids…
## 4 audi         a6 quattro    3.1  2008     6 auto… 4        17    25 p     mids…
## 5 audi         a6 quattro    4.2  2008     8 auto… 4        16    23 p     mids…
## 6 chevrolet    c1500 subu…   5.3  2008     8 auto… r        14    20 r     suv
test <- dixon.test(subsubdat$hwy)
test
## 
##  Dixon test for outliers
## 
## data:  subsubdat$hwy
## Q = 0.44444, p-value = 0.1297
## alternative hypothesis: lowest value 20 is an outlier
  • Los resultados muestran que el segundo valor más bajo, 20, no es un valor atípico (valor \(p\) = 0.13).

Prueba de Rosner

  • La prueba de Rosner para detectar valores atípicos tiene las siguientes ventajas. Se utiliza para detectar varios valores atípicos a la vez (a diferencia de la prueba de Grubbs y Dixon, que debe realizarse de forma iterativa para detectar múltiples valores atípicos), y está diseñado para evitar el problema del enmascaramiento, en el que un valor atípico cercano a otro atípico puede pasar desapercibido. A diferencia de la prueba de Dixon, hay que tener en cuenta que la prueba de Rosner es más apropiada cuando el tamaño de la muestra es grande (\(n\geq 20\)). Por tanto, volvemos a utilizar el conjunto de datos inicial dat, que incluye 234 observaciones.

  • Para realizar la prueba de Rosner utilizamos la función rosnerTest() del paquete EnvStats. Esta función requiere al menos 2 argumentos: los datos y el número de presuntos valores atípicos k (con k = 3 como número de presuntos valores atípicos por defecto). Para este ejemplo, establecemos que el número de presuntos valores atípicos sea igual a 3, tal y como sugiere el número de posibles valores atípicos esbozado en el boxplot al principio del artículo.

library(EnvStats)
test <- rosnerTest(dat$hwy, k = 3)
  • Los resultados interesantes se ofrecen en la tabla all.stats
test$all.stats
##   i   Mean.i      SD.i Value Obs.Num     R.i+1 lambda.i+1 Outlier
## 1 0 24.21795 13.684345   212      34 13.722399   3.652091    TRUE
## 2 1 23.41202  5.951835    44     213  3.459098   3.650836   FALSE
## 3 2 23.32328  5.808172    44     222  3.559936   3.649575   FALSE
  • Basándonos en la prueba de Rosner, vemos que sólo hay un valor atípico (véase la columna de valores atípicos), y que es la observación 34 (véase Obs.Num) con un valor de 212 (véase Value).

Tratamiento de los valores atípicos

  • Una vez identificados los valores atípicos y habiendo decidido enmendar la situación según la naturaleza del problema, puede considerar uno de los siguientes enfoques.

    • Imputación

      • Este método se ha tratado en detalle en la discusión sobre el tratamiento de los valores faltantes.
    • Capping

      • Para los valores que se encuentran fuera de los límites de \(1.5\cdot\mathsf{IQR}\), podríamos poner un tope sustituyendo las observaciones que se encuentran fuera del límite inferior por el valor del \(5^{\textsf{th}}\) percentíl y las que se encuentran por encima del límite superior, por el valor del \(95^{\textsf{th}}\) percentíl. A continuación se muestra un código de ejemplo que logra esto
x <- dat$hwy
qnt <- quantile(x, probs=c(.25, .75), na.rm = T)  #Quartiles
caps <- quantile(x, probs=c(.05, .95), na.rm = T) #5th,95th Percentiles
H <- 1.5 * IQR(x, na.rm = T)
x[x < (qnt[1] - H)] <- caps[1]
x[x > (qnt[2] + H)] <- caps[2]
head(x)
## [1] 29 29 31 30 26 26
  • Predicción

    • En otro enfoque, los valores atípicos pueden sustituirse por valores faltantes (NA) y luego pueden predecirse considerándolos como una variable de respuesta. Ya hemos hablado de cómo predecir los valores faltantes en la sección anterior.

Ejercicio para entregar

  • Realicé un EDA para el siguiente conjunto de datos (ver Diabetes Datasert). El conjunto de datos consta de varias variables médicas predictoras (independientes) y una variable objetivo (dependiente), resultado. Las variables independientes incluyen el número de embarazos que ha tenido la paciente, BMI (Body mass index (weight in kg/(height in m)^2), nivel de insulina, edad, etc. En este dataset, sustituimos todos los valores nulos “ausentes” por NAN y realizamos el respectivo análisis de datos faltantes. Nótese que está escrito en Python, escriba el equivalente en R.
df.loc[df["Glucose"] == 0.0, "Glucose"] = np.NAN
df.loc[df["BloodPressure"] == 0.0, "BloodPressure"] = np.NAN
df.loc[df["SkinThickness"] == 0.0, "SkinThickness"] = np.NAN
df.loc[df["Insulin"] == 0.0, "Insulin"] = np.NAN
df.loc[df["BMI"] == 0.0, "BMI"] = np.NAN
  • Aplique imputación de datos usando las siguientes opciones para method:

    • method="pmm"”
    • method="norm.predict"
    • method="norm.nob"
    • method="norm"
  • Identifique datos atípicos para cada variable en el dataset usando las técnicas estudiadas en clase. Además, realice imputación de los datos atípicos con base en lo desarrollado en el ítem anterior.

    • Imputación/Capping/Predicción
    • Test de Rosner, Dixon, Grubbs, Hampel, Percentiles, Boxplots, Histograms, Descriptivos

Modelo de series de tiempo

  • En esta sección estudiaremos la forma de implementar el modelo ARIMA estudiado antes en la sección de Python. La teoría abordada para este modelo es la misma, por lo tanto, algunas cosas serán omitidas y pasaremos directamente a la implementación en R de este modelo predictivo.

Modelo ARIMA

  • El modelo Media Móvil Integrada Autorregresiva (ARIMA) es el nombre genérico de una familia de modelos predictivos que se basan en los procesos Autorregresivo (AR) y de Media Móvil (MA).

  • Entre los modelos predictivos tradicionales (por ejemplo, la regresión lineal, suavización exponencial, etc.), el modelo ARIMA se considera el enfoque más avanzado y robusto.

  • En esta sección, presentaremos los componentes del modelo: los procesos AR y MA y el componente de diferenciación I. Además, nos centraremos en los métodos y enfoques para ajustar los parámetros del modelo con el uso de diferenciación, la función de autocorrelación (ACF) y la función de autocorrelación parcial (PACF).

  • En esta sección cubriremos lo siguiente

    • El proceso estacionario
    • El proceso random walk
      • Random walk: \(X_{t}=X_{t-1}+\varepsilon_{t}\), donde \(\varepsilon_{t}\), v. a. determina la dirección del movimiento tomando valores “izquierda (-1) o derecha (1)” con igual probabilidad.
    • Los procesos AR y MA
    • Los modelos ARMA y ARIMA
    • El modelo ARIMA estacional
  • Librerías:

    • forecast
    • TSstudio
    • plotly
    • dplyr
    • lubridate
    • stats
    • datasets
    • base

El proceso estacionario

  • Uno de los principales supuestos de la familia de modelos ARIMA es que las series de entrada siguen la estructura de un proceso estacionario. Este supuesto se basa en el Teorema de representación de Wold, que afirma que cualquier proceso estacionario puede representarse como una combinación lineal de ruido blanco.

  • Por lo tanto, antes de sumergirnos en los componentes del modelo ARIMA, vamos a detenernos a hablar del proceso estacionario. El proceso estacionario, en el contexto de los datos de las series temporales, describe un estado estocástico de la serie. Los datos de las series temporales son estacionarios si se dan las siguientes condiciones:

    • La media y la varianza de la serie no cambian con el tiempo: \(E(X_{t})=E(X_{t+k})=\mu\) y \(V(X_{t})=V(X_{t+k})=\sigma^2\)
    • La estructura de correlación de la serie, junto con sus rezagos, permanece igual a lo largo del tiempo tiempo: \[\gamma_{k}=E[(X_{t}-\mu)(X_{t+k}-\mu)]\]
Serie de tiempo estacionaria. Fuente Montgromery 2015
Serie de tiempo estacionaria. Fuente Montgromery 2015


  • En los siguientes ejemplos, utilizaremos la función arima.sim del paquete stats para simular datos de series temporales estacionarios y no estacionarios y los representaremos con la función ts_plot del paquete TSstudio. La función arima.sim nos permite simular datos de series temporales basándonos en los componentes y características principales del modelo ARIMA:

    • Un proceso autorregresivo (AR): Establece una relación entre la serie y sus \(p\) rezagos pasados con el uso de un modelo de regresión

    • Un proceso de media móvil (MA): Similar al proceso AR, el proceso MA establece la relación con el término de error en el tiempo \(t\) y los términos de error pasados, con el uso de regresión entre los dos componentes

    • Proceso integrado (I): El proceso de diferenciar la serie con sus \(d\) rezagos para transformar la serie en un estado estacionario. Aquí, el argumento del modelo de la función define \(p, q\) y \(d\), así como el orden de los procesos AR, MA, y los procesos I del modelo.

  • Consideraríamos una serie temporal de datos como no estacionaria siempre que las condiciones mencionadas anteriormente no se cumplan. Los ejemplos más comunes de una serie con una estructura no estacionaria son los siguientes:

  • Serie con tendencia dominante: La media de la serie cambia a lo largo del tiempo como función del cambio en la tendencia de la serie, y por tanto la serie es no estacionaria

  • Serie con componente estacional multiplicativo: En este caso, la varianza de la serie es una función de la oscilación estacional a lo largo del tiempo, que aumenta o disminuye con el tiempo

  • La serie clásica AirPassenger (el número mensual de pasajeros de las aerolíneas entre 1949 y 1960) del paquete de conjuntos de datos es un buen ejemplo de una serie que viola las dos condiciones del proceso estacionario.

  • Puede visualizar la serie usando View(AirPassengers). Dado que la serie tiene tanto una fuerte tendencia lineal como un componente estacional multiplicativo, tanto la media como la varianza cambian con el tiempo:

library(TSstudio)
data(AirPassengers)
class(AirPassengers)
## [1] "ts"
str(AirPassengers)
##  Time-Series [1:144] from 1949 to 1961: 112 118 132 129 121 135 148 148 136 119 ...
ts_plot(AirPassengers,
        title = "Monthly Airline Passenger Numbers 1949-1960",
        Ytitle = "Thousands of Passengers",
        Xtitle = "Year")
summary(AirPassengers)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   104.0   180.0   265.5   280.3   360.5   622.0
hist(AirPassengers, main = "Histgram for Air Passengers", xlab = "Passengers", breaks = "Sturges", probability = TRUE)
lines(density(AirPassengers))

sum(is.na(AirPassengers))
## [1] 0
frequency(AirPassengers)
## [1] 12
cycle(AirPassengers)
##      Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
## 1949   1   2   3   4   5   6   7   8   9  10  11  12
## 1950   1   2   3   4   5   6   7   8   9  10  11  12
## 1951   1   2   3   4   5   6   7   8   9  10  11  12
## 1952   1   2   3   4   5   6   7   8   9  10  11  12
## 1953   1   2   3   4   5   6   7   8   9  10  11  12
## 1954   1   2   3   4   5   6   7   8   9  10  11  12
## 1955   1   2   3   4   5   6   7   8   9  10  11  12
## 1956   1   2   3   4   5   6   7   8   9  10  11  12
## 1957   1   2   3   4   5   6   7   8   9  10  11  12
## 1958   1   2   3   4   5   6   7   8   9  10  11  12
## 1959   1   2   3   4   5   6   7   8   9  10  11  12
## 1960   1   2   3   4   5   6   7   8   9  10  11  12
boxplot(AirPassengers~cycle(AirPassengers),xlab="Date", ylab = "Passenger Numbers (1000's)" ,main ="Monthly Air Passengers Boxplot from 1949 to 1961")

Transformación de una serie no estacionaria en una serie serie estacionaria

  • En la mayoría de los casos, a menos que tenga mucha suerte, sus datos brutos probablemente vendrán con una tendencia u otra forma de oscilación que viole los supuestos del proceso estacionario. Por lo tanto, para manejar esto, tendrá que aplicar algunos pasos de transformación con el fin de llevar la serie a un estado estacionario. Los métodos de transformación más comunes son la diferenciación de la serie (detrending) y la transformación logarítmica (o ambas). Repasemos las aplicaciones de estos métodos.

Diferenciación de series temporales

  • El método más habitual para transformar los datos de una serie temporal no estacionaria en un estado estacionario es diferenciar la serie con sus rezagos. El principal efecto de diferenciar una serie es la eliminación de la tendencia de la serie (o la pérdida de tendencia de la serie), que ayuda a estabilizar la media de la serie.

  • Medimos el grado u orden de diferenciación de la serie por el número de veces que diferenciamos la serie con sus rezagos. Por ejemplo, la siguiente ecuación define la diferencia de primer orden:

\[ Y_{t}'=Y_{t}-Y_{t-1} \]

  • Aquí \(Y_{t}'\), representa la diferencia de primer orden de la serie, y \(Y_{t}, Y_{t-1}\) representan la serie y su primer rezago respectivamente. En algunos casos, el uso de la diferencia de primer orden no es suficiente para llevar la serie a un estado estacionario, y es posible que desee aplicar la diferencia de segundo orden:

\[ Y_{t}^{''}=Y_{t}^{'}-Y_{t-1}^{'}=(Y_{t}-Y_{t-1})-(Y_{t-1}-Y_{t-2})= Y_{t}-2Y_{t-1}+Y_{t-2} \]

  • Otra forma de diferenciación es la diferenciación estacional, que se basa en diferenciar la series con el rezago estacional:

\[ Y_{t}^{'}=Y_{t}-Y_{t-f} \]

  • Aquí, \(f\) representa la frecuencia de la serie y \(Y_{t-f}\) representa el rezago estacional de la serie. La función diff del paquete base de R diferencia la serie de entrada con un rezago específico, configurando el argumento lag de rezago correspondiente. Volvamos a la serie AirPassenger y veamos cómo el primer orden y la diferenciación estacional afectan a la estructura de la serie.

  • Empezaremos con la diferencia de primer orden:

library(TSstudio)
data(AirPassengers)
ts_plot(diff(AirPassengers, lag = 1),
        title = "AirPassengers Series - First Differencing",
        Xtitle = "Year",
        Ytitle = "Differencing of Thousands of Passengers")
  • Puede ver que la primera diferencia de la serie AirPassenger eliminó la tendencia de la serie y que la media de la serie es, en general, constante en el tiempo. Por otra parte, hay una clara evidencia de que la variación de la serie está aumentando con el tiempo, y por lo tanto la serie aún no es estacionaria.

  • Además de la diferencia de primer orden, tomar la diferencia estacional de la serie podría resolver este problema. Realicemos la diferencia estacional a la diferencia de primer orden y dibujemos la serie resultante

library(TSstudio)
data(AirPassengers)
ts_plot(diff(diff(AirPassengers, lag = 1), 12),
        title = "AirPassengers Series - First and Seasonal Differencing",
        Xtitle = "Year",
        Ytitle = "Differencing of Thousands of Passengers")
  • La diferencia estacional ha conseguido estabilizar la variación de la serie. Ahora la serie parece ser estacionaria.

Transformación logarítmica

  • Podemos utilizar transformación logarítmica para estabilizar una oscilación estacional multiplicativa, si es que existe. Este enfoque no es una sustitución de la diferenciación, sino una adición. Por ejemplo, en el caso de AirPassenger de la sección anterior, vimos que la primera diferenciación hace un gran trabajo al estabilizar la media de la serie, pero no es suficiente para estabilizar la varianza de la serie.

  • Por lo tanto, podemos aplicar una transformación logarítmica para transformar la estructura estacional de multiplicable a aditiva y luego aplicar la diferencia de primer orden para estacionar la serie

ts_plot(log(AirPassengers),
        title = "AirPassengers Series - Log Transformation",
        Xtitle = "Year",
        Ytitle = "Log of Thousands of Passengers")
ts_plot(diff(log(AirPassengers), lag = 1),
        title = "AirPassengers Series - First Differencing with Log Transformation",
        Xtitle = "Year",
        Ytitle = "Differencing/Log of Thousands of Passengers")
  • La transformación logarítmica con la diferenciación de primer orden está haciendo un mejor trabajo de transformación de la serie en un estado estacionario con respecto a la doble diferenciación (de primer orden con diferenciación estacional) que utilizamos anteriormente.

Proceso ARIMA

  • Una de las limitaciones de los modelos AR, MA y ARMA es que no pueden manejar datos de series temporales no estacionarias. Por lo tanto, si la serie de entrada es no estacionaria, se requiere un paso de pre-procesamiento para transformar la serie de un estado no estacionario a un estado estacionario.

  • El modelo ARIMA ofrece una solución para este problema al añadir el proceso integrado para el modelo ARMA. El proceso integrado (I) consiste simplemente en diferenciar la serie con sus rezagos, donde el grado de diferenciación está representado por el parámetro d. Podemos generalizar el proceso de diferenciación con la siguiente ecuación:

    \[ Y_{d}=(Y_{t}-Y_{t-1})-\cdots-(Y_{t-d+1}-Y_{t-d}), \] donde \(Y_{d}\) es la \(d\) diferenciación de la series. Agreguemos el componente de diferenciación al modelo ARMA y formalicemos el modelo ARIMA:

    \[ \textsf{ARIMA}(p, d, q): Y_{d}=c+\sum_{i=1}^{p}\phi_{i}Y_{d-i}+\sum_{i=1}^{q}\theta_{i}\epsilon_{t-i}+\epsilon_{t}, \]

    donde

    • \(\textsf{ARIMA}(p,d,q)\) define un proceso ARIMA con un proceso AR de orden \(p\), un grado \(d\) de diferenciación, y un proceso MA de orden \(q\)
    • \(Y_{d}\) es la diferencia \(d\) de la serie \(Y_{t}\)
    • \(c\) representa una constante (o desviación)
    • \(p\) define el número de rezagos que se regresan contra \(Y_{t}\)
    • \(Y_{d-i}\) es el coeficiente del rezago \(i\) de la serie
    • \(q\) define el número de términos de error pasados que se utilizarán en la ecuación
    • \(\theta_{i}\) es el coeficiente correspondiente de \(\epsilon_{t-i}\)
    • \(\epsilon_{t-q},\dots,\epsilon_{t}\) son términos de error de ruido blanco
    • \(\epsilon_{t}\) representa el término de error, que es ruido blanco
  • Se pueden representar los modelos AR, MA o ARMA con el modelo ARIMA, por ejemplo:

    • El modelo ARIMA(0, 0, 0) es equivalente al ruido blanco
    • El modelo ARIMA(0, 1, 0) es equivalente a un paseo aleatorio
    • El modelo ARIMA(1, 0, 0) es equivalente a un proceso AR(1)
    • El modelo ARIMA(0, 0, 1) es equivalente a un proceso MA(1)
    • El modelo ARIMA(1, 0, 1) es equivalente a un proceso ARMA(1,1)

Identificación del grado de diferenciación del modelo

  • Al igual que los parámetros \(p\) y \(q\), el ajuste del parámetro \(d\) (el grado de diferenciación de la de la serie) puede hacerse con los gráficos ACF y PACF. En el siguiente ejemplo, utilizaremos los precios mensuales de Robusta Coffee desde el año 2000.
  • Esta serie forma parte del objeto Coffee_Prices del paquete TSstudio. Empezaremos cargando la serie Coffee_Prices y restando los precios mensuales del café robusta desde enero de 2010 utilizando la función window:
data("Coffee_Prices")
robusta_price <- window(Coffee_Prices[,1], start = c(2000, 1))

ts_plot(robusta_price,
        title = "The Robusta Coffee Monthly Prices",
        Ytitle = "Price in USD",
        Xtitle = "Year")
  • Como se puede ver, los precios del Café Robusta a lo largo del tiempo tienen una tendencia al alza, por lo que no se encuentra en un estado estacionario. Además, como esta serie representa precios continuos, es probable que la serie tenga una fuerte relación de correlación con sus rezagos pasados (ya que los cambios en el precio son típicamente cercanos al precio anterior).

  • Volveremos a utilizar la función acf() para identificar el tipo de relación entre la serie y sus rezagos:

acf(robusta_price, lag.max = 120, ylim = c(-0.5, 1))

  • Como se puede ver en la salida anterior del gráfico ACF, la autocorrelación de la serie con sus rezagos decae lentamente en el tiempo de forma lineal. La eliminación de la tendencia de la serie y de la correlación entre la serie y sus rezagos puede hacerse por diferenciación. Comenzaremos con la primera diferenciación utilizando la función diff:
robusta_price_d1 <- diff(robusta_price)
par(mfrow=c(1,2))
acf(robusta_price_d1, lag.max = 20)
pacf(robusta_price_d1, lag.max = 20)

  • Los gráficos ACF y PACF de la primera diferencia de la serie indican que un proceso AR(2)es apropiado para utilizar en la serie diferenciada, ya que el ACF es de cola (geometric) y la banda de significancia en la PACF corta significativamente en dos rezagos.

  • Por lo tanto, aplicaremos un modelo ARIMA(2,1,0) a la serie robusta_price para incluir la primera diferencia:

library(forecast)

robusta_md <- arima(robusta_price, order = c(2, 1, 0))
  • Utilizaremos la función summary para revisar los detalles del modelo:
summary(robusta_md)
## 
## Call:
## arima(x = robusta_price, order = c(2, 1, 0))
## 
## Coefficients:
##          ar1      ar2
##       0.2806  -0.0095
## s.e.  0.0674   0.0673
## 
## sigma^2 estimated as 0.007142:  log likelihood = 231.39,  aic = -456.78
## 
## Training set error measures:
##                       ME       RMSE        MAE        MPE     MAPE     MASE
## Training set 0.002618444 0.08431708 0.06491399 0.08073421 4.252179 1.001022
##                      ACF1
## Training set -0.001067261
  • Por último, pero no menos importante, comprobaremos los residuos del modelo:
checkresiduals(robusta_md)

## 
##  Ljung-Box test
## 
## data:  Residuals from ARIMA(2,1,0)
## Q* = 26.584, df = 22, p-value = 0.2275
## 
## Model df: 2.   Total lags used: 24
  • En general, el gráfico de los residuos del modelo y la prueba de Ljung-Box indican que los residuos son white noise. El gráfico ACF indica que hay algunos rezagos correlacionados, pero ellos son solo significativos en los bordes, por lo que podemos ignorarlos.

  • Muchas pruebas estadísticas se utilizan para intentar rechazar alguna hipótesis nula. En este caso concreto, la prueba de Ljung-Box trata de rechazar la hipótesis nula de independencia de los residuos

    • Si el valor \(p\leq0.05\): Puede rechazar la hipótesis nula asumiendo un 5% de probabilidad de error. Por lo tanto, puede asumir que sus valores muestran dependencia entre sí.

    • Si el valor \(p > 0.05\): No tiene suficiente evidencia estadística para rechazar la hipótesis nula. Así que no puede asumir que sus valores son dependientes.

  • Esto podría significar que sus valores son dependientes de todos modos o puede significar que sus valores son independientes. Pero usted no está probando ninguna posibilidad específica, lo que su prueba realmente es que usted no puede afirmar la dependencia de los valores, ni puede afirmar la independencia de los valores.

Predicción del consumo mensual de gas natural en EE.UU

  • El modelo ARIMA estacional (SARIMA), como su nombre indica, es una versión designada del modelo ARIMA para series temporales con un componente estacional. Una serie temporal con un componente estacional tiene una fuerte relación con sus rezagos estacionales.

  • El modelo SARIMA utiliza los rezagos estacionales de manera similar a como lo hace el modelo ARIMA, esto es, utiliza los rezagos no estacionales con los procesos AR y MA y la diferenciación. Para ello, añade los tres componentes siguientes al modelo ARIMA

    • Proceso SAR(P): Un proceso AR estacional de la serie con sus P rezagos estacionales pasados. Por ejemplo, un SAR(2) es un proceso AR de la serie con sus dos últimos rezagos estacionales, es decir, \(Y_{t}=c+\Phi_{1}Y_{t-f}+\Phi_{2}Y_{t-2f}+\epsilon_{t}\) donde \(\Phi\) representa el coeficiente estacional del proceso SAR, y \(f\) representa la frecuencia de la serie.

    • Proceso SMA(Q): Un proceso MA estacional de la serie con sus Q términos de error estacionales pasados. Por ejemplo, un SMA(1) es un proceso de media móvil de la serie con su término de error estacional pasado, es decir, \(Y_{t}=\mu+\epsilon_{t}+\Theta_{1}\epsilon_{t-f}\), donde \(\Theta\) representa el coeficiente estacional del proceso SMA, y \(f\) representa la frecuencia de la serie.

    • Proceso SI(D): Una diferenciación estacional de la serie con sus últimos D rezagos estacionales. De forma similar, podemos diferenciar la serie con su rezago estacional, es decir \(Y_{D=1}^{'}=Y_{t}-Y_{t-f}\).

  • Utilizamos la siguiente notación para denotar los parámetros SARIMA, donde los parámetros \(P\) y \(Q\) representan los ordenes correspondientes de los procesos AR y MA estacionales de la serie con sus rezagos estacionales, y \(D\) define el grado de diferenciación de la serie con sus rezagos estacionales.

\[ \textsf{SARIMA}(p, d, q)\times(P, D, Q), \]

  • Aplicaremos lo aprendido acerca del modelo ARIMA para pronosticar US Monthly Natural Gas consumption. Vamos a cargar la serie USgas del paquete TSstudio:
library(TSstudio)
data(USgas)

ts_plot(USgas,
        title = "US Monthly Natural Gas consumption",
        Ytitle = "Billion Cubic Feet",
        Xtitle = "Year")
  • La serie US Monthly Natural Gas consumption tiene un fuerte patrón estacional, además la serie tiene una tendencia al alza, por lo tanto, se requiere el uso de los modelos SARIMA. Puede usar las funciones: ts_info(USgas) y ts_decompose(USgas) para identificar tendencia, estacionalidad e información importantes relacionada con la serie de tiempo.

  • Empezaremos por definir particiones training y testing usando la función ts_split, dejando los últimos 12 meses de la serie como partición de prueba:

ts_info(USgas)
##  The USgas series is a ts object with 1 variable and 238 observations
##  Frequency: 12 
##  Start time: 2000 1 
##  End time: 2019 10
ts_decompose(USgas)
USgas_split <- ts_split(USgas, sample.out = 12)

train <- USgas_split$train
test <- USgas_split$test
  • Antes de iniciar el proceso de entrenamiento del modelo SARIMA, realizaremos diagnósticos con respecto a la correlación de la serie con las funciones ACF y PACF. Dado que nos interesa ver la relación de la serie con sus rezagos estacionales, aumentaremos el número de rezagos a calcular y mostrar, estableciendo el argumento lag.max a 60 rezagos
par(mfrow=c(1,2))
acf(train, lag.max = 60)
pacf(train, lag.max = 60)

  • El gráfico ACF anterior indica que la serie tiene una fuerte correlación tanto con la parte estacional como con los rezagos no estacionales. Además, el decaimiento lineal de los rezagos estacionales indica que la serie no es estacionaria y que se requiere una diferenciación estacional.

  • Comenzaremos con diferenciación estacional de la serie y trazaremos el resultado para identificar si la serie se encuentra en un estado estacionario:

USgas_d12 <- diff(train, 12)

ts_plot(USgas_d12,
        title = "US Monthly Natural Gas consumption - First Seasonal Difference",
        Ytitle = "Billion Cubic Feet (First Difference)",
        Xtitle = "Year")
  • Aunque hemos eliminado la tendencia de la serie, nótese que la variación de la serie aún no es estable. Por lo tanto, también intentaremos tomar la primera diferencia de la serie:
USgas_d12_1 <- diff(diff(USgas_d12, 1))

ts_plot(USgas_d12_1,
        title = "US Monthly Natural Gas consumption - First Seasonal and Non-Seasonal Differencing",
        Ytitle = "Billion Cubic Feet (Difference)",
        Xtitle = "Year")
  • Después de tomar la diferenciación de primer orden, junto con la diferenciación estacional de primer orden, la serie parece estabilizarse en torno a la línea del eje \(x\) cero (o bastante cerca de ser estable). Después de transformar la serie en un estado estacionario, podemos revisar las funciones ACF y PACF de nuevo para identificar el proceso necesario
par(mfrow=c(1,2))
acf(USgas_d12_1, lag.max = 60)
pacf(USgas_d12_1, lag.max = 60)

  • La principal observación de los gráficos ACF y PACF anteriores es que tanto los rezagos no estacionales como los rezagos estacionales (en ambos gráficos) se están reduciendo. Por lo tanto, podemos concluir que después de diferenciar las series y transformarlas en un estado estacionario, deberíamos aplicar un proceso proceso ARMA para los componentes estacionales y no estacionales del modelo SARIMA
ACF PACF
AR Decaimiento geométrico Significativo hasta \(p\) rezagos
MA Significativo hasta \(p\) rezagos Decaimiento geométrico
ARMA Decaimiento geométrico Decaimiento geométrico

La función auto.arima

  • Uno de los principales retos de la previsión con la familia de modelos ARIMA es el proceso de ajuste de los modelos. Como hemos visto en este capítulo, este proceso incluye muchos pasos manuales que se requieren para verificar la estructura de las series (estacionarias o no estacionarias), las transformaciones de los datos, el análisis descriptivo con los gráficos ACF y PACF para identificar el tipo de proceso y, finalmente, afinar los parámetros del modelo.

  • Si bien entrenar un modelo ARIMA para una sola serie puede llevar unos minutos, es posible que no se pueda hacer si se tienen docenas de series que pronosticar. La función auto.arima del paquete forecast ofrece una solución a este problema. Este algoritmo automatiza el proceso de ajuste del modelo ARIMA con el uso de métodos para identificar tanto la estructura de la serie (estacionaria o no) como el tipo (estacional o no), y establece los parámetros del modelo en consecuencia. Por ejemplo, podemos utilizar la función para pronosticar USgas:

library(forecast)

USgas_auto_md <- auto.arima(train)
USgas_auto_md
## Series: train 
## ARIMA(2,1,1)(2,1,1)[12] 
## 
## Coefficients:
##          ar1      ar2      ma1    sar1     sar2     sma1
##       0.4301  -0.0372  -0.9098  0.0117  -0.2673  -0.7431
## s.e.  0.0794   0.0741   0.0452  0.0887   0.0830   0.0751
## 
## sigma^2 = 10446:  log likelihood = -1292.83
## AIC=2599.67   AICc=2600.22   BIC=2623.2
  • Utilizando los argumentos por defecto de la función auto.arima se obtiene el modelo ARIMA que minimice la puntuación AIC. En este caso, se seleccionó un modelo con una puntuación AIC de 2599.67.

  • Los valores de s.e corresponden al error estándar de los parámetros AR/MA estimados. Además, sigma^2 es la varianza de los valores residuales \(\sigma^{2}(\varepsilon_{t})\).

  • Por defecto, la función auto.arima aplica una búsqueda de modelos más corta utilizando un enfoque por pasos para reducir el tiempo de búsqueda. La contrapartida de este enfoque es que el modelo puede pasar por alto algunos modelos que pueden obtener mejores resultados.

  • Podemos improvisar con los resultados de auto.arima ajustando el argumento de búsqueda del modelo. El argumento step-wise, cuando se establece en FALSE, permite establecer una búsqueda más robusta y exhaustiva, con el coste de un mayor tiempo de búsqueda.

  • Esta compensación entre el rendimiento y el tiempo de cálculo puede equilibrarse siempre que se tenga un conocimiento previo de la estructura y las características de la serie. Por ejemplo, volvamos a entrenar el conjunto de entrenamiento de la serie USgas esta vez con la siguiente configuración:

    • Establezca los parámetros de diferenciación d y D en 1.
    • Limite el orden del modelo a siete utilizando el argumento max.order. El argumento max.order define los valores máximos de \(p+q+P+Q\), por lo que deberíamos fijarlo en cinco (dado que \(d\) y \(D\) están fijados en 1).
    • Bajo estas restricciones, busque todas las combinaciones posibles estableciendo el argumento argumento stepwise a FALSE.
    • Establezca el argumento aproximation en FALSE para obtener cálculos más precisos de los criterios de información:
USgas_auto_md2 <- auto.arima(train,
                             max.order = 5,
                             D = 1,
                             d = 1,
                             stepwise = FALSE,
                             approximation = FALSE)
USgas_auto_md2
## Series: train 
## ARIMA(1,1,1)(2,1,1)[12] 
## 
## Coefficients:
##          ar1      ma1    sar1     sar2     sma1
##       0.4247  -0.9180  0.0132  -0.2639  -0.7449
## s.e.  0.0770   0.0376  0.0894   0.0834   0.0753
## 
## sigma^2 = 10405:  log likelihood = -1292.96
## AIC=2597.91   AICc=2598.32   BIC=2618.08
  • Utilicemos el modelo entrenado USgas_best_md2 para pronosticar las observaciones correspondientes del del conjunto testing:
USgas_test_fc <- forecast(USgas_auto_md2, h = 12)
  • Evaluamos el rendimiento del modelo con la función accuracy:
accuracy(USgas_test_fc, test)
##                     ME      RMSE      MAE       MPE     MAPE      MASE
## Training set  6.081099  97.85701 73.36854 0.1298714 3.517097 0.6371821
## Test set     42.211253 104.79281 83.09943 1.4913412 3.314280 0.7216918
##                      ACF1 Theil's U
## Training set  0.004565602        NA
## Test set     -0.049999868 0.3469228
  • Ahora, utilizaremos la función test_forecast para obtener una visión más intuitiva del rendimiento del modelo del modelo en las particiones de entrenamiento y de prueba:
test_forecast(USgas,
              forecast.obj = USgas_test_fc,
              test = test)
  • Dado que se han cumplido las condiciones anteriores, podemos pasar al último paso del proceso de predicción y generar la predicción final con el modelo seleccionado. Empezaremos por reentrenando el modelo seleccionado en toda la serie:
final_md <- arima(USgas, order = c(1,1,1), 
                  seasonal = list(order = c(2,1,1)))
  • Antes de pronosticar los próximos 12 meses, verifiquemos que los residuos del modelo satisfacen la condición del modelo:
checkresiduals(final_md)

## 
##  Ljung-Box test
## 
## data:  Residuals from ARIMA(1,1,1)(2,1,1)[12]
## Q* = 30.173, df = 19, p-value = 0.04964
## 
## Model df: 5.   Total lags used: 24
  • Observando el gráfico de residuos anterior, se puede ver que residuos son white noise y se distribuyen normalmente. Además, la prueba de Ljung-Box confirma que no hay autocorrelación con un valor \(p\) de 0.04964, por lo tanto no podemos rechazar la hipótesis nula de que los residuos son white noise. Utilicemos la función de predicción para pronosticar los próximos 12 meses de la serie USgas:
USgas_fc <- forecast(final_md, h = 12)
  • Podemos trazar la predicción con la función plot_forecast:
plot_forecast(USgas_fc,
              title = "US Natural Gas Consumption - Forecast",
              Ytitle = "Billion Cubic Feet",
              Xtitle = "Year")
  • Para evitar el problema de búsqueda de modelos más corta utilizada por el modelo auto.arima, la cual puede pasar por alto algunos modelos que pueden obtener mejores resultados, a continuación definimos una función la cual encuentra el mejor modelo ARIMA con base en la minimización del coeficiente de Akaike (AIC) para distintos rangos de los parámetros \(p, d, q\) del modelo, para obtener el mejor modelo en términos de bondad de ajuste.
best_ARIMA <- function(ts_in, p_n, d_n, q_n) {
  best_aic <- Inf
  best_pdq <- NULL
  best_PDQ <- NULL
  fit <- NULL
  for(p in 1:p_n) {
    for(d in 1:d_n) {
      for (q in 1:q_n) {
        for(P in 1:p_n) {
          for(D in 1:d_n) {
            for (Q in 1:q_n) {
              tryCatch({
                fit <- arima(scale(ts_in), 
                             order=c(p, d, q), 
                             seasonal = list(order = c(P, D, Q), period = 12),
                             xreg=1:length(ts_in), 
                             method="CSS-ML")
                tmp_aic <- AIC(fit)
                if (tmp_aic < best_aic) {
                  best_aic <- tmp_aic
                  best_pdq = c(p, d, q)
                  best_PDQ = c(P, D, Q)
                }
              }, error=function(e){})
            }
          }
        }
      }
    }
  }
  return(list("best_aic" = best_aic, "best_pdq" = best_pdq, "best_PDQ" = best_PDQ))
}
  • Aquí CSS minimiza la suma de los residuos al cuadrado, ML maximiza la función log-verosimilitud del modelo ARIMA y CSS-ML combina ambos métodos. CSS es el más rápido pero menos preciso, MLE es el más lento pero el más preciso; CSS-MLE es un híbrido (medio rápido, medio preciso).

  • Utilizamos la función creada (best_ARIMA), para encontrar el mejor modelo ARIMA, utilizando por ejemplo, un rango de 0 a 2 para todos los seis parámetros del modelo. Puede probar con un rango mas amplio el rango para los valores \(p, d, q\).

best_model <- NULL
if(file.exists("best_arima.rda")) {
  best_model = readRDS("best_arima.rda")
} else {
  best_model = best_ARIMA(train, 2, 2, 2)
  saveRDS(best_model, file = "best_arima.rda")
}
  • Reconstruimos el modelo con base en los mejores parámetros encontrados
fit_model <- arima(train, order=best_model$best_pdq, seasonal = list(order = best_model$best_PDQ, period = 12))
fit_model
## 
## Call:
## arima(x = train, order = best_model$best_pdq, seasonal = list(order = best_model$best_PDQ, 
##     period = 12))
## 
## Coefficients:
##          ar1      ma1    sar1     sar2     sma1
##       0.4247  -0.9180  0.0132  -0.2639  -0.7449
## s.e.  0.0770   0.0376  0.0894   0.0834   0.0753
## 
## sigma^2 estimated as 10160:  log likelihood = -1292.96,  aic = 2597.91
  • Nótese que el modelo encontrado por medio del loop implementado, coincide con el obtenido utilizando la librería auto.arima. Esto sucede debido a que la metodología de selección de modelos es la misma y además, el rango para cada uno de los parámetros componentes, lo hemos limitado a dos.
USgas_pred <- forecast(fit_model, h = 12)
  • Verificamos los supuestos de normalidad e independencia de los residuos obenidos a partir de nuestro modelo.
checkresiduals(fit_model)

## 
##  Ljung-Box test
## 
## data:  Residuals from ARIMA(1,1,1)(2,1,1)[12]
## Q* = 25.336, df = 19, p-value = 0.1498
## 
## Model df: 5.   Total lags used: 24
  • Los resultados entregados por medio de la función checkresiduals muestran que los residuos se distribuyen de una forma normal aproximada y además son independedientes. Seguidamente, procedemos a realizar el gráfico con las predicciones obtenidas por medio del modelo encontrado.
plot_forecast(USgas_pred,
              title = "US Natural Gas Consumption - Forecast",
              Ytitle = "Billion Cubic Feet",
              Xtitle = "Year")
  • Con base en las predicciones para un horizonte de 12 días, calculamos métricas de precisión para el modelo obtenido.
accuracy(USgas_pred, test)
##                     ME      RMSE      MAE       MPE     MAPE      MASE
## Training set  6.081099  97.85701 73.36854 0.1298714 3.517097 0.6371821
## Test set     42.211253 104.79281 83.09943 1.4913412 3.314280 0.7216918
##                      ACF1 Theil's U
## Training set  0.004565602        NA
## Test set     -0.049999868 0.3469228
  • Utilizamos nuevamente la función test_forecast para visualizar de forma dinámica el ajuste del modelos y las respectivas predicciones.
test_forecast(USgas,
              forecast.obj = USgas_test_fc,
              test = test)

Ejercicio para entregar

  1. Considere la serie de tiempo asociada con las acciones de Tecnoglass desde que comenzó a comercializarse hasta la fecha del día de hoy. Puede utilizar la API de Yahoo Finance para obtener esta serie de tiempo (ver yahoofinancer).

  2. Repita TODOS los pasos indicados en esta sección para encontrar modelos ARIMA para predecir el precio de las acciones de Tecnoglass con los siguientes horizontes: 7, 14 días, 21 días, 28 días. Utilizar siempre predicciones usando rolling con ventana de predicción continua de un día. Cualquier cantidad de pasos extra para enriquecer su análisis predictivo serán aceptados siempre y cuando sean acordes con lo que indica la teoría de análisis de series de tiempo.

  1. Repita el paso 2 ahora sin utilizar rolling. Esto es, realice el pronóstico solo utilizando forecast() para los diferentes horizontes de predicción, 7, 14 días, 21 días, 28 días.

  2. Realice tablas de error para los ítems 1 y 2, utilizando las métricas: MAPE, MAE, RMSE, MSE, R2. Además, agregue el gráfico de correlación entre la observación real y su predicción en el test, \(\text{Corr}(y_{t}, \tilde{y}_{t})\).

  3. Repita el análisis desarrollado en los pasos anteriores, considerando ahora el criterio de inferencia Bayesiana (BIC) y el criterio de información de Hannan–Quinn (HQIC) para encontrar el mejor modelo ARIMA y, compare los errores con aquellos obtenidos con el criterio de Akaike.

  4. Escriba en cada paso las conclusiones y análisis estadísticos asociados con los resultados obtenidos. Realice tests de normalidad e independencia para los residuales obtenidos para cada predicción, en cada caso agregue las correspondientes conclusiones. Figuras y algoritmos que no estén acompañados de una conclusión, descripción y análisis estadístico, no serán tenidas en cuenta.

Introducción a PostgreSQL en R

  • Cuando se trata de grandes conjuntos de datos que potencialmente exceden la memoria de su máquina, es bueno tener otra posibilidad, como su propio servidor con una base de datos SQL/PostgreSQL, donde se puede consultar los datos. Por ejemplo, un conjunto de datos financieros de 5 GB caben en una memoria RAM básica, pero los datos consumen muchos recursos. Una solución es utilizar una base de datos basada en SQL, donde puedo consultar los datos en trozos más pequeños, dejando recursos para el cálculo.

  • Aunque MySQL es la más utilizada, PostgreSQL tiene la ventaja de ser de código abierto y gratuita para todos los usos. Sin embargo, todavía tenemos que conseguir un servidor. Una forma posible de hacerlo es alquilar un servidor de Amazon, sin embargo, existe la opción de utilizar Render tal como se explicó en la sección de Python.

Conexión con R

  • Ahora es el momento de conectarse a la base de datos con R. Este enfoque utiliza el paquete RPostgreSQL. Los siguientes paquetes y herramientas deben ser instalados para poder hacer uso de la API y realizar la conexión con éxito

    • RPostgresql
    • DBI
    • Su propia base de datos PostgreSQL
    • Acceso remoto a su base de datos
  • El paquete RPostgreSQL y DBI se puede instalar desde CRAN o Github

install.packages("RPostgreSQL")
install.packages("DBI")
devtools::install_git('https://github.com/r-dbi/DBI.git')
devtools::install_git('https://github.com/cran/RPostgreSQL.git)
  • Para conectar, necesitamos introducir los siguientes comandos en R, nos conectaremos a la base de datos creada anteriormente en Docker en la sección de Python. RPostgres es una interfaz compatible con DBI para la base de datos Postgres. Es una reescritura desde cero usando C++ y Rcpp.

  • Este paquete actúa tanto como el controlador de la base de datos como la interfaz DBI. El código y la información adicional están disponibles en su repositorio GitHub aquí: RPostgres. Debe modificar la información que recibe la funcion dbConnect() por aquella que obtuvo al crear su base de datos en Docker

library(DBI)
library(RPostgreSQL)

con <- dbConnect(RPostgres::Postgres(), 
                dbname = "undatascience-db", 
                host = "localhost", 
                port = 5432, 
                user = "undatascience-user", 
                password = "undatascience-password")
  • Para visualizar la lista de tablas que hemos importado antes en la base de datos utilizamos dbListTables()
dbListTables(conn = con)
## [1] "company"    "tecnoglass"

Importación de tablas

  • Carguemos como dataframe un archivo CSV que contiene los precios de las acciones de Tecnoglass Inc. (TGLS), empresa Barranquillera que cotiza en Nasdaq. Para esto usamos la función read.csv().
TGLS_df <- read.csv(file = 'C:/Data/TGLS.csv')
head(TGLS_df)
##         Date Open  High  Low Close Adj.Close Volume
## 1 2012-05-10 9.97 10.00 9.50  9.80  7.945550   6900
## 2 2012-05-11 9.70  9.70 9.70  9.70  7.864471    300
## 3 2012-05-14 9.80  9.80 9.80  9.80  7.945550    100
## 4 2012-05-15 9.75  9.75 9.75  9.75  7.905012    300
## 5 2012-05-16 9.75  9.75 9.75  9.75  7.905012      0
## 6 2012-05-17 9.60  9.60 9.60  9.60  7.783393    800
  • Los nombres de las columnas de este dataframe son problemáticos para las bases de datos (y especialmente para PostgreSQL) por varias razones: los "." en los nombres pueden ser un problema, PostgreSQL espera que los nombres de las columnas estén todos en minúsculas.

  • Aquí hay una función para hacer que los nombres de las columnas sean seguros para las bases de datos (ver Dealing with Regular Expressions). Luego obtenemos algunos estadísticos usando la función summary()

dbSafeNames = function(names) {
  names = gsub('[^a-z0-9]+','_',tolower(names))
  names = make.names(names, unique=TRUE, allow_=TRUE)
  names = gsub('.','_',names, fixed=TRUE)
  names
}
colnames(TGLS_df) = dbSafeNames(colnames(TGLS_df))
head(TGLS_df)
##         date open  high  low close adj_close volume
## 1 2012-05-10 9.97 10.00 9.50  9.80  7.945550   6900
## 2 2012-05-11 9.70  9.70 9.70  9.70  7.864471    300
## 3 2012-05-14 9.80  9.80 9.80  9.80  7.945550    100
## 4 2012-05-15 9.75  9.75 9.75  9.75  7.905012    300
## 5 2012-05-16 9.75  9.75 9.75  9.75  7.905012      0
## 6 2012-05-17 9.60  9.60 9.60  9.60  7.783393    800
summary(TGLS_df)
##      date                open             high             low        
##  Length:2200        Min.   : 2.440   Min.   : 2.540   Min.   : 2.150  
##  Class :character   1st Qu.: 7.987   1st Qu.: 8.117   1st Qu.: 7.900  
##  Mode  :character   Median : 9.840   Median : 9.870   Median : 9.800  
##                     Mean   : 9.493   Mean   : 9.614   Mean   : 9.351  
##                     3rd Qu.:10.940   3rd Qu.:11.045   3rd Qu.:10.790  
##                     Max.   :15.950   Max.   :15.950   Max.   :14.920  
##      close          adj_close          volume       
##  Min.   : 2.290   Min.   : 2.243   Min.   :      0  
##  1st Qu.: 7.960   1st Qu.: 7.497   1st Qu.:   2000  
##  Median : 9.840   Median : 8.067   Median :  15200  
##  Mean   : 9.477   Mean   : 8.060   Mean   :  31071  
##  3rd Qu.:10.910   3rd Qu.: 8.930   3rd Qu.:  38600  
##  Max.   :15.060   Max.   :12.210   Max.   :1439200
  • Ahora procedemos a insertar tabla TGLS_df en nuestra base de datos Docker usando la función dbWriteTable()
dbWriteTable(con, 'tecnoglass', TGLS_df, row.names=FALSE, overwrite=TRUE)
  • La función dbWriteTable() devuelve TRUE si la tabla fue escrita con éxito. Tenga en cuenta que esta llamada fallará si ya existe la tabla en la base de datos. Utilice overwrite = TRUE para forzar la sobreescritura de una tabla existente, y append = TRUE para añadirla a una tabla existente. Verifique que la tabla fué creada usando pgAdmin

  • Ahora puedes volver a leer la tabla usando dbGetQuery() o dbReadTable
dtab = dbGetQuery(con, "SELECT * FROM tecnoglass")
summary(dtab)
##      date                open             high             low        
##  Length:2200        Min.   : 2.440   Min.   : 2.540   Min.   : 2.150  
##  Class :character   1st Qu.: 7.987   1st Qu.: 8.117   1st Qu.: 7.900  
##  Mode  :character   Median : 9.840   Median : 9.870   Median : 9.800  
##                     Mean   : 9.493   Mean   : 9.614   Mean   : 9.351  
##                     3rd Qu.:10.940   3rd Qu.:11.045   3rd Qu.:10.790  
##                     Max.   :15.950   Max.   :15.950   Max.   :14.920  
##      close          adj_close          volume       
##  Min.   : 2.290   Min.   : 2.243   Min.   :      0  
##  1st Qu.: 7.960   1st Qu.: 7.497   1st Qu.:   2000  
##  Median : 9.840   Median : 8.067   Median :  15200  
##  Mean   : 9.477   Mean   : 8.060   Mean   :  31071  
##  3rd Qu.:10.910   3rd Qu.: 8.930   3rd Qu.:  38600  
##  Max.   :15.060   Max.   :12.210   Max.   :1439200
rm(dtab)
dtab = dbReadTable(con, "tecnoglass")
summary(dtab)
##      date                open             high             low        
##  Length:2200        Min.   : 2.440   Min.   : 2.540   Min.   : 2.150  
##  Class :character   1st Qu.: 7.987   1st Qu.: 8.117   1st Qu.: 7.900  
##  Mode  :character   Median : 9.840   Median : 9.870   Median : 9.800  
##                     Mean   : 9.493   Mean   : 9.614   Mean   : 9.351  
##                     3rd Qu.:10.940   3rd Qu.:11.045   3rd Qu.:10.790  
##                     Max.   :15.950   Max.   :15.950   Max.   :14.920  
##      close          adj_close          volume       
##  Min.   : 2.290   Min.   : 2.243   Min.   :      0  
##  1st Qu.: 7.960   1st Qu.: 7.497   1st Qu.:   2000  
##  Median : 9.840   Median : 8.067   Median :  15200  
##  Mean   : 9.477   Mean   : 8.060   Mean   :  31071  
##  3rd Qu.:10.910   3rd Qu.: 8.930   3rd Qu.:  38600  
##  Max.   :15.060   Max.   :12.210   Max.   :1439200
  • Por supuesto, el objetivo de utilizar una base de datos es extraer subconjuntos o transformaciones de sus datos, utilizando SQL. Para esto procedemos de la siguiente forma
rm(dtab)
dtab = dbGetQuery(con, "SELECT date, open, close, high, low FROM tecnoglass")
head(dtab)
##         date open close  high  low
## 1 2012-05-10 9.97  9.80 10.00 9.50
## 2 2012-05-11 9.70  9.70  9.70 9.70
## 3 2012-05-14 9.80  9.80  9.80 9.80
## 4 2012-05-15 9.75  9.75  9.75 9.75
## 5 2012-05-16 9.75  9.75  9.75 9.75
## 6 2012-05-17 9.60  9.60  9.60 9.60
  • Puedes utilizar dbSendQuery para enviar consultas que no devuelven un resultado tipo marco de datos. Luego asegúrese de enviar los cambios a la base de datos usando dbCommit
dbBegin(con)
dbSendQuery(con, "DROP TABLE IF EXISTS tecnoglass")
dbCommit(con)
  • Cuando hayas terminado, desconecta
dbDisconnect(con)

PostgreSQL Neon Server

  • Neon es un servidor de base de datos PostgreSQL sin servidor totalmente gestionado con un generoso nivel gratuito. Ofrece características modernas para desarrolladores, como escalabilidad automática, ramificación, almacenamiento ilimitado y más. Neon se destaca por su alta disponibilidad, su almacenamiento específico para la nube y su compatibilidad con las últimas versiones de PostgreSQL. Es de código abierto, escrito en Rust, y se integra con sistemas de almacenamiento en la nube como S3 para ofrecer un rendimiento óptimo y una reducción significativa de costos.

  • Para usar Neon debe crear inicialmente una cuenta en el sitio web neon.tech. Posteriormente, elija el nombre del proyecto, versión de PostgreSQL, así como también la versión de la base de datos a utilizar. A la derecha podrá visualizar las capacidades de la base de datos en su versión Free Tier. Haga click en Create Project

  • Posterior a esto, podrá visualizar la información acerca de credenciales para poder realizar su conexión a la base de datos. Hacemos click en Parameters only, para poder visualizar las credenciales que nos permitirán realizar la conexión a la base de datos desde R por ejemplo. Guarde la información y haga click en I’ll do this later

  • Ahora podrá conectarse a la base de datos creada en Neon desde R usando las librerías DBI y RPostgreSQL. Además de escribir sobre esta base de datos la tabla tecnoglass como en el ejercicio anterior
library(DBI)
library(RPostgreSQL)

con <- dbConnect(RPostgres::Postgres(), 
                dbname = "datavizuninortedb", 
                host = "ep-dawn-dew-a5bt3cti.us-east-2.aws.neon.tech", 
                port = 5432, 
                user = "datavizuninortedb_owner", 
                password = "8CTM7rcdXlgG")
dbWriteTable(con, 'tecnoglass', TGLS_df, row.names=FALSE, overwrite=TRUE)
rm(dtab)
dtab = dbGetQuery(con, "SELECT date, open, close, high, low FROM tecnoglass")
knitr::kable(head(dtab)) 
date open close high low
2012-05-10 9.97 9.80 10.00 9.50
2012-05-11 9.70 9.70 9.70 9.70
2012-05-14 9.80 9.80 9.80 9.80
2012-05-15 9.75 9.75 9.75 9.75
2012-05-16 9.75 9.75 9.75 9.75
2012-05-17 9.60 9.60 9.60 9.60
rm(dtab)
dtab = dbReadTable(con, "tecnoglass")
summary(dtab)
##      date                open             high             low        
##  Length:2200        Min.   : 2.440   Min.   : 2.540   Min.   : 2.150  
##  Class :character   1st Qu.: 7.987   1st Qu.: 8.117   1st Qu.: 7.900  
##  Mode  :character   Median : 9.840   Median : 9.870   Median : 9.800  
##                     Mean   : 9.493   Mean   : 9.614   Mean   : 9.351  
##                     3rd Qu.:10.940   3rd Qu.:11.045   3rd Qu.:10.790  
##                     Max.   :15.950   Max.   :15.950   Max.   :14.920  
##      close          adj_close          volume       
##  Min.   : 2.290   Min.   : 2.243   Min.   :      0  
##  1st Qu.: 7.960   1st Qu.: 7.497   1st Qu.:   2000  
##  Median : 9.840   Median : 8.067   Median :  15200  
##  Mean   : 9.477   Mean   : 8.060   Mean   :  31071  
##  3rd Qu.:10.910   3rd Qu.: 8.930   3rd Qu.:  38600  
##  Max.   :15.060   Max.   :12.210   Max.   :1439200
dbDisconnect(con)
  • Podrá visualizar desde su Neon Dashboard la tabla creada tecnoglass además de otras funcionalidades ofrecidas

Candlesticks plot

  • Usaremos los datos importados en la base de datos de Neon para realizar gráfico de velas japonesas usando plotly. Inciamos realizando la conexión a nuestra base de datos en Neon, luego realizamos la figura utilizando la función plot_ly() usando type="candlestick"
library(DBI)
library(RPostgreSQL)

con <- dbConnect(RPostgres::Postgres(), 
                dbname = "datavizuninortedb", 
                host = "ep-dawn-dew-a5bt3cti.us-east-2.aws.neon.tech", 
                port = 5432, 
                user = "datavizuninortedb_owner", 
                password = "8CTM7rcdXlgG")
dtab = dbGetQuery(con, "SELECT * FROM tecnoglass")
head(dtab)
##         date open  high  low close adj_close volume
## 1 2012-05-10 9.97 10.00 9.50  9.80  7.945550   6900
## 2 2012-05-11 9.70  9.70 9.70  9.70  7.864471    300
## 3 2012-05-14 9.80  9.80 9.80  9.80  7.945550    100
## 4 2012-05-15 9.75  9.75 9.75  9.75  7.905012    300
## 5 2012-05-16 9.75  9.75 9.75  9.75  7.905012      0
## 6 2012-05-17 9.60  9.60 9.60  9.60  7.783393    800
library(plotly)


fig <- dtab %>% plot_ly(x = ~date, type="candlestick",
                        open = ~open, close = ~close,
                        high = ~high, low = ~low) 
fig <- fig %>% layout(title = "Basic Candlestick Chart",
                      xaxis = list(title = 'Day'),
                      yaxis = list(title = 'TGLS-USD'))
fig
i <- list(line = list(color = '#FFD700'))
d <- list(line = list(color = '#0000ff'))

fig <- dtab %>% plot_ly(x = ~date, type="ohlc",
                        open = ~open, close = ~close,
                        high = ~high, low = ~low,
                        increasing = i, decreasing = d) 
fig <- fig %>% layout(title = "Basic OHLC Chart",
                      xaxis = list(rangeslider = list(visible = F), title = 'Day'),
                      yaxis = list(title = 'TGLS-USD'))

fig
dbDisconnect(con)

Ejercicio para entregar

  • Cada una de las consultas SQL estudiadas en la sección de Python se pueden realizar desde R. La diferencia central se encuentra en la forma como se usa la API de PostgreSQL para R, a saber RPostgreSQL, pero, las consultas SQL se mantienen iguales.

  • A manera de ejercicio, en esta tarea se propone dar solución a los problemas PostgreSQL Exercises, haciendo uso de Docker, Neon y la librería RPostgreSQL. Los ejercicios que se proponen resolver, serán un complemento a las consultas estudiadas en la sección de Python y R. Son de gran importancia en su tarea con Científico de Datos. Siga los siguientes pasos

    • Usando Docker, Neon y RPostgreSQL crear las tablas que aparecen el el siguiente link gettingstarted. Para descargar los datos a insertar en las tablas bookings, facilities, members, debe usar las consultas INSERT INTO que aparece en el siguiente link pgexercises.sql.

    • Resuelva los ejercicios que aparecen en el siguiente link: questions. Esto es, debe resolver los ejercicios de las secciones:

      • Simple SQL Queries
      • Joins and Subqueries
      • Modifying data
      • Aggregation
      • Working with Timestamps
      • String Operations
      • Recursive Queries
  • Al final, como todas las tareas del curso, debe entregar un link de RPubs/Quarto con la solución a estos ejercicios y un ZIP con la base de datos creada y los archivos fuente, así como screenshots de las tablas creadas en Neon si estas no superan 0.5 GB.

Visualización con plotly y shiny

Introducción a plotly

  • Cualquier gráfico hecho con el paquete R de plotly es impulsado por la biblioteca JavaScript plotly.js. La función plot_ly() proporciona una interfaz ‘directa’ a plotly.js con algunas abstracciones adicionales para ayudar a reducir la escritura. Estas abstracciones, inspiradas en la gramática de los gráficos y ggplot2, hacen que sea mucho más rápido iterar de un gráfico a otro, facilitando el descubrimiento de características interesantes en los datos (Wilkinson 2005; Wickham 2009). Para demostrarlo, usaremos plot_ly() para explorar el conjunto de datos diamonds de ggplot2 y aprenderemos un poco cómo funciona plotly
library(plotly)
library(ggplot2)

data(diamonds, package = "ggplot2")
knitr::kable(head(diamonds))
carat cut color clarity depth table price x y z
0.23 Ideal E SI2 61.5 55 326 3.95 3.98 2.43
0.21 Premium E SI1 59.8 61 326 3.89 3.84 2.31
0.23 Good E VS1 56.9 65 327 4.05 4.07 2.31
0.29 Premium I VS2 62.4 58 334 4.20 4.23 2.63
0.31 Good J SI2 63.3 58 335 4.34 4.35 2.75
0.24 Very Good J VVS2 62.8 57 336 3.94 3.96 2.48
  • La siguiente es una descripción de cada una de las variables en diamonds

  • Si asignamos nombres de variables (p. ej., cut, clarity, etc.) a propiedades visuales (p. ej., x, y, color, etc.) dentro de plot_ly(), como se ve en la figura, se intenta encontrar una representación geométrica sensata de esa información por nosotros. En breve veremos cómo especificar estas representaciones geométricas (así como otras codificaciones visuales) para crear diferentes tipos de gráficos.
plot_ly(diamonds, x = ~cut)
plot_ly(diamonds, x = ~cut, y = ~clarity)
plot_ly(diamonds, x = ~cut, color = ~clarity, colors = "Accent")
  • La función plot_ly() tiene numerosos argumentos que son exclusivos del paquete R (por ejemplo, color, stroke, span, symbol, linetype, etc.) y facilitan la codificación de las variables de datos (por ejemplo, claridad del diamante) como propiedades visuales (por ejemplo, color). Por defecto, estos argumentos asignan valores de una variable de datos a un rango visual definido por la forma plural del argumento.

Introducción a ggplotly

  • La función ggplotly() del paquete plotly tiene la capacidad de traducir ggplot2 a plotly. Esta funcionalidad puede ser realmente útil para añadir rápidamente interactividad a su flujo de trabajo ggplot2 existente. Además, incluso si conoce bien plot_ly() y ggplotly() puede seguir siendo deseable para crear visualizaciones que no son necesariamente fáciles de lograr sin ella. Para demostrarlo, vamos a explorar la relación entre price y otras variables del conocido conjunto de datos diamonds.

  • El binning hexagonal (es decir, geom_hex()) es una forma útil de visualizar una densidad 2D, como la relación entre price y carat, como se muestra en la siguiente figura, donde podemos ver que existe una fuerte relación lineal positiva entre el logaritmo de los quilates y el precio. También muestra que, para muchos, el quilate sólo se redondea a un número determinado, indicado por las bandas azul claro. Hacer este gráfico interactivo facilita la decodificación de los colores hexagonales en los recuentos que representan.

p <- ggplot(diamonds, aes(x = log(carat), y = log(price))) + 
  geom_hex(bins = 100)
ggplotly(p)
  • Es bueno utilizar ggplotly() sobre plot_ly() para aprovechar la interfaz consistente y expresiva de ggplot2 para explorar resúmenes estadísticos entre grupos. Por ejemplo, al incluir una variable de color discreta (e.g cut) con geom_freqpoly(), se obtiene un polígono de frecuencia para cada nivel de esa variable. Esta capacidad de generar rápidamente codificaciones visuales de resúmenes estadísticos a través de un número arbitrario de grupos funciona básicamente para cualquier geom (e.g, geom_boxplot(), geom_histogram(), geom_density(), etc) y es una característica clave de ggplot2.
p <- ggplot(diamonds, aes(x = log(price), color = clarity)) +
  geom_freqpoly()
ggplotly(p)

Barras e histogramas

  • Las funciones add_bars() y add_histogram() envuelven los tipos de trazado de barras e histogramas de plotly.js. La principal diferencia entre ellas es que las trazas de barras requieren alturas de barras (tanto x como y), mientras que las trazas de histograma requieren sólo una variable, y plotly.js maneja el binning en el navegador. Y quizás de forma confusa, ambas funciones pueden utilizarse para visualizar la distribución de una variable numérica o discreta. Así que, esencialmente, la única diferencia entre ellas es dónde se produce el binning.

  • La siguiente figura compara el algoritmo de agrupación por defecto en plotly.js con algunos algoritmos diferentes disponibles en R a través de la función hist(). Aunque plotly.js tiene la capacidad de personalizar los bins del histograma a través de xbins/ybins, R tiene diversas facilidades para estimar el número óptimo de bins en un histograma que podemos aprovechar fácilmente. La función hist() por sí sola nos permite referenciar 3 algoritmos famosos por su nombre (Sturges 1926; Freedman-Diaconis 1981 y Scott 1979), pero también hay paquetes (por ejemplo, el paquete histogram) que amplían esta interfaz para incorporar más metodología (Mildenberger, Rozenholc y Zasada. 2009). La función price_hist() que aparece a continuación envuelve la función hist() para obtener los resultados del binning, y mapear esos bins a una versión graficada del histograma usando add_bars()

p1 <- plot_ly(diamonds, x = ~price) %>%
  add_histogram(name = "plotly.js")

price_hist <- function(method = "FD") {
  h <- hist(diamonds$price, breaks = method, plot = FALSE)
  plot_ly(x = h$mids, y = h$counts) %>% add_bars(name = method)
}

subplot(
  p1, price_hist(), price_hist("Sturges"),  price_hist("Scott"),
  nrows = 4, shareX = TRUE
)
  • La siguiente figura muestra dos formas de crear un gráfico de barras básico. Aunque los resultados visuales son los mismos, vale la pena señalar la diferencia en la implementación. La función add_histogram() envía todos los valores observados al navegador y deja que plotly.js realice el binning. Se necesita más esfuerzo humano para realizar el binning en R, pero hacerlo tiene la ventaja de enviar menos datos, y requiere menos trabajo de cálculo del navegador web. En este caso, sólo tenemos unos 50.000 registros, por lo que no hay mucha diferencia en los tiempos de carga o el tamaño de la página. Sin embargo, con 1 millón de registros, el tiempo de carga de la página es más del doble y el tamaño de la página casi se duplica.
library(dplyr)
p1 <- plot_ly(diamonds, x = ~cut) %>%
  add_histogram()

p2 <- diamonds %>%
  count(cut) %>%
  plot_ly(x = ~cut, y = ~n) %>% 
  add_bars()

subplot(p1, p2) %>% hide_legend()
  • A menudo es útil ver cómo cambia la distribución numérica con respecto a una variable discreta. Cuando se utilizan barras para visualizar múltiples distribuciones numéricas, se recomienda trazar cada distribución en su propio eje utilizando una pantalla de múltiplos pequeños, en lugar de intentar superponerlas en un solo eje. Obsérvese cómo la función one_plot() define lo que debe mostrarse en cada panel, y luego se emplea una estrategia de dividir-aplicar-recombinar (es decir, split(), lapply(), subplot()) para generar la visualización del enrejado
one_plot <- function(d) {
  plot_ly(d, x = ~price) %>%
    add_annotations(
      ~unique(clarity), x = 0.5, y = 1, 
      xref = "paper", yref = "paper", showarrow = FALSE
    )
}

diamonds %>%
  split(.$clarity) %>%
  lapply(one_plot) %>% 
  subplot(nrows = 2, shareX = TRUE, titleX = FALSE) %>%
  hide_legend()
  • La visualización de las distribuciones discretas múltiples es difícil. Esta sutil complejidad se debe a que tanto los recuentos como las proporciones son importantes para comprender las distribuciones discretas multivariadas. La figura siguiente presenta los recuentos de diamantes, divididos por su talla y su claridad, mediante un gráfico de barras agrupadas.
plot_ly(diamonds, x = ~cut, color = ~clarity) %>%
  add_histogram()
  • La distribución de la claridad dentro de los diamantes “ideales” parece ser bastante similar a la de otros diamantes, pero es difícil hacer esta comparación utilizando los recuentos brutos. La siguiente figura facilita esta comparación mostrando la frecuencia relativa de los diamantes por claridad, dado un corte.
cc <- count(diamonds, cut, clarity) #counts for frequencies of combinations
cc2 <- left_join(cc, count(cc, cut, wt = n, name = 'nn')) # counts for cut frequencies in cc
str(cc2)
## tibble [40 × 4] (S3: tbl_df/tbl/data.frame)
##  $ cut    : Ord.factor w/ 5 levels "Fair"<"Good"<..: 1 1 1 1 1 1 1 1 2 2 ...
##  $ clarity: Ord.factor w/ 8 levels "I1"<"SI2"<"SI1"<..: 1 2 3 4 5 6 7 8 1 2 ...
##  $ n      : int [1:40] 210 466 408 261 170 69 17 9 96 1081 ...
##  $ nn     : int [1:40] 1610 1610 1610 1610 1610 1610 1610 1610 4906 4906 ...
cc2 %>%
  mutate(prop = n / nn) %>%
  plot_ly(x = ~cut, y = ~prop, color = ~clarity) %>%
  add_bars() %>%
  layout(barmode = "stack")

Boxplots

  • Los boxplots codifican el resumen de cinco números de una variable numérica, y proporcionan una forma decente de comparar muchas distribuciones numéricas. La tarea visual de comparar múltiples boxplots es relativamente fácil (es decir, comparar la posición a lo largo de una escala común) en comparación con algunas alternativas comunes (por ejemplo, una visualización enrejada de histogramas), pero el boxplot es a veces inadecuado para capturar distribuciones complejas (por ejemplo, multimodales) (en este caso, un polígono de frecuencia proporciona una buena alternativa). La función add_boxplot() requiere una variable numérica y garantiza que los boxplots se orienten correctamente, independientemente de si la variable numérica se coloca en la escala x o y. Como muestra la siguiente figura, en el eje ortogonal al eje numérico, puede proporcionar una variable discreta (para el condicionamiento) o suministrar un único valor (para nombrar la categoría del eje).
p <- plot_ly(diamonds, y = ~price, color = I("black"),
             alpha = 0.1, boxpoints = "suspectedoutliers")
p1 <- p %>% add_boxplot(x = "Overall")
p2 <- p %>% add_boxplot(x = ~cut)
subplot(
  p1, p2, shareY = TRUE,
  widths = c(0.2, 0.8), margin = 0
) %>% hide_legend()
  • Si desea realizar una partición por más de una variable discreta, podría utilizar la interacción de esas variables con el eje discreto, y colorear por la variable anidada, como hace la siguiente figura con la claridad y la talla del diamante. Otro enfoque sería utilizar una visualización enrejada
plot_ly(diamonds, x = ~price, y = ~interaction(clarity, cut)) %>%
  add_boxplot(color = ~clarity) %>%
  layout(yaxis = list(title = ""))
  • También es útil ordenar los gráficos de caja según algo significativo, como la mediana del precio. La siguiente figura presenta la misma información anterior, pero ordena los gráficos de caja por su mediana, y deja claro de inmediato que los diamantes con una talla "SI2" tienen el precio más alto, por término medio
d <- diamonds %>%
  mutate(cc = interaction(clarity, cut))

lvls <- d %>%
  group_by(cc) %>%
  summarise(m = median(price)) %>%
  arrange(m) %>%
  pull(cc)

plot_ly(d, x = ~price, y = ~factor(cc, lvls)) %>%
  add_boxplot(color = ~clarity) %>%
  layout(yaxis = list(title = ""))
  • Al igual que add_histogram(), add_boxplot() envía los datos brutos al navegador y permite a plotly.js calcular las estadísticas de resumen. Desafortunadamente, plotly.js todavía no permite calcular estadísticas para boxplots.

Mapas

  • Hay numerosas maneras de hacer un mapa con plotly, cada una con sus propios puntos fuertes y débiles. En general, los enfoques se dividen en dos categorías: integrados o personalizados. Los mapas integrados aprovechan el soporte integrado de plotly.js para renderizar una capa de mapa base. Actualmente hay dos formas de hacer mapas integrados: a través de Mapbox o a través de un mapa base integrado con d3.js. El enfoque integrado es conveniente si necesitas un mapa rápido y no necesariamente necesitas representaciones sofisticadas de objetos geoespaciales. Por otro lado, el enfoque de mapeo personalizado ofrece un control completo, ya que usted está proporcionando toda la información necesaria para representar el/los objetos geoespaciales. Mas adelante revisaremos la creación de mapas sofisticados (por ejemplo, cartogramas) utilizando el paquete sf R, pero también es posible hacer mapas personalizados de plotly a través de otras herramientas para la geoinformática (por ejemplo, sp, ggmap, etc).

  • Vale la pena señalar que plotly pretende ser una biblioteca de visualización de propósito general, por lo tanto, no pretende ser el conjunto de herramientas de visualización geoespacial más completo. Dicho esto, hay beneficios en el uso de mapas basados en plotly, ya que las APIs de mapeo son muy similares al resto de plotly, y se puede aprovechar el ecosistema más grande de plotly (por ejemplo, la vinculación de las vistas del lado del cliente). Sin embargo, si te encuentras con limitaciones con la funcionalidad de mapeo de plotly, hay un conjunto muy rico de herramientas para la visualización geoespacial interactiva en R, incluyendo pero no limitado a: leaflet, mapview, mapedit, tmap, y mapdeck.

Mapas integrados

  • Si tiene datos de latitud/longitud bastante simples y quiere hacer un mapa rápido, puede probar una de las opciones de mapeo integradas de plotly (es decir, plot_mapbox() y plot_geo()). En general, puede tratar estas funciones constructoras como un reemplazo directo de plot_ly() y obtener un mapa base dinámico representado detrás de sus datos. Además, todas las capas basadas en la dispersión funcionan como cabría esperar con plot_ly(). Por ejemplo, la siguiente figura utiliza plot_mapbox() y add_markers() para crear un gráfico de burbujas. Para poder usar plot_mapbox, primero debes crear un MAPBOX_TOKEN en el sitio web de Mapbox y luego agregar la siguiente línea a tu código.

head(maps::canada.cities)
##            name country.etc    pop   lat    long capital
## 1 Abbotsford BC          BC 157795 49.06 -122.30       0
## 2      Acton ON          ON   8308 43.63  -80.03       0
## 3 Acton Vale QC          QC   5153 45.63  -72.57       0
## 4    Airdrie AB          AB  25863 51.30 -114.02       0
## 5    Aklavik NT          NT    643 68.22 -135.00       0
## 6    Albanel QC          QC   1090 48.87  -72.42       0
library(plotly)
Sys.setenv('MAPBOX_TOKEN' = 'pk.eyJ1IjoibGloa2lyIiwiYSI6ImNrdXdtcWNyeTE4cngybm1wYzBpbjZrZzEifQ.gdBWHAZHRJQL265am4jHpg')

plot_mapbox(maps::canada.cities) %>%
  add_markers(
    x = ~long,
    y = ~lat,
    size = ~pop,
    color = ~country.etc,
    colors = "Accent",
    text = ~paste(name, pop),
    hoverinfo = "text"
  )
  • El estilo del mapa base de Mapbox se controla a través del atributo layout.mapbox.style. El paquete de plotly viene con soporte para 7 estilos diferentes, pero también puedes suministrar una URL personalizada a un estilo mapbox personalizado. Para obtener todos los nombres de estilo de mapa base pre-empaquetados, puedes tomarlos del esquema oficial plotly.js(). Antes debes instalar listviewer para que funcione la siguiente orden
styles <- schema()$layout$layoutAttributes$mapbox$style$values
styles
##  [1] "basic"             "streets"           "outdoors"         
##  [4] "light"             "dark"              "satellite"        
##  [7] "satellite-streets" "carto-darkmatter"  "carto-positron"   
## [10] "open-street-map"   "stamen-terrain"    "stamen-toner"     
## [13] "stamen-watercolor" "white-bg"
  • Cualquiera de estos valores puede ser utilizado para un estilo mapbox. La siguiente figura muestra el mapa base de imágenes terrestres por satélite.
layout(plot_mapbox(), mapbox = list(style = "satellite"))
  • Ahora veamos cómo crear un menú desplegable integrado en plotly.js para controlar el estilo del mapa base a través del atributo layout.updatemenus. La idea detrás de un menú desplegable integrado de plotly.js es proporcionar una lista de botones (es decir, elementos de menú) donde cada botón invoca un método de plotly.js con algunos argumentos. En este caso, cada botón utiliza el método relayout para modificar el atributo layout.mapbox.style.
style_buttons <- lapply(styles, function(s) {
  list(
    label = s, 
    method = "relayout", #modifica atributos de diseño mapbox.style
    args = list("mapbox.style", s)
  )
})
layout(
  plot_mapbox(), 
  mapbox = list(style = "dark"),
  updatemenus = list(
    list(y = 0.8, buttons = style_buttons)
  )
)
  • La otra solución de mapeo integrada en plotly es plot_geo(). En comparación con plot_mapbox(), este enfoque tiene soporte para diferentes proyecciones de mapeo, pero el estilo del mapa base es limitado y puede ser más engorroso. La siguiente rutina muestra el uso de plot_geo() junto con add_markers() y add_segments() para visualizar las rutas de vuelo dentro de los Estados Unidos. Mientras que plot_mapbox() está fijado a una proyección, el constructor plot_geo() tiene un puñado de proyecciones diferentes disponibles, incluyendo la proyección que da la ilusión del globo 3D.
library(plotly)
library(dplyr)

air <- read.csv('https://plotly-r.com/data-raw/airport_locations.csv')
knitr::kable(head(air))
iata airport city state country lat long cnt
ORD Chicago O’Hare International Chicago IL USA 41.97960 -87.90446 25129
ATL William B Hartsfield-Atlanta Intl Atlanta GA USA 33.64044 -84.42694 21925
DFW Dallas-Fort Worth International Dallas-Fort Worth TX USA 32.89595 -97.03720 20662
PHX Phoenix Sky Harbor International Phoenix AZ USA 33.43417 -112.00806 17290
DEN Denver Intl Denver CO USA 39.85841 -104.66700 13781
IAH George Bush Intercontinental Houston TX USA 29.98047 -95.33972 13223
flights <- read.csv('https://plotly-r.com/data-raw/flight_paths.csv')
knitr::kable(head(flights))
start_lat start_lon end_lat end_lon airline airport1 airport2 cnt
32.89595 -97.03720 35.04022 -106.60919 AA DFW ABQ 444
41.97960 -87.90446 30.19453 -97.66987 AA ORD AUS 166
32.89595 -97.03720 41.93887 -72.68323 AA DFW BDL 162
18.43942 -66.00183 41.93887 -72.68323 AA SJU BDL 56
32.89595 -97.03720 33.56294 -86.75355 AA DFW BHM 168
25.79325 -80.29056 36.12448 -86.67818 AA MIA BNA 56
flights$id <- seq_len(nrow(flights))
geo <- list(
  projection = list(
    type = 'orthographic',
    rotation = list(lon = -100, lat = 40, roll = 0)
  ),
  showland = TRUE,
  landcolor = toRGB("gray95"),
  countrycolor = toRGB("gray80")
)

plot_geo(color = I("red")) %>% # "I("red") color should be "red" without any further transformation
  add_markers(
    data = air, x = ~long, y = ~lat, text = ~airport,
    size = ~cnt, hoverinfo = "text", alpha = 0.5
  ) %>%
  add_segments(
    data = group_by(flights, id),
    x = ~start_lon, xend = ~end_lon,
    y = ~start_lat, yend = ~end_lat,
    alpha = 0.3, size = I(1), hoverinfo = "none"
  ) %>%
  layout(geo = geo, showlegend = FALSE)

Mapas coropléticos

  • Además de los trazos de dispersión, ambas soluciones de mapeo integrado (es decir, plot_mapbox() y plot_geo()) tienen un tipo de trazo de coroplético optimizado (es decir, los tipos choroplethmapbox y choropleth trace). Comparativamente, choroplethmapbox es más potente porque se puede especificar completamente la colección de características utilizando GeoJSON, pero, choropleth puede ser un poco más fácil de utilizar si se ajusta a su caso de uso.

  • La siguiente rutina muestra la densidad de población de los Estados Unidos a través del trazado de mapas coropléticos utilizando los datos de los estados de los Estados Unidos del paquete de conjuntos de datos (R Core Team 2016). Con solo proporcionar un atributo z, los objetos de plotly_geo() intentarán crear un mapa coroplético, pero también tendrá que proporcionar ubicaciones y un modo de ubicación.

  • Vale la pena señalar que el modo de ubicación se limita actualmente a los países y los estados de EE.UU., por lo que si necesita una unidad geográfica diferente (por ejemplo, condados, municipios, etc.), debe utilizar el tipo de trazado choroplethmapbox y/o utilizar un enfoque de mapeo “personalizado” que revisaremos mas adelante

knitr::kable(head(state.x77))
Population Income Illiteracy Life Exp Murder HS Grad Frost Area
Alabama 3615 3624 2.1 69.05 15.1 41.3 20 50708
Alaska 365 6315 1.5 69.31 11.3 66.7 152 566432
Arizona 2212 4530 1.8 70.55 7.8 58.1 15 113417
Arkansas 2110 3378 1.9 70.66 10.1 39.9 65 51945
California 21198 5114 1.1 71.71 10.3 62.6 20 156361
Colorado 2541 4884 0.7 72.06 6.8 63.9 166 103766
knitr::kable(head(state.abb))
x
AL
AK
AZ
AR
CA
CO
density <- state.x77[, "Population"] / state.x77[, "Area"]

g <- list(
  scope = 'usa',
  projection = list(type = 'albers usa'),
  lakecolor = toRGB('white')
)

plot_geo() %>%
  add_trace(
    z = ~density, text = state.name, span = I(0),
    locations = state.abb, locationmode = 'USA-states'
  ) %>%
  layout(geo = g)
  • Choroplethmapbox es más flexible que choropleth porque usted suministra su propia definición GeoJSON del choropleth a través del atributo geojson. Actualmente este atributo debe ser una URL que apunte a un archivo geojson. Además, la ubicación debe apuntar a un atributo id de nivel superior de cada característica dentro del archivo geojson. La siguiente figura muestra cómo podríamos visualizar la información anterior, pero esta vez utilizando choroplethmapbox
plot_ly() %>%
  add_trace(
    type = "choroplethmapbox",
    geojson = paste(c(
      "https://gist.githubusercontent.com/cpsievert/",
      "7cdcb444fb2670bd2767d349379ae886/raw/",
      "cf5631bfd2e385891bb0a9788a179d7f023bf6c8/", 
      "us-states.json"
    ), collapse = ""),
    locations = row.names(state.x77),
    z = state.x77[, "Population"] / state.x77[, "Area"],
    span = I(0) #span, o anchura del trazo, cero por defecto
  ) %>%
  layout(
    mapbox = list(
      style = "light",
      zoom = 4,
      center = list(lon = -98.58, lat = 39.82)
    )
  ) %>%
  config(
    mapboxAccessToken = Sys.getenv("MAPBOX_TOKEN"),
    toImageButtonOptions = list(
      format = "svg", #Scalable Vector Graphics (SVG)
      width = NULL, 
      height = NULL
    )
  )
  • Las figuras anteriores no son una forma ideal de visualizar la población estatal desde el punto de vista de la percepción gráfica. Normalmente utilizamos el color en las coropletas para codificar una variable numérica (por ejemplo, el PIB, las exportaciones netas, la nota media de la selectividad, etc,…) y el ojo percibe de forma natural el área que cubre un color concreto como proporcional a su efecto global. Esto acaba siendo engañoso, ya que el área que cubre el color no suele tener ninguna relación razonable con los datos codificados por el color.

Ejercicio para entregar

  • El siguiente ejercicio busca poner en practica lo aprendido con respecto a las visualizaciones recientemente estudiadas usando Plotly y Mapbox. Cada una de las siguientes practicas han sido desarrolladas en lenguaje Python, es tarea del estudiante, recrear cada visualización usando lenguaje R. Cada figura debe llevar su respectivo análisis e interpretación.

Shiny

  • Hay varios marcos diferentes para crear aplicaciones web a través de R, pero centraremos nuestra atención en la vinculación de gráficos plotly con shiny un paquete de R para crear aplicaciones web reactivas completamente en R. El modelo de programación reactiva de Shiny permite a los programadores de R aprovechar sus conocimientos existentes de R y crear aplicaciones web basadas en datos sin ninguna experiencia previa en programación web. Shiny en sí mismo es en gran medida agnóstico al motor utilizado para renderizar las vistas de datos (es decir, puede incorporar cualquier tipo de salida de R), pero el propio shiny también agrega algún soporte especial para interactuar con gráficos e imágenes estáticas de R (Chang 2017).

  • Al vincular los gráficos en una aplicación web, hay compensaciones a considerar cuando se utilizan gráficos estáticos de R en lugar de gráficos basados en la web. Resulta que esas compensaciones se complementan muy bien con las fortalezas y debilidades relativas de la vinculación de vistas con plotly, lo que hace que su combinación sea un potente conjunto de herramientas para vincular vistas en la web desde R. El propio Shiny proporciona una forma de acceder a eventos con gráficos estáticos hechos con cualquiera de los siguientes paquetes de R: graphics, ggplot2 y lattice. Estos paquetes son muy maduros, con todas las funciones, bien probados, y soportan una gama increíblemente amplia de gráficos, pero como deben ser regenerados en el servidor, son fundamentalmente limitados desde una perspectiva de gráficos interactivos.

  • Comparativamente, plotly no tiene la misma gama e historia, pero proporciona más opciones y control sobre la interactividad. Más concretamente, debido a que plotly está intrínsecamente basado en la web, permite un mayor control sobre cómo se actualizan los gráficos en respuesta a la entrada del usuario (por ejemplo, cambiar el color de unos pocos puntos en lugar de redibujar toda la imagen). Esta sección aborda el cómo utilizar gráficos plotly dentro de shiny, cómo conseguir que esos gráficos se comuniquen con otros tipos de vistas de datos, y cómo hacerlo todo de forma eficiente.

Su primera aplicación Shiny

  • El patrón más común de plotly+shiny utiliza una entrada de shiny para controlar una salida de plotly. Mostraremos un ejemplo sencillo de uso de la función selectizeInput() de shiny para crear un desplegable que controla un gráfico de plotly. Este ejemplo, así como cualquier otra aplicación shiny, tiene dos partes principales:

    • La interfaz de usuario, ui, define cómo se muestran los widgets de entrada y salida en la página. La función fluidPage() ofrece una forma agradable y rápida de obtener un diseño responsivo basado en una cuadrícula, pero también vale la pena señalar que la UI es completamente personalizable, y paquetes como shinydashboard facilitan el aprovechamiento de marcos de diseño más sofisticados (Chang y Borges Ribeiro 2018).

    • La función del servidor, server, define un mapeo de los valores de entrada a los widgets de salida. Más concretamente, el servidor shiny es una R function() entre los valores ingresados por el cliente y las salidas generadas en el servidor web.

  • Cada widget de entrada, incluido el selectizeInput(), está vinculado a un valor de entrada al que se puede acceder en el servidor dentro de una expresión reactiva. Las expresiones reactivas de Shiny construyen un gráfico de dependencia entre las salidas (también conocidas como puntos finales reactivos) y las entradas (también conocidas como fuentes reactivas). La verdadera potencia de las expresiones reactivas reside en su capacidad para encadenar y almacenar en caché los cálculos, pero centrémonos primero en la generación de salidas. Para generar una salida, tienes que elegir una función adecuada para renderizar el resultado de una expresión reactiva.

  • Consideraremos el siguiente dataset txhousing para este ejemplo. La siguiente rutina utiliza la función renderPlotly() para renderizar una expresión reactiva que genera un gráfico plotly. Esta expresión depende del valor de entrada input$cities (es decir, el valor de entrada ligado al widget de entrada con un inputId de "cities") y almacena la salida como output$p. Esto indica a shiny que inserte el gráfico reactivo en el contenedor plotlyOutput(outputId = "p") definido en la interfaz de usuario.

str(txhousing)
## tibble [8,602 × 9] (S3: tbl_df/tbl/data.frame)
##  $ city     : chr [1:8602] "Abilene" "Abilene" "Abilene" "Abilene" ...
##  $ year     : int [1:8602] 2000 2000 2000 2000 2000 2000 2000 2000 2000 2000 ...
##  $ month    : int [1:8602] 1 2 3 4 5 6 7 8 9 10 ...
##  $ sales    : num [1:8602] 72 98 130 98 141 156 152 131 104 101 ...
##  $ volume   : num [1:8602] 5380000 6505000 9285000 9730000 10590000 ...
##  $ median   : num [1:8602] 71400 58700 58100 68600 67300 66900 73500 75000 64500 59300 ...
##  $ listings : num [1:8602] 701 746 784 785 794 780 742 765 771 764 ...
##  $ inventory: num [1:8602] 6.3 6.6 6.8 6.9 6.8 6.6 6.2 6.4 6.5 6.6 ...
##  $ date     : num [1:8602] 2000 2000 2000 2000 2000 ...
library(shiny)
library(plotly)

ui <- fluidPage(
  selectizeInput(
    inputId = "cities", 
    label = "Select a city", 
    choices = unique(txhousing$city), 
    selected = "Abilene",
    multiple = TRUE
  ),
  plotlyOutput(outputId = "p")
)

server <- function(input, output) {
  output$p <- renderPlotly({
    plot_ly(txhousing, x = ~date, y = ~median) %>%
      filter(city %in% input$cities) %>%
      group_by(city) %>%
      add_lines()
  })
}

shinyApp(ui, server)
library("vembedr")
embed_url("https://youtu.be/htnDHPpnZbs")
  • Si, en lugar de un gráfico plotly, una expresión reactiva genera un gráfico R estático, simplemente utilice renderPlot() (en lugar de renderPlotly()) para renderizarlo y plotOutput() (en lugar de plotlyOutput()) para posicionarlo. Otros widgets de salida shiny utilizan esta convención de nombres: renderDataTable()/datatableOutput(), renderPrint()/verbatimTextOutput(), renderText()/textOutput(), renderImage()/imageOutput(), etc. Los paquetes que se basan en el estándar htmlwidgets (por ejemplo, plotly y leaflet) son, en cierto sentido, también widgets de salida de Shiny que se animan a seguir esta misma convención de nombres (por ejemplo, renderPlotly()/plotlyOutput() y renderLeaflet()/leafletOutput()).

  • Shiny también viene preempaquetado con un puñado de otros widgets de entrada útiles. Aunque muchas aplicaciones de Shiny los utilizan directamente"out-of-the-box", los widgets de entrada pueden estilizarse fácilmente con CSS y/o SASS, e incluso se pueden integrar widgets de entrada personalizados (Mastny 2018; RStudio 2014a).

    • selectInput()/selectizeInput() para los menús desplegables.
    • numericInput() para un solo número.
    • sliderInput() para un rango numérico.
    • textInput() para una cadena de caracteres.
    • dateInput() para una sola fecha.
    • dateRangeInput() para un rango de fechas.
    • fileInput() para subir archivos.
    • checkboxInput()/checkboxGroupInput()/radioButtons() para elegir una lista de opciones.
  • A partir de ahora nuestro enfoque es enlazar múltiples gráficos en shiny a través de la manipulación directa, por lo que nos centramos menos en el uso de estos widgets de entrada, y más en el uso de plotly y los gráficos estáticos de R como entradas a otros widgets de salida.

Ocultación y rediseño al cambiar de tamaño

  • La función renderPlotly() renderiza cualquier cosa que la función plotly_build() entienda, incluyendo los objetos plot_ly(), ggplotly() y ggplot2. También renderiza NULL como un div HTML vacío, lo que es útil para ciertos casos en los que no tiene sentido renderizar un gráfico. La siguiente rutina aprovecha estas características para representar un div vacío mientras se muestra el marcador de posición de selectizeInput(), pero luego representa un gráfico plotly mediante ggplotly() una vez que se han seleccionado las ciudades.

  • También muestra cómo hacer que la salida de plotly dependa del tamaño del contenedor que contiene el gráfico de plotly. Por defecto, cuando un navegador cambia de tamaño, el tamaño del gráfico se cambia puramente del lado del cliente, pero esta expresión reactiva se reejecutará cuando la ventana del navegador cambie de tamaño. Debido a razones técnicas, esto puede mejorar el comportamiento del redimensionamiento de ggplotly(), pero debe usarse con precaución cuando se manejan grandes datos y largos tiempos de representación. Aquí req se asegúrese de que los valores están disponibles, antes de proceder con un cálculo o acción.

library(shiny)

cities <- unique(txhousing$city)

ui <- fluidPage(
  selectizeInput(
    inputId = "cities", 
    label = NULL,
    choices = c("Please choose a city" = "", cities), 
    multiple = TRUE
  ),
  plotlyOutput(outputId = "p")
)

server <- function(input, output, session) {
  output$p <- renderPlotly({
    req(input$cities)
    if (identical(input$cities, "")) return(NULL)
    p <- ggplot(data = filter(txhousing, city %in% input$cities)) + 
      geom_line(aes(date, median, group = city))
    height <- session$clientData$output_p_height
    width <- session$clientData$output_p_width
    ggplotly(p, height = height, width = width)
  })
}

shinyApp(ui, server)
library("vembedr")
embed_url("https://youtu.be/Szj_CiDz1WI")

Eventos de alcance

  • Esta sección aprovecha la interfaz para acceder a los eventos de entrada de plotly para informar a otras vistas de datos sobre esos eventos. Cuando se gestionan múltiples vistas que se comunican entre sí, es necesario saber qué vistas son una fuente de interacción y cuáles son un objetivo (¡una vista puede ser ambas cosas a la vez!). La función event_data() proporciona un argumento de origen para ayudar a refinar qué vista(s) sirve(n) como fuente de un evento. El argumento fuente toma un ID de cadena, y cuando ese ID coincide con la fuente de un gráfico de plot_ly()/ggplotly(), entonces el event_data() se “asigna” a esa vista. Para tener una mejor idea de cómo funciona esto, considere el siguiente videoclip

  • Nótese que permite hacer clic en una celda de un mapa de calor de correlación para generar un gráfico de dispersión de las dos variables correspondientes, lo que permite ver más de cerca su relación. En el caso de un mapa de calor, los datos de eventos vinculados a un evento plotly_click contienen las categorías x e y relevantes (por ejemplo, los nombres de las variables de datos de interés) y el valor z (por ejemplo, la correlación de Pearson entre esas variables). Para obtener los datos de los clics del mapa térmico, y sólo del mapa térmico, es importante que el argumento fuente de la función event_data() coincida con el argumento fuente de plot_ly(). De lo contrario, si el argumento fuente no se especificara event_data("plotly_click") también se dispararía cuando el usuario hiciera clic en el gráfico de dispersión, causando probablemente un error. En este ejemplo utilizaremos el dataset mtcars de R. Se usa el ajuste por medio de un modelo lineal con la función lm.

knitr::kable(head(mtcars))
mpg cyl disp hp drat wt qsec vs am gear carb
Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
library(shiny)

# cache computation of the correlation matrix
correlation <- round(cor(mtcars), 3)

ui <- fluidPage(
  plotlyOutput("heat"),
  plotlyOutput("scatterplot")
)

server <- function(input, output, session) {
  
  output$heat <- renderPlotly({
    plot_ly(source = "heat_plot") %>%
      add_heatmap(
        x = names(mtcars), 
        y = names(mtcars), 
        z = correlation
      )
  })
  
  output$scatterplot <- renderPlotly({
    # if there is no click data, render nothing!
    clickData <- event_data("plotly_click", source = "heat_plot")
    if (is.null(clickData)) return(NULL)
    
    # Obtain the clicked x/y variables and fit linear model
    vars <- c(clickData[["x"]], clickData[["y"]])
    d <- setNames(mtcars[vars], c("x", "y"))
    yhat <- fitted(lm(y ~ x, data = d))
    
    # scatterplot with fitted line
    plot_ly(d, x = ~x) %>%
      add_markers(y = ~y) %>%
      add_lines(y = ~yhat) %>%
      layout(
        xaxis = list(title = clickData[["x"]]), 
        yaxis = list(title = clickData[["y"]]), 
        showlegend = FALSE
      )
  })
  
}

shinyApp(ui, server)
library("vembedr")
embed_url("https://youtu.be/QXUVPiSZHcw")

Drill-down

  • La siguiente app muestra las ventas desglosadas por categoría de negocio (por ejemplo, Furniture, Office Supplies, Technology) en un gráfico circular. Permite al usuario hacer clic en una porción del gráfico para “profundizar” en las subcategorías de la categoría elegida.

  • En términos de implementación, el aspecto clave aquí es mantener el estado de la categoría actualmente seleccionada a través de un reactiveVal() y actualizar ese valor cuando se hace clic en una categoría o se pulsa el botón "Back". La función reactive() envuelve una expresión normal para crear una expresión reactiva. Conceptualmente, una expresión reactiva es una expresión cuyo resultado cambiará con el tiempo.

library(tidyverse)
sales <- read_csv("https://plotly-r.com/data-raw/sales.csv")
knitr::kable(head(sales))
id category sub_category order_date sales
CG-12520 Furniture Bookcases 2016-11-08 261.9600
CG-12520 Furniture Chairs 2016-11-08 731.9400
DV-13045 Office Supplies Labels 2016-06-12 14.6200
SO-20335 Furniture Tables 2015-10-11 957.5775
SO-20335 Office Supplies Storage 2015-10-11 22.3680
BH-11710 Furniture Furnishings 2014-06-09 48.8600
library(shiny)
library(dplyr)
library(readr)
library(purrr) # just for `%||%`

sales <- read_csv("https://plotly-r.com/data-raw/sales.csv")
categories <- unique(sales$category)

ui <- fluidPage(plotlyOutput("pie"), uiOutput("back"))

server <- function(input, output, session) {
  # for maintaining the current category (i.e. selection)
  current_category <- reactiveVal()
  
  # report sales by category, unless a category is chosen
  sales_data <- reactive({
    if (!length(current_category())) {
      return(count(sales, category, wt = sales))
    }
    sales %>%
      filter(category %in% current_category()) %>%
      count(sub_category, wt = sales)
  })
  
  # Note that pie charts don't currently attach the label/value 
  # with the click data, but we can include as `customdata`
  output$pie <- renderPlotly({
    d <- setNames(sales_data(), c("labels", "values"))
    plot_ly(d) %>%
      add_pie(
        labels = ~labels, 
        values = ~values, 
        customdata = ~labels
      ) %>%
      layout(title = current_category() %||% "Total Sales")
  })
  
  # update the current category when appropriate
  observe({
    cd <- event_data("plotly_click")$customdata[[1]]
    if (isTRUE(cd %in% categories)) current_category(cd)
  })
  
  # populate back button if category is chosen
  output$back <- renderUI({
    if (length(current_category())) 
      actionButton("clear", "Back", icon("chevron-left"))
  })
  
  # clear the chosen category on back button press
  observeEvent(input$clear, current_category(NULL))
}

shinyApp(ui, server)
library("vembedr")
embed_url("https://youtu.be/wAfmLwyHPWU")
  • Un desglose básico como el de la app anterior es algo útil por sí mismo, pero se vuelve mucho más útil cuando se vincula a múltiples vistas de los datos. La siguiente rutina mejora la app anteriror para mostrar las ventas a lo largo del tiempo por categoría o subcategoría (si se elige una categoría). Observe que el aspecto clave de la implementación sigue siendo el mismo (es decir, el mantenimiento del estado a través de reactiveValue()), la principal diferencia es que la vista de la serie de tiempo ahora también responde a los cambios en la categoría actualmente seleccionada. Es decir, ambas vistas muestran las ventas por categoría cuando no se selecciona ninguna categoría, y las ventas por subcategoría cuando se selecciona una categoría.
library(shiny)
library(dplyr)
library(readr)
library(plotly)
library(purrr)

sales <- read_csv("https://plotly-r.com/data-raw/sales.csv")
categories <- unique(sales$category)

ui <- fluidPage(
  plotlyOutput("bar"),
  uiOutput("back"),
  plotlyOutput("time")
)

server <- function(input, output, session) {
  
  current_category <- reactiveVal()
  
  # report sales by category, unless a category is chosen
  sales_data <- reactive({
    if (!length(current_category())) {
      return(count(sales, category, wt = sales))
    }
    sales %>%
      filter(category %in% current_category()) %>%
      count(sub_category, wt = sales)
  })
  
  # the pie chart
  output$bar <- renderPlotly({
    d <- setNames(sales_data(), c("x", "y"))
    
    plot_ly(d) %>%
      add_bars(x = ~x, y = ~y, color = ~x) %>%
      layout(title = current_category() %||% "Total Sales")
  })
  
  # same as sales_data
  sales_data_time <- reactive({
    if (!length(current_category())) {
      return(count(sales, category, order_date, wt = sales))
    }
    sales %>%
      filter(category %in% current_category()) %>%
      count(sub_category, order_date, wt = sales)
  })
  
  output$time <- renderPlotly({
    d <- setNames(sales_data_time(), c("color", "x", "y"))
    plot_ly(d) %>%
      add_lines(x = ~x, y = ~y, color = ~color)
  })
  
  # update the current category when appropriate
  observe({
    cd <- event_data("plotly_click")$x
    if (isTRUE(cd %in% categories)) current_category(cd)
  })
  
  # populate back button if category is chosen
  output$back <- renderUI({
    if (length(current_category())) 
      actionButton("clear", "Back", icon("chevron-left"))
  })
  
  # clear the chosen category on back button press
  observeEvent(input$clear, current_category(NULL))
}

shinyApp(ui, server)
library("vembedr")
embed_url("https://youtu.be/9sp1vUgmHLI")

Despliegue de aplicación Shiny

  • La plataforma shinyapps.io es bastante usada para desplegar aplicaciones Shiny. Por lo tanto para seguir con los siguientes pasos debe crearse una cuenta en shinyapps.io. shinyapps.io ofrece un plan gratuito, pero está limitado a 5 aplicaciones activas y a un uso mensual de 25 horas activas máximo. Si despliegas tu aplicación a disposición de un público amplio, esperarías superar el límite mensual de horas activas con bastante rapidez. Para aumentar el límite mensual (o para publicar más de 5 aplicaciones), tendrás que actualizar tu plan a uno de pago.
  1. Abra RStudio y cree una nueva aplicación Shiny

  1. Asignele un nombre (sin espacio), elige dónde guardarlo y haz clic en el botón Create

  1. Luego de esto copie en la app por defecto el código por ejemplo de la anterior app creada para el drill-down. De la misma manera que cuando se abre un nuevo documento R Markdown, se crea el código para una aplicación Shiny básica. Ejecute la aplicación haciendo clic en el botón Run App para ver el resultado

4. Se abrirá la app. Para publicarla, debemos hacer click en el botón Publish

  • Luego de esto hacemos click en ShinyApps.io para conectarnos a nuestra cuenta, creada anteriormente

  1. Luego de esto necesitaremos un token para poder conectar nuestra app al servidor de ShinyApps.io. Con este fin, seguimos las instrucciones que aparecen en la ventana, esto es, vamos a nuestra cuenta y hacemos click en nuestro nombre de usuario, luego presionamos el boton Tokens, despues de esto hacemos click en Show para visualizar el token, luego Show secret/Copy to clipboard. Luego de realizar esto, pegamos nuestro token en la ventana de publicación de nuestra app

  1. Luego le va a aparecer la siguiente ventana con la opción de publicar su aplicación. Haga click en Publish

  1. Después de varios segundos (dependiendo del peso de su aplicación), la aplicación Shiny debería aparecer en su navegador de Internet. Para volver a desplegar la aplicación puede hacerlo desde su aplicación haciendo click en el botón resaltado en la imagen y deberá visualizar su despliegue en la parte inferior. Ahora puede realizar cambio y usar sólo ese botón cuando desde hacer el despliegue de su aplicación. La app de este ejemplo ha sido publicada en el siguiente link https://lihkir.shinyapps.io/drill-down-app/.

  1. Puede revisar en su cuenta que se ha creado satisfactoriamente su aplicación. Puede visualizar las conexiones y demás información en los botones que se resaltan en rojo. Revise que información entrega cada uno.

Ejercicio para entregar

  • Despliegue en shinnyapps.io los dashboard de los dos ejemplos estudiados en la sección de Python: Introducción a Dash, Dos ejemplos de Dash. Esto es, desplegar en shinyapps.io los Dash asociados con: Dash App para Mapas y Dash App Financiera estudiados en esta sección de Python.

  • Posteriormente, realice cambios en el (back/front end) de los dos Dashboards. Por ejemplo,

    • Agregue a los dashboard nuevos estilos: Cambiar título, logo, color de fondo,….. Además, agregué un nuevo Dropdown para la columna Affected by. Agregue divisiones con diagramas descriptivos para cada una de las columnas del dataframe. Realice agrupación por State, Affected by, Year y realice gráficos descriptivos que entreguen información valiosa.
    • Agregue además de la ventana de selección de stocks, un nuevo Dropdown, el cual le permita por medio de un menú de opciones, seleccionar entre 4 indicadores de análisis técnico (ver Commonly Used Technical Indicators; Technical Indicators Mathematical Description; 7 Technical Indicators to Build a Trading Toolkit, Best Day Trading Indicators for Beginners), por ejemplo:
      • On-balance volume (OBV)
      • Accumulation/distribution (A/D) line
      • Average directional index
      • Aroon oscillator
      • Moving average convergence divergence (MACD)
      • Relative strength index (RSI)
      • Stochastic oscillator
  • Cada uno de estos indicadores cuenta con parámetros que pueden modificarse desde un Dropdown, por ejemplo, en el caso de la media movil, agregue un Dropdown para seleccionar el periodo. Para las Bollinger Bands, agregue otro Dropdown para, además del periodo de la media móvil, agregar, el número de desviaciones estándar que se desviará de la media cada banda, etc,…

  • Asegúrese de que todos los indicadores seleccionados aparezcan en el mismo candlestick, esto es fundamental para la toma de decisiones. Por ejemplo, en un solo chart, el objetivo es que el inversionista pueda obtener un mínimo de 4 confirmaciones, por medio del análisis visual de cada uno de los indicadores suministrados por el Dashboard, antes de tomar una decisión de compra o venta (long/short). Por lo tanto, debe visualizar todos los 4 indicadores en un solo plot sobrepuesto.