Una muy breve introducción a Tidyverse

1 Tidyverse

El universo de los paquetes de tidyverse, una colección de paquetes de funciones para un uso especialmente enfocado en la ciencia de datos, abrió un antes y después en la programación de R. En este post voy a resumir muy brevemente lo más esencial para inicarse en este mundo. La gramática sigue en todas las funciones una estructura común. Lo más esencial es que el primer argumento es el objeto y a continuación viene el resto de argumentos. Además, se proporciona un conjunto de verbos que facilitan el uso de las funciones. En la actualidad, la filosofía de las funciones también se refleja en otros paquetes que hacen compatible su uso con la colección de tidyverse. Por ejemplo, el paquete sf (simple feature) para el tratamiento de datos vectoriales, permite el uso de múltiples funciones que encontramos en el paquete dplyr.

El núcleo de la colección lo constituyen los siguientes paquetes:

Paquete Descripción
ggplot2 Gramática para la creación de gráficos
purrr Programación funcional de R
tibble Sistema moderno y efectivo de tablas
dplyr Gramatica para la manipulación de datos
tidyr Conjunto de funciones para ordenar datos
stringr Conjunto de funciones para trabajar con caracteres
readr Una forma fácil y rápida para importar datos
forcats Herramientas y funciones para trabajar fácilmente con factores

Además de los paquetes menciondos, también se usa muy frecuentemente lubridate para trabajar con fechas y horas, y también readxl que nos permite importar archivos en formato Excel. Para conocer todos los paquetes disponibles podemos emplear la función tidyverse_packages().

##  [1] "broom"         "cli"           "crayon"        "dbplyr"       
##  [5] "dplyr"         "dtplyr"        "forcats"       "googledrive"  
##  [9] "googlesheets4" "ggplot2"       "haven"         "hms"          
## [13] "httr"          "jsonlite"      "lubridate"     "magrittr"     
## [17] "modelr"        "pillar"        "purrr"         "readr"        
## [21] "readxl"        "reprex"        "rlang"         "rstudioapi"   
## [25] "rvest"         "stringr"       "tibble"        "tidyr"        
## [29] "xml2"          "tidyverse"

Es muy fácil encontrarnos con conflicos de funciones, o sea, que el mismo nombre de función exista en varios paquetes. Para evitarlo, podemos escribir el nombre del paquete delante de la función que queremos usar, separados por el símbolo de dos puntos escrito dos veces (package_name::function_name).

Antes de empezar con los paquetes, espero que sea verdaderamente una breve introducción, algunos comentarios sobre el estilo al programar en R.

2 Guía de estilo

En R no existe una guía de estilo universal, o sea, en la sintaxis de R no es necesario seguir normas concretas para nuestros scripts. Es recomendable trabajar de forma homogénea y clara a la hora de escribir con un estilo uniforme y legible. La colección de tidyverse tiene una guia propia (https://style.tidyverse.org/).

Las recomendaciones más importes son:

  • Evitar usar más de 80 caracteres por línea para permitir leer el código completo.
  • Usar siempre un espacio después de una coma, nunca antes.
  • Los operadores (==, +, -, <-, %>%, etc.) deben tener un espacio antes y después.
  • No hay espacio entre el nombre de una función y el primer paréntesis, ni entre el último arguemento y el paréntesis final de una función.
  • Evitar reutilizar nombres de funciones y variables comunes (c <- 5 vs. c())
  • Ordenar el script separando las partes con la forma de comentario # Importar datos -----
  • Se deben evitar tildes o símbolos especiales en nombres, archivos, rutas, etc.
  • Nombres de los objetos deben seguir una estructura constante: day_one, day_1.

Es aconsejable usar una correcta indentación para multiples argumentos de una función o funciones encadenadas por el operador pipe (%>%).

3 Pipe %>%

Para facilitar el trabajo en la gestión, manipulación y visualización de datos, el paquete magrittr introduce el operador llamado pipe en la forma %>% con el objetivo de combinar varias funciones sin la necesidad de asignar el resultado a un nuevo objeto. El operador pipe pasa a la salida de una función aplicada al primer argumento de la siguiente función. Esta forma de combinar funciones permite encadenar varios pasos de forma simultánea. En el siguiente ejemplo, muy sencillo, pasamos el vector 1:5 a la función mean() para calcular el promedio.

1:5 %>% mean()
## [1] 3

4 Paquetes de Tidyverse

4.1 Lectura y escritura

El paquete readr facilita la lectura o escritura de múltiples formatos de archivo usando funciones que comienzan por read_* o write_*. En comparación con R Base las funciones son más rápidas, ayudan a limpiar los nombres de las columnas y las fechas son convertidas automáticamente. Las tablas importadas son de clase tibble (tbl_df), una versión moderna de data.frame del paquete tibble. En el mismo sentido se puede usar la función read_excel() del paquete readxl para importar datos de hojas de Excel (más detalles también en esta entrada de mi blog). En el siguiente ejemplo importamos los datos de la movilidad registrada por Google (enlace) durante los últimos meses a causa de la pandemia COVID-19 (descarga).

Función lectura Descripción
read_csv() o read_csv2() coma o punto-coma (CSV)
read_delim() separador general
read_table() espacio blanco
# cargar el paquete
library(tidyverse)

google_mobility <- read_csv("Global_Mobility_Report.csv")
## Rows: 516697 Columns: 13
## -- Column specification --------------------------------------------------------
## Delimiter: ","
## chr  (6): country_region_code, country_region, sub_region_1, sub_region_2, i...
## dbl  (6): retail_and_recreation_percent_change_from_baseline, grocery_and_ph...
## date (1): date
## 
## i Use `spec()` to retrieve the full column specification for this data.
## i Specify the column types or set `show_col_types = FALSE` to quiet this message.
google_mobility
## # A tibble: 516,697 x 13
##    country_region_code country_region  sub_region_1 sub_region_2 iso_3166_2_code
##    <chr>               <chr>           <chr>        <chr>        <chr>          
##  1 AE                  United Arab Em~ <NA>         <NA>         <NA>           
##  2 AE                  United Arab Em~ <NA>         <NA>         <NA>           
##  3 AE                  United Arab Em~ <NA>         <NA>         <NA>           
##  4 AE                  United Arab Em~ <NA>         <NA>         <NA>           
##  5 AE                  United Arab Em~ <NA>         <NA>         <NA>           
##  6 AE                  United Arab Em~ <NA>         <NA>         <NA>           
##  7 AE                  United Arab Em~ <NA>         <NA>         <NA>           
##  8 AE                  United Arab Em~ <NA>         <NA>         <NA>           
##  9 AE                  United Arab Em~ <NA>         <NA>         <NA>           
## 10 AE                  United Arab Em~ <NA>         <NA>         <NA>           
## # ... with 516,687 more rows, and 8 more variables: census_fips_code <chr>,
## #   date <date>, retail_and_recreation_percent_change_from_baseline <dbl>,
## #   grocery_and_pharmacy_percent_change_from_baseline <dbl>,
## #   parks_percent_change_from_baseline <dbl>,
## #   transit_stations_percent_change_from_baseline <dbl>,
## #   workplaces_percent_change_from_baseline <dbl>,
## #   residential_percent_change_from_baseline <dbl>

Debemos prestar atención a los nombres de los argumentos, ya que cambian en las funciones de readr. Por ejemplo, el argumento conocido header = TRUE de read.csv() es en este caso col_names = TRUE. Podemos encontrar más detalles en el Cheat-Sheet de readr .

4.2 Manipulación de caracteres

Cuando se requiere manipular cadenas de texto usamos el paquete stringr, cuyas funciones siempre empiezan por str_* seguidas por un verbo y el primer argumento.

Algunas de estas funciones son las siguientes:

Función Descripción
str_replace() reemplazar patrones
str_c() combinar characteres
str_detect() detectar patrones
str_extract() extraer patrones
str_sub() extraer por posición
str_length() longitud de la cadena de caracteres

Se suelen usar expresiones regulares para patrones de caracteres. Por ejemplo, la expresión regular [aeiou] coincide con cualquier caracter único que sea una vocal. El uso de corchetes [] corresponde a clases de caracteres. Por ejemplo, [abc] corresponde a cada letra independientemente de la posición. [a-z] o [A-Z] o [0-9] cada uno entre a y z ó 0 y 9. Y por último, [:punct:] puntuación, etc. Con llaves “{}” podemos indicar el número del elemento anterior {2} sería dos veces, {1,2} entre una y dos, etc. Además con $o ^ podemos indicar si el patrón empieza al principio o termina al final. Podemos encontrar más detalles y patrones en el Cheat-Sheet de stringr.

# reemplazamos 'er' al final por vacío

str_replace(month.name, "er$", "")
##  [1] "January"  "February" "March"    "April"    "May"      "June"    
##  [7] "July"     "August"   "Septemb"  "Octob"    "Novemb"   "Decemb"
str_replace(month.name, "^Ma", "")
##  [1] "January"   "February"  "rch"       "April"     "y"         "June"     
##  [7] "July"      "August"    "September" "October"   "November"  "December"
# combinar caracteres

a <- str_c(month.name, 1:12, sep = "_")
a
##  [1] "January_1"   "February_2"  "March_3"     "April_4"     "May_5"      
##  [6] "June_6"      "July_7"      "August_8"    "September_9" "October_10" 
## [11] "November_11" "December_12"
# colapsar combinación

str_c(month.name, collapse = ", ")
## [1] "January, February, March, April, May, June, July, August, September, October, November, December"
# dedectamos patrones

str_detect(a, "_[1-5]{1}")
##  [1]  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE  TRUE  TRUE  TRUE
# extraemos patrones

str_extract(a, "_[1-9]{1,2}")
##  [1] "_1"  "_2"  "_3"  "_4"  "_5"  "_6"  "_7"  "_8"  "_9"  "_1"  "_11" "_12"
# extraermos los caracteres en las posiciones entre 1 y 2

str_sub(month.name, 1, 2)
##  [1] "Ja" "Fe" "Ma" "Ap" "Ma" "Ju" "Ju" "Au" "Se" "Oc" "No" "De"
# longitud de cada mes

str_length(month.name)
##  [1] 7 8 5 5 3 4 4 6 9 7 8 8
# con pipe, el '.' representa al objeto que pasa el operador %>%
str_length(month.name) %>% 
   str_c(month.name, ., sep = ".")
##  [1] "January.7"   "February.8"  "March.5"     "April.5"     "May.3"      
##  [6] "June.4"      "July.4"      "August.6"    "September.9" "October.7"  
## [11] "November.8"  "December.8"

Una función muy útil es str_glue() para interpolar caracteres.

name <- c("Juan", "Michael")
age <- c(50, 80) 
date_today <- Sys.Date()

str_glue(
  "My name is {name}, ",
  "I'am {age}, ",
  "and my birth year is {format(date_today-age*365, '%Y')}."
)
## My name is Juan, I'am 50, and my birth year is 1972.
## My name is Michael, I'am 80, and my birth year is 1942.

4.3 Manejo de fechas y horas

El paquete lubridate ayuda en el manejo de fechas y horas. Nos permite crear los objetos reconocidos por R con funciones (como ymd() ó ymd_hms()) y hacer cálculos.

Debemos conocer las siguientes abreviaturas:

  • ymd: representa y:year, m:month, d:day
  • hms: representa h:hour, m:minutes, s:seconds
# paquete
library(lubridate)
## 
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union
# vector de fechas
dat <- c("1999/12/31", "2000/01/07", "2005/05/20","2010/03/25")

# vector de fechas y horas
dat_time <- c("1988-08-01 05:00", "2000-02-01 22:00")

# convertir a clase date
dat <- ymd(dat) 
dat
## [1] "1999-12-31" "2000-01-07" "2005-05-20" "2010-03-25"
# otras formatos
dmy("05-02-2000")
## [1] "2000-02-05"
ymd("20000506")
## [1] "2000-05-06"
# convertir a POSIXct
dat_time <- ymd_hm(dat_time)
dat_time
## [1] "1988-08-01 05:00:00 UTC" "2000-02-01 22:00:00 UTC"
# diferentes formatos en un vector 
dat_mix <- c("1999/12/05", "05-09-2008", "2000/08/09", "25-10-2019")

# indicar formato con la convención conocida en ?strptime
parse_date_time(dat_mix, order = c("%Y/%m/%d", "%d-%m-%Y"))
## [1] "1999-12-05 UTC" "2008-09-05 UTC" "2000-08-09 UTC" "2019-10-25 UTC"

Más funciones útiles:

# extraer el año
year(dat)
## [1] 1999 2000 2005 2010
# el mes
month(dat)
## [1] 12  1  5  3
month(dat, label = TRUE) # como etiqueta
## [1] dic ene may mar
## 12 Levels: ene < feb < mar < abr < may < jun < jul < ago < sep < ... < dic
# el día de la semana
wday(dat)
## [1] 6 6 6 5
wday(dat, label = TRUE) # como etiqueta
## [1] vi\\. vi\\. vi\\. ju\\.
## Levels: do\\. < lu\\. < ma\\. < mi\\. < ju\\. < vi\\. < sá\\.
# la hora
hour(dat_time)
## [1]  5 22
# sumar 10 días
dat + days(10)
## [1] "2000-01-10" "2000-01-17" "2005-05-30" "2010-04-04"
# sumar 1 mes
dat + months(1)
## [1] "2000-01-31" "2000-02-07" "2005-06-20" "2010-04-25"

Por último, la función make_date() es muy útil en crear fechas a partir de diferentes partes de las mismas como puede ser el año, mes, etc.

# crear fecha a partir de sus elementos, aquí con año y mes
make_date(2000, 5)
## [1] "2000-05-01"
# crear fecha con hora 
make_datetime(2005, 5, 23, 5)
## [1] "2005-05-23 05:00:00 UTC"

Podemos encontrar más detalles en el Cheat-Sheet de lubridate.

4.4 Manipulación de tablas y vectores

Los paquetes dplyr y tidyr nos proporciona una gramática de manipulación de datos con un conjunto de verbos útiles para resolver los problemas más comunes. Las funciones más importantes son:

Función Descripción
mutate() añadir nuevas variables o modificar existentes
select() seleccionar variables
filter() filtrar
summarise() resumir/reducir
arrange() ordenar
group_by() agrupar
rename() renombrar columnas

En caso de que no lo hayas hecho antes, importamos los datos de movilidad.

google_mobility <- read_csv("Global_Mobility_Report.csv")
## Rows: 516697 Columns: 13
## -- Column specification --------------------------------------------------------
## Delimiter: ","
## chr  (6): country_region_code, country_region, sub_region_1, sub_region_2, i...
## dbl  (6): retail_and_recreation_percent_change_from_baseline, grocery_and_ph...
## date (1): date
## 
## i Use `spec()` to retrieve the full column specification for this data.
## i Specify the column types or set `show_col_types = FALSE` to quiet this message.

4.4.1 Selecionar y renombrar

Podemos selecionar o eliminar columnas con la función select(), usando el nombre o índice de la(s) columna(s). Para suprimir columnas hacemos uso del signo negativo. La función rename ayuda en renombrar columnas o bien con el mismo nombre o con su índice.

residential_mobility <- select(google_mobility, 
                               country_region_code:sub_region_1, 
                               date, 
                               residential_percent_change_from_baseline) %>% 
                        rename(resi = 5)

4.4.2 Filtrar y ordenar

Para filtrar datos, empleamos filter() con operadores lógicos (|, ==, >, etc) o funciones que devuelven un valor lógico (str_detect(), is.na(), etc.). La función arrange() ordena de menor a mayor por una o múltiples variables (con el signo negativo - se invierte el orden de mayor a menor).

filter(residential_mobility, 
       country_region_code == "US")
## # A tibble: 304,648 x 5
##    country_region_code country_region sub_region_1 date        resi
##    <chr>               <chr>          <chr>        <date>     <dbl>
##  1 US                  United States  <NA>         2020-02-15    -1
##  2 US                  United States  <NA>         2020-02-16    -1
##  3 US                  United States  <NA>         2020-02-17     5
##  4 US                  United States  <NA>         2020-02-18     1
##  5 US                  United States  <NA>         2020-02-19     0
##  6 US                  United States  <NA>         2020-02-20     1
##  7 US                  United States  <NA>         2020-02-21     0
##  8 US                  United States  <NA>         2020-02-22    -1
##  9 US                  United States  <NA>         2020-02-23    -1
## 10 US                  United States  <NA>         2020-02-24     0
## # ... with 304,638 more rows
filter(residential_mobility, 
       country_region_code == "US", 
       sub_region_1 == "New York")
## # A tibble: 7,068 x 5
##    country_region_code country_region sub_region_1 date        resi
##    <chr>               <chr>          <chr>        <date>     <dbl>
##  1 US                  United States  New York     2020-02-15     0
##  2 US                  United States  New York     2020-02-16    -1
##  3 US                  United States  New York     2020-02-17     9
##  4 US                  United States  New York     2020-02-18     3
##  5 US                  United States  New York     2020-02-19     2
##  6 US                  United States  New York     2020-02-20     2
##  7 US                  United States  New York     2020-02-21     3
##  8 US                  United States  New York     2020-02-22    -1
##  9 US                  United States  New York     2020-02-23    -1
## 10 US                  United States  New York     2020-02-24     0
## # ... with 7,058 more rows
filter(residential_mobility, 
       resi > 50) %>% 
          arrange(-resi)
## # A tibble: 32 x 5
##    country_region_code country_region sub_region_1              date        resi
##    <chr>               <chr>          <chr>                     <date>     <dbl>
##  1 KW                  Kuwait         Al Farwaniyah Governorate 2020-05-14    56
##  2 KW                  Kuwait         Al Farwaniyah Governorate 2020-05-21    55
##  3 SG                  Singapore      <NA>                      2020-05-01    55
##  4 KW                  Kuwait         Al Farwaniyah Governorate 2020-05-28    54
##  5 PE                  Peru           Metropolitan Municipalit~ 2020-04-10    54
##  6 EC                  Ecuador        Pichincha                 2020-03-27    53
##  7 KW                  Kuwait         Al Farwaniyah Governorate 2020-05-11    53
##  8 KW                  Kuwait         Al Farwaniyah Governorate 2020-05-13    53
##  9 KW                  Kuwait         Al Farwaniyah Governorate 2020-05-20    53
## 10 SG                  Singapore      <NA>                      2020-04-10    53
## # ... with 22 more rows

4.4.3 Agrupar y resumir

¿Dónde encontramos mayor variabilidad entre regiones en cada país el día 1 de abril de 2020?

Para responder a esta pregunta, primero filtramos los datos y después agrupamos por la columna de país. Cuando empleamos la función summarise() posterior a la agrupación, nos permite resumir por estos grupos. Incluso, la combinación del group_by() con la función mutate() permite modificar columnas por grupos. En summarise() calculamos el valor máximo, mínimo y la diferencia entre ambos extremos creando nuevas columnas.

resi_variability <- residential_mobility %>% 
                        filter(date == ymd("2020-04-01"),
                               !is.na(sub_region_1)) %>% 
                          group_by(country_region) %>% 
                       summarise(mx = max(resi, na.rm = TRUE), 
                                 min = min(resi, na.rm = TRUE),
                                 range = abs(mx)-abs(min))
## Warning in max(resi, na.rm = TRUE): ningun argumento finito para max; retornando
## -Inf

## Warning in max(resi, na.rm = TRUE): ningun argumento finito para max; retornando
## -Inf

## Warning in max(resi, na.rm = TRUE): ningun argumento finito para max; retornando
## -Inf

## Warning in max(resi, na.rm = TRUE): ningun argumento finito para max; retornando
## -Inf

## Warning in max(resi, na.rm = TRUE): ningun argumento finito para max; retornando
## -Inf

## Warning in max(resi, na.rm = TRUE): ningun argumento finito para max; retornando
## -Inf
## Warning in min(resi, na.rm = TRUE): ningún argumento finito para min; retornando
## Inf

## Warning in min(resi, na.rm = TRUE): ningún argumento finito para min; retornando
## Inf

## Warning in min(resi, na.rm = TRUE): ningún argumento finito para min; retornando
## Inf

## Warning in min(resi, na.rm = TRUE): ningún argumento finito para min; retornando
## Inf

## Warning in min(resi, na.rm = TRUE): ningún argumento finito para min; retornando
## Inf

## Warning in min(resi, na.rm = TRUE): ningún argumento finito para min; retornando
## Inf
arrange(resi_variability, -range)
## # A tibble: 94 x 4
##    country_region    mx   min range
##    <chr>          <dbl> <dbl> <dbl>
##  1 Nigeria           43     6    37
##  2 United States     35     6    29
##  3 India             36    15    21
##  4 Malaysia          45    26    19
##  5 Philippines       40    21    19
##  6 Vietnam           28     9    19
##  7 Colombia          41    24    17
##  8 Ecuador           44    27    17
##  9 Argentina         35    19    16
## 10 Chile             30    14    16
## # ... with 84 more rows

4.4.4 Unir tablas

¿Cómo podemos filtrar los datos para obtener un subconjunto de Europa?

Para ello, importamos datos espaciales con el código de país y una columna de las regiones. Explicaciones detalladas sobre el paquete sf (simple feature) para trabajar con datos vectoriales, lo dejaremos para otro post.

library(rnaturalearth) # paquete de datos vectoriales

# datos de países
wld <- ne_countries(returnclass = "sf")

# filtramos los países con código y seleccionamos las dos columnas de interés
wld <- filter(wld, !is.na(iso_a2)) %>% select(iso_a2, subregion)

# plot
plot(wld)

Otras funciones de dplyr nos permiten unir tablas: *_join(). Según hacia qué tabla (izquierda o derecha) se quiere unir, cambia la función : left_join(), right_join() o incluso full_join(). El argumento by no es necesario siempre y cuando ambas tablas tienen una columna en común. No obstante, en este caso la columna de fusión es diferente, por eso, usamos el modo c("country_region_code"="iso_a2"). El paquete forcats de tidyverse tiene muchas funciones útiles para manejar variables categóricas (factors), variables que tienen un conjunto fijo y conocido de valores posibles. Todas las funciones de forcats tienen el prefijo fct_*. Por ejemplo, en este caso usamos fct_reorder() para reordenar las etiquetas de los países en orden de la máxima basada en los registros de movibilidad residencial. Finalmente, creamos una nueva columna ‘resi_real’ para cambiar el valor de referencia, el promedio (baseline), fijado en 0 a 100.

subset_europe <- filter(residential_mobility, 
                        is.na(sub_region_1),
                        !is.na(resi)) %>%
                 left_join(wld, by = c("country_region_code"="iso_a2")) %>% 
                 filter(subregion %in% c("Northern Europe",
                                         "Southern Europe",
                                          "Western Europe",
                                          "Eastern Europe")) %>%
                 mutate(resi_real = resi + 100,
                        region = fct_reorder(country_region, 
                                             resi, 
                                            .fun = "max", 
                                            .desc = FALSE)) %>% 
                select(-geometry, -sub_region_1)

str(subset_europe)
## tibble [3,988 x 7] (S3: tbl_df/tbl/data.frame)
##  $ country_region_code: chr [1:3988] "AT" "AT" "AT" "AT" ...
##  $ country_region     : chr [1:3988] "Austria" "Austria" "Austria" "Austria" ...
##  $ date               : Date[1:3988], format: "2020-02-15" "2020-02-16" ...
##  $ resi               : num [1:3988] -2 -2 0 0 1 0 1 -2 0 -1 ...
##  $ subregion          : chr [1:3988] "Western Europe" "Western Europe" "Western Europe" "Western Europe" ...
##  $ resi_real          : num [1:3988] 98 98 100 100 101 100 101 98 100 99 ...
##  $ region             : Factor w/ 35 levels "Belarus","Ukraine",..: 18 18 18 18 18 18 18 18 18 18 ...

4.4.5 Tablas largas y anchas

Antes de pasar a la visualización con ggplot2. Es muy habitual modificar la tabla entre dos formatos principales. Una tabla es tidy cuando 1) cada variable es una columna 2) cada observación/caso es una fila y 3) cada tipo de unidad observacional forma una tabla.

# subconjunto 
mobility_selection <- select(subset_europe, country_region_code, date:resi)
mobility_selection
## # A tibble: 3,988 x 3
##    country_region_code date        resi
##    <chr>               <date>     <dbl>
##  1 AT                  2020-02-15    -2
##  2 AT                  2020-02-16    -2
##  3 AT                  2020-02-17     0
##  4 AT                  2020-02-18     0
##  5 AT                  2020-02-19     1
##  6 AT                  2020-02-20     0
##  7 AT                  2020-02-21     1
##  8 AT                  2020-02-22    -2
##  9 AT                  2020-02-23     0
## 10 AT                  2020-02-24    -1
## # ... with 3,978 more rows
# tabla ancha
mobi_wide <- pivot_wider(mobility_selection, 
                         names_from = country_region_code,
                         values_from = resi)
mobi_wide
## # A tibble: 114 x 36
##    date          AT    BA    BE    BG    BY    CH    CZ    DE    DK    EE    ES
##    <date>     <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
##  1 2020-02-15    -2    -1    -1     0    -1    -1    -2    -1     0     0    -2
##  2 2020-02-16    -2    -1     1    -3     0    -1    -1     0     1     0    -2
##  3 2020-02-17     0    -1     0    -2     0     1     0     0     1     1    -1
##  4 2020-02-18     0    -1     0    -2     0     1     0     1     1     1     0
##  5 2020-02-19     1    -1     0    -1    -1     1     0     1     1     0    -1
##  6 2020-02-20     0    -1     0     0    -1     0     0     1     1     0    -1
##  7 2020-02-21     1    -2     0    -1    -1     1     0     2     1     1    -2
##  8 2020-02-22    -2    -1     0     0    -2    -2    -3     0     1     0    -2
##  9 2020-02-23     0    -1     0    -3    -1    -1     0     0     0    -2    -3
## 10 2020-02-24    -1    -1     4    -1     0     0     0     4     0    16     0
## # ... with 104 more rows, and 24 more variables: FI <dbl>, FR <dbl>, GB <dbl>,
## #   GR <dbl>, HR <dbl>, HU <dbl>, IE <dbl>, IT <dbl>, LT <dbl>, LU <dbl>,
## #   LV <dbl>, MD <dbl>, MK <dbl>, NL <dbl>, NO <dbl>, PL <dbl>, PT <dbl>,
## #   RO <dbl>, RS <dbl>, RU <dbl>, SE <dbl>, SI <dbl>, SK <dbl>, UA <dbl>
# tabla larga
pivot_longer(mobi_wide,
             2:36,
             names_to = "country_code",
             values_to = "resi")
## # A tibble: 3,990 x 3
##    date       country_code  resi
##    <date>     <chr>        <dbl>
##  1 2020-02-15 AT              -2
##  2 2020-02-15 BA              -1
##  3 2020-02-15 BE              -1
##  4 2020-02-15 BG               0
##  5 2020-02-15 BY              -1
##  6 2020-02-15 CH              -1
##  7 2020-02-15 CZ              -2
##  8 2020-02-15 DE              -1
##  9 2020-02-15 DK               0
## 10 2020-02-15 EE               0
## # ... with 3,980 more rows

Otro grupo de funciones a las que deberías echar un vistazo son: separate(), case_when(), complete(). Podemos encontrar más detalles en el Cheat-Sheet de dplyr

4.5 Visualizar datos

ggplot2 es un sistema moderno, y con una enorme variedad de opciones, para visualización de datos. A diferencia del sistema gráfico de R Base se utiliza una gramática diferente. La gramática de los gráficos (grammar of graphics, de allí “gg”) consiste en la suma de varias capas u objetos independientes que se combinan usando + para construir el gráfico final. ggplot diferencia entre los datos, lo que se visualiza y la forma en que se visualiza.

  • data: nuestro conjunto de datos (data.frame o tibble)

  • aesthetics: con la función aes() indicamos las variables que corresponden a los ejes x, y, z,… o, cuando se pretende aplicar parámetros gráficos (color, size, shape) según una variable. Es posible incluir aes() en ggplot() o en la función correspondiente a una geometría geom_*.

  • geometries: son objetos geom_* que indican la geometría a usar, (p. ej.: geom_point(), geom_line(), geom_boxplot(), etc.).

  • scales: son objetos de tipo scales_* (p. ej.: scale_x_continous(), scale_colour_manual()) para manipular las ejes, definir colores, etc.

  • statistics: son objetos stat_* (p.ej.: stat_density()) que permiten aplicar transformaciones estadísticas.

Podemos encontrar más detalles en el Cheat-Sheet de ggplot2. ggplot es complementado constantemente con extensiones para geometrías u otras opciones gráficas (https://exts.ggplot2.tidyverse.org/ggiraph.html), para obtener ideas gráficas, debes echarle un vistazo a la Galería de Gráficos R (https://www.r-graph-gallery.com/).

4.5.1 Gráfico de linea y puntos

Creamos un subconjunto de nuestros datos de movilidad para residencias y parques, filtrando los registros de regiones italianas. Además, dividimos los valores de movilidad en porcentaje por 100 para obtener la fracción, ya que ggplot2 nos permite indicar la unidad de porcentaje en el argumento de las etiquetas (último gráfico de esta sección).

# creamos el subconjunto
it <- filter(google_mobility, 
             country_region == "Italy", 
             is.na(sub_region_1)) %>% 
      mutate(resi = residential_percent_change_from_baseline/100,   
             parks = parks_percent_change_from_baseline/100)


# gráfico de línea 
ggplot(it, 
       aes(date, resi)) + 
  geom_line()

# gráfico de dispersión con línea de correlación
ggplot(it, 
       aes(parks, resi)) + 
  geom_point() +
  geom_smooth(method = "lm")
## `geom_smooth()` using formula 'y ~ x'

Para modificar los ejes, empleamos las diferentes funciones de scale_* que debemos adpatar a las escalas de medición (date, discrete, continuous, etc.). La función labs() nos ayuda en definir los títulos de ejes, del gráfico y de la leyenda. Por último, añadimos con theme_light() el estilo del gráfico (otros son theme_bw(), theme_minimal(), etc.). También podríamos hacer cambios de todos los elementos gráficos a través de theme().

# time serie plot
ggplot(it, 
       aes(date, resi)) + 
  geom_line(colour = "#560A86", size = 0.8) +
  scale_x_date(date_breaks = "10 days", 
               date_labels = "%d %b") +
  scale_y_continuous(breaks = seq(-0.1, 1, 0.1), 
                     labels = scales::percent) +
  labs(x = "", 
       y = "Residential mobility",
       title = "Mobility during COVID-19") +
  theme_light()

# scatter plot
ggplot(it, 
       aes(parks, resi)) + 
  geom_point(alpha = .4, size = 2) +
  geom_smooth(method = "lm") +
  scale_x_continuous(breaks = seq(-1, 1.4, 0.2), 
                     labels = scales::percent) +
  scale_y_continuous(breaks = seq(-1, 1, 0.1), 
                     labels = scales::percent) +
  labs(x = "Park mobility", 
       y = "Residential mobility",
       title = "Mobility during COVID-19") +
  theme_light()
## `geom_smooth()` using formula 'y ~ x'

4.5.2 Boxplot

Podemos visualizar diferentes aspectos de los datos de movilidad con otras geometrías. Aquí creamos boxplots por cada país europeo representando la variabilidad de movilidad entre y en los países durante la pandemia del COVID-19.

# subconjunto
subset_europe_reg <- filter(residential_mobility, 
                           !is.na(sub_region_1),
                           !is.na(resi)) %>%
                     left_join(wld, by = c("country_region_code"="iso_a2")) %>% 
                     filter(subregion %in% c("Northern Europe",
                                         "Southern Europe",
                                          "Western Europe",
                                          "Eastern Europe")) %>% 
                     mutate(resi = resi/100, 
                            country_region = fct_reorder(country_region, resi))

# boxplot
ggplot(subset_europe_reg, 
       aes(country_region, resi, fill = subregion)) + 
  geom_boxplot() +
  scale_y_continuous(breaks = seq(-0.1, 1, 0.1), labels = scales::percent) +
  scale_fill_brewer(palette = "Set1") +
  coord_flip() +
   labs(x = "", 
       y = "Residential mobility",
       title = "Mobility during COVID-19", 
       fill = "") +
  theme_minimal()

4.5.3 Heatmap

Para visualizar la tendencia de todos los países europeos es recomendable usar un heatmap en lugar de un bulto de líneas. Antes de constuir el gráfico, creamos un vector de fechas para las etiquetas con los domingos en el período de registros.

# secuencia de fechas
df <- data.frame(d = seq(ymd("2020-02-15"), ymd("2020-06-07"), "day"))

# filtramos los domingos creando el día de la semana
sundays <- df %>% 
            mutate(wd = wday(d, week_start = 1)) %>% 
             filter(wd == 7) %>% 
              pull(d)

Si queremos usar etiquetas en otras lenguas, es necesario cambiar la configuración regional del sistema.

Sys.setlocale("LC_TIME", "English")
## [1] "English_United States.1252"

El relleno de color para los boxplots lo dibujamos por cada región de los países europeos. Podemos fijar el tipo de color con scale_fill_*, en este caso, de las gamas viridis.
Además, la función guides() nos permite modificar la barra de color de la leyenda. Por último, aquí vemos el uso de theme() con cambios adicionales a theme_minimal().

# headmap
ggplot(subset_europe, 
       aes(date, region, fill = resi_real)) +
  geom_tile() +
  scale_x_date(breaks = sundays,
               date_labels = "%d %b") +
  scale_fill_viridis_c(option = "A", 
                       breaks = c(91, 146),
                       labels = c("Less", "More"), 
                       direction = -1) +
  theme_minimal() +
  theme(legend.position = "top", 
        title = element_text(size = 14),
        panel.grid.major.x = element_line(colour = "white", linetype = "dashed"),
        panel.grid.minor.x = element_blank(),
        panel.grid.major.y = element_blank(),
        panel.ontop = TRUE,
        plot.margin = margin(r = 1, unit = "cm")) +
  labs(y = "", 
       x = "", 
       fill = "", 
       title = "Mobility trends for places of residence",
       caption = "Data: google.com/covid19/mobility/") +
  guides(fill = guide_colorbar(barwidth = 10, 
                               barheight = .5,
                               label.position = "top", 
                               ticks = FALSE)) +
  coord_cartesian(expand = FALSE)

4.6 Aplicar funciones sobre vectores o listas

El paquete purrr contiene un conjunto de funciones avanzadas de programación funcional para trabajar con funciones y vectores. La familia de funciones lapply() conocido de R Basecorresponde a las funciones de map() en este paquete. Una de las mayores ventajas es poder reducir el uso de bucles (for, etc.).

# lista con dos vectores
vec_list <- list(x = 1:10, y = 50:70)

# calculamos el promedio para cada uno
map(vec_list, mean)
## $x
## [1] 5.5
## 
## $y
## [1] 60
# podemos cambiar tipo de salida map_* (dbl, chr, lgl, etc.)
map_dbl(vec_list, mean)
##    x    y 
##  5.5 60.0

Un ejemplo más complejo. Calculamos el coeficiente de correlación entre la movilidad residencial y la de los parques en todos los países europeos. Para obtener un resumen tidy de un modelo o un test usamos la función tidy() del paquete broom.

library(broom) # tidy outputs

# función adaptada 
cor_test <- function(x, formula) { 
  
df <- cor.test(as.formula(formula), data = x) %>% tidy()

return(df)
  
}

# preparamos los datos
europe_reg <- filter(google_mobility, 
                           !is.na(sub_region_1),
                           !is.na(residential_percent_change_from_baseline)) %>%
                     left_join(wld, by = c("country_region_code"="iso_a2")) %>% 
                     filter(subregion %in% c("Northern Europe",
                                         "Southern Europe",
                                          "Western Europe",
                                          "Eastern Europe"))
# aplicamos la función a cada país creando una lista
europe_reg %>%
  split(.$country_region_code) %>% 
  map(cor_test, formula = "~ residential_percent_change_from_baseline + parks_percent_change_from_baseline")  
## $AT
## # A tibble: 1 x 8
##   estimate statistic  p.value parameter conf.low conf.high method    alternative
##      <dbl>     <dbl>    <dbl>     <int>    <dbl>     <dbl> <chr>     <chr>      
## 1   -0.360     -12.3 2.68e-32      1009   -0.413    -0.305 Pearson'~ two.sided  
## 
## $BE
## # A tibble: 1 x 8
##   estimate statistic     p.value parameter conf.low conf.high method alternative
##      <dbl>     <dbl>       <dbl>     <int>    <dbl>     <dbl> <chr>  <chr>      
## 1   -0.312     -6.06     3.67e-9       340   -0.405    -0.213 Pears~ two.sided  
## 
## $BG
## # A tibble: 1 x 8
##   estimate statistic   p.value parameter conf.low conf.high method   alternative
##      <dbl>     <dbl>     <dbl>     <int>    <dbl>     <dbl> <chr>    <chr>      
## 1   -0.677     -37.8 1.47e-227      1694   -0.702    -0.650 Pearson~ two.sided  
## 
## $CH
## # A tibble: 1 x 8
##   estimate statistic p.value parameter conf.low conf.high method     alternative
##      <dbl>     <dbl>   <dbl>     <int>    <dbl>     <dbl> <chr>      <chr>      
## 1  -0.0786     -2.91 0.00370      1360   -0.131   -0.0256 Pearson's~ two.sided  
## 
## $CZ
## # A tibble: 1 x 8
##   estimate statistic  p.value parameter conf.low conf.high method    alternative
##      <dbl>     <dbl>    <dbl>     <int>    <dbl>     <dbl> <chr>     <chr>      
## 1  -0.0837     -3.35 0.000824      1593   -0.132   -0.0347 Pearson'~ two.sided  
## 
## $DE
## # A tibble: 1 x 8
##   estimate statistic p.value parameter conf.low conf.high method     alternative
##      <dbl>     <dbl>   <dbl>     <int>    <dbl>     <dbl> <chr>      <chr>      
## 1  0.00239     0.102   0.919      1814  -0.0436    0.0484 Pearson's~ two.sided  
## 
## $DK
## # A tibble: 1 x 8
##   estimate statistic     p.value parameter conf.low conf.high method alternative
##      <dbl>     <dbl>       <dbl>     <int>    <dbl>     <dbl> <chr>  <chr>      
## 1    0.237      5.81     1.04e-8       567    0.158     0.313 Pears~ two.sided  
## 
## $EE
## # A tibble: 1 x 8
##   estimate statistic p.value parameter conf.low conf.high method     alternative
##      <dbl>     <dbl>   <dbl>     <int>    <dbl>     <dbl> <chr>      <chr>      
## 1   -0.235     -2.88 0.00462       142   -0.384   -0.0740 Pearson's~ two.sided  
## 
## $ES
## # A tibble: 1 x 8
##   estimate statistic p.value parameter conf.low conf.high method     alternative
##      <dbl>     <dbl>   <dbl>     <int>    <dbl>     <dbl> <chr>      <chr>      
## 1   -0.825     -65.4       0      2005   -0.839    -0.811 Pearson's~ two.sided  
## 
## $FI
## # A tibble: 1 x 8
##   estimate statistic p.value parameter conf.low conf.high method     alternative
##      <dbl>     <dbl>   <dbl>     <int>    <dbl>     <dbl> <chr>      <chr>      
## 1   0.0427      1.42   0.155      1106  -0.0162     0.101 Pearson's~ two.sided  
## 
## $FR
## # A tibble: 1 x 8
##   estimate statistic   p.value parameter conf.low conf.high method   alternative
##      <dbl>     <dbl>     <dbl>     <int>    <dbl>     <dbl> <chr>    <chr>      
## 1   -0.698     -37.4 3.29e-216      1474   -0.723    -0.671 Pearson~ two.sided  
## 
## $GB
## # A tibble: 1 x 8
##   estimate statistic  p.value parameter conf.low conf.high method    alternative
##      <dbl>     <dbl>    <dbl>     <int>    <dbl>     <dbl> <chr>     <chr>      
## 1   -0.105     -11.0 9.19e-28     10712   -0.124   -0.0865 Pearson'~ two.sided  
## 
## $GR
## # A tibble: 1 x 8
##   estimate statistic   p.value parameter conf.low conf.high method   alternative
##      <dbl>     <dbl>     <dbl>     <int>    <dbl>     <dbl> <chr>    <chr>      
## 1   -0.692     -27.0 1.03e-114       796   -0.726    -0.654 Pearson~ two.sided  
## 
## $HR
## # A tibble: 1 x 8
##   estimate statistic  p.value parameter conf.low conf.high method    alternative
##      <dbl>     <dbl>    <dbl>     <int>    <dbl>     <dbl> <chr>     <chr>      
## 1   -0.579     -21.9 9.32e-87       954   -0.620    -0.536 Pearson'~ two.sided  
## 
## $HU
## # A tibble: 1 x 8
##   estimate statistic  p.value parameter conf.low conf.high method    alternative
##      <dbl>     <dbl>    <dbl>     <int>    <dbl>     <dbl> <chr>     <chr>      
## 1   -0.342     -15.6 6.71e-52      1843   -0.382    -0.301 Pearson'~ two.sided  
## 
## $IE
## # A tibble: 1 x 8
##   estimate statistic  p.value parameter conf.low conf.high method    alternative
##      <dbl>     <dbl>    <dbl>     <int>    <dbl>     <dbl> <chr>     <chr>      
## 1   -0.222     -8.45 7.49e-17      1378   -0.271    -0.171 Pearson'~ two.sided  
## 
## $IT
## # A tibble: 1 x 8
##   estimate statistic p.value parameter conf.low conf.high method     alternative
##      <dbl>     <dbl>   <dbl>     <int>    <dbl>     <dbl> <chr>      <chr>      
## 1   -0.831     -71.0       0      2250   -0.844    -0.818 Pearson's~ two.sided  
## 
## $LT
## # A tibble: 1 x 8
##   estimate statistic     p.value parameter conf.low conf.high method alternative
##      <dbl>     <dbl>       <dbl>     <int>    <dbl>     <dbl> <chr>  <chr>      
## 1   -0.204     -5.45     7.17e-8       686   -0.274    -0.131 Pears~ two.sided  
## 
## $LV
## # A tibble: 1 x 8
##   estimate statistic  p.value parameter conf.low conf.high method    alternative
##      <dbl>     <dbl>    <dbl>     <int>    <dbl>     <dbl> <chr>     <chr>      
## 1   -0.544     -6.87 3.84e-10       112   -0.662    -0.401 Pearson'~ two.sided  
## 
## $NL
## # A tibble: 1 x 8
##   estimate statistic     p.value parameter conf.low conf.high method alternative
##      <dbl>     <dbl>       <dbl>     <int>    <dbl>     <dbl> <chr>  <chr>      
## 1    0.143      5.31 0.000000125      1356   0.0903     0.195 Pears~ two.sided  
## 
## $NO
## # A tibble: 1 x 8
##   estimate statistic p.value parameter conf.low conf.high method     alternative
##      <dbl>     <dbl>   <dbl>     <int>    <dbl>     <dbl> <chr>      <chr>      
## 1   0.0483      1.69  0.0911      1221 -0.00774     0.104 Pearson's~ two.sided  
## 
## $PL
## # A tibble: 1 x 8
##   estimate statistic   p.value parameter conf.low conf.high method   alternative
##      <dbl>     <dbl>     <dbl>     <int>    <dbl>     <dbl> <chr>    <chr>      
## 1   -0.531     -26.7 6.08e-133      1815   -0.564    -0.498 Pearson~ two.sided  
## 
## $PT
## # A tibble: 1 x 8
##   estimate statistic   p.value parameter conf.low conf.high method   alternative
##      <dbl>     <dbl>     <dbl>     <int>    <dbl>     <dbl> <chr>    <chr>      
## 1   -0.729     -46.9 2.12e-321      1938   -0.749    -0.707 Pearson~ two.sided  
## 
## $RO
## # A tibble: 1 x 8
##   estimate statistic p.value parameter conf.low conf.high method     alternative
##      <dbl>     <dbl>   <dbl>     <int>    <dbl>     <dbl> <chr>      <chr>      
## 1   -0.640     -56.0       0      4517   -0.657    -0.623 Pearson's~ two.sided  
## 
## $SE
## # A tibble: 1 x 8
##   estimate statistic   p.value parameter conf.low conf.high method   alternative
##      <dbl>     <dbl>     <dbl>     <int>    <dbl>     <dbl> <chr>    <chr>      
## 1    0.106      3.93 0.0000909      1367   0.0529     0.158 Pearson~ two.sided  
## 
## $SI
## # A tibble: 1 x 8
##   estimate statistic  p.value parameter conf.low conf.high method    alternative
##      <dbl>     <dbl>    <dbl>     <int>    <dbl>     <dbl> <chr>     <chr>      
## 1   -0.627     -11.4 1.98e-23       200   -0.704    -0.535 Pearson'~ two.sided  
## 
## $SK
## # A tibble: 1 x 8
##   estimate statistic     p.value parameter conf.low conf.high method alternative
##      <dbl>     <dbl>       <dbl>     <int>    <dbl>     <dbl> <chr>  <chr>      
## 1   -0.196     -5.70     1.65e-8       810   -0.262    -0.129 Pears~ two.sided

Como ya hemos visto anteriormente, existen subfunciones de map_* para obtener en lugar de una lista un objeto de otra clase, aquí de data.frame.

cor_mobility <- europe_reg %>%
                  split(.$country_region_code) %>% 
                     map_df(cor_test, 
                            formula = "~ residential_percent_change_from_baseline + parks_percent_change_from_baseline", 
                            .id = "country_code")

arrange(cor_mobility, estimate)
## # A tibble: 27 x 9
##    country_code estimate statistic   p.value parameter conf.low conf.high method
##    <chr>           <dbl>     <dbl>     <dbl>     <int>    <dbl>     <dbl> <chr> 
##  1 IT             -0.831    -71.0  0              2250   -0.844    -0.818 Pears~
##  2 ES             -0.825    -65.4  0              2005   -0.839    -0.811 Pears~
##  3 PT             -0.729    -46.9  2.12e-321      1938   -0.749    -0.707 Pears~
##  4 FR             -0.698    -37.4  3.29e-216      1474   -0.723    -0.671 Pears~
##  5 GR             -0.692    -27.0  1.03e-114       796   -0.726    -0.654 Pears~
##  6 BG             -0.677    -37.8  1.47e-227      1694   -0.702    -0.650 Pears~
##  7 RO             -0.640    -56.0  0              4517   -0.657    -0.623 Pears~
##  8 SI             -0.627    -11.4  1.98e- 23       200   -0.704    -0.535 Pears~
##  9 HR             -0.579    -21.9  9.32e- 87       954   -0.620    -0.536 Pears~
## 10 LV             -0.544     -6.87 3.84e- 10       112   -0.662    -0.401 Pears~
## # ... with 17 more rows, and 1 more variable: alternative <chr>

Otros ejemplos prácticos aquí en este post or este otro. Podemos encontrar más detalles en el Cheat-Sheet de purrr.

Buy Me A Coffee

Dr. Dominic Royé
Dr. Dominic Royé
Investigador y responsable de ciencia de datos

Relacionado