4.2 Manipulación de datos con dplyr y tidyr

En esta sección se realiza una breve introducción al paquete dplyr y se comentan algunas de las utilidades del paquete tidyr que pueden resultar de interés5.

La referencia recomendada para iniciarse en esta herramienta es el Capítulo 5 Data transformation de R for Data Science. También puede resultar de utilidad la viñeta del paquete Introduction to dplyr o la chuleta (menú de RStudio Help > Cheat Sheets > Data Transformation with dplyr).

4.2.1 El paquete dplyr

library(dplyr)

La principal ventaja de dplyr es que permite trabajar (de la misma forma) con datos en distintos formatos:

  • data.frame, tibble.

  • data.table: extensión (paquete backend) dtplyr.

  • conjuntos de datos más grandes que la memoria disponible: extensiones duckdb y arrow (incluyendo almacenamiento en la nube, e.g. AWS).

  • bases de datos relacionales (lenguaje SQL, locales o remotas); extensión dbplyr.

  • grandes volúmenes de datos (incluso almacenados en múltiples servidores; ecosistema Hadoop/Spark): extensión sparklyr (ver menú de RStudio Help > Cheat Sheets > Interfacing Spark with sparklyr).

El paquete dplyr permite sustituir operaciones con funciones base de R (como subset, split, apply, sapply, lapply, tapply, aggregate…) por una “gramática” más sencilla para la manipulación de datos. En lugar de operar sobre vectores como la mayoría de las funciones base, opera sobre conjuntos de datos (de forma que es compatible con el operador %>%). Los principales “verbos” (funciones) son:

  • select(): seleccionar variables (ver también rename, relocate, pull).

  • mutate(): crear variables (ver también transmute()).

  • filter(): seleccionar casos/filas (ver también slice()).

  • arrange(): ordenar casos/filas.

  • summarise(): resumir valores.

  • group_by(): permite operaciones por grupo empleando el concepto “dividir-aplicar-combinar” (ungroup() elimina el agrupamiento).

NOTA: Para entender el funcionamiento de ciertas funciones (como rowwise()) y las posibilidades en el manejo de datos, hay que tener en cuenta que un data.frame no es más que una lista cuyas componentes (variables) tienen la misma longitud. Realmente las componentes también pueden ser listas de la misma longitud y, por tanto, podemos almacenar casi cualquier estructura de datos en un data.frame.

En la primera parte de este capítulo consideraremos solo data.frame por comodidad. Emplearemos como ejemplo los datos de empleados de banca almacenados en el fichero empleados.RData (y supondremos que estamos interesados en estudiar si hay discriminación por cuestión de sexo o raza).

load("data/empleados.RData")
attr(empleados, "variable.labels") <- NULL                  

En la Sección 4.5 final emplearemos una base de datos relacional como ejemplo.

4.2.2 Operaciones con variables (columnas)

Podemos seleccionar variables con select():

emplea2 <- empleados %>% select(id, sexo, minoria, tiempemp, 
                                salini, salario)
head(emplea2)
##   id   sexo minoria tiempemp salini salario
## 1  1 Hombre      No       98  27000   57000
## 2  2 Hombre      No       98  18750   40200
## 3  3  Mujer      No       98  12000   21450
## 4  4  Mujer      No       98  13200   21900
## 5  5 Hombre      No       98  21000   45000
## 6  6 Hombre      No       98  13500   32100

Se puede cambiar el nombre (ver también rename()):

empleados %>% select(sexo, noblanca = minoria, salario) %>% head()
##     sexo noblanca salario
## 1 Hombre       No   57000
## 2 Hombre       No   40200
## 3  Mujer       No   21450
## 4  Mujer       No   21900
## 5 Hombre       No   45000
## 6 Hombre       No   32100

Se pueden emplear los nombres de variables como índices:

empleados %>% select(sexo:salario) %>% head()
##     sexo    fechnac educ         catlab salario
## 1 Hombre 1952-02-03   15      Directivo   57000
## 2 Hombre 1958-05-23   16 Administrativo   40200
## 3  Mujer 1929-07-26   12 Administrativo   21450
## 4  Mujer 1947-04-15    8 Administrativo   21900
## 5 Hombre 1955-02-09   15 Administrativo   45000
## 6 Hombre 1958-08-22   15 Administrativo   32100
# empleados %>% select(-(sexo:salario)) %>% head()
empleados %>% select(!(sexo:salario)) %>% head()
##   id salini tiempemp expprev minoria     sexoraza
## 1  1  27000       98     144      No Blanca varón
## 2  2  18750       98      36      No Blanca varón
## 3  3  12000       98     381      No Blanca mujer
## 4  4  13200       98     190      No Blanca mujer
## 5  5  21000       98     138      No Blanca varón
## 6  6  13500       98      67      No Blanca varón

Se pueden emplear distintas herramientas (selection helpers) para seleccionar variables (ver paquete tidyselect):

Por ejemplo:

empleados %>% select(starts_with("s")) %>% head()
##     sexo salario salini     sexoraza
## 1 Hombre   57000  27000 Blanca varón
## 2 Hombre   40200  18750 Blanca varón
## 3  Mujer   21450  12000 Blanca mujer
## 4  Mujer   21900  13200 Blanca mujer
## 5 Hombre   45000  21000 Blanca varón
## 6 Hombre   32100  13500 Blanca varón

Podemos crear variables con mutate():

emplea2 %>% 
  mutate(incsal = salario - salini, tsal = incsal/tiempemp) %>% 
  head()
##   id   sexo minoria tiempemp salini salario incsal      tsal
## 1  1 Hombre      No       98  27000   57000  30000 306.12245
## 2  2 Hombre      No       98  18750   40200  21450 218.87755
## 3  3  Mujer      No       98  12000   21450   9450  96.42857
## 4  4  Mujer      No       98  13200   21900   8700  88.77551
## 5  5 Hombre      No       98  21000   45000  24000 244.89796
## 6  6 Hombre      No       98  13500   32100  18600 189.79592

4.2.3 Operaciones con casos (filas)

Podemos seleccionar casos con filter():

emplea2 %>% filter(sexo == "Mujer", minoria == "Sí") %>% head()
## [1] id       sexo     minoria  tiempemp salini   salario 
## <0 rows> (o 0- extensión row.names)

Podemos reordenar casos con arrange():

emplea2 %>% arrange(salario) %>% head()
##    id  sexo minoria tiempemp salini salario
## 1 378 Mujer      No       70  10200   15750
## 2 338 Mujer      No       74  10200   15900
## 3  90 Mujer      No       92   9750   16200
## 4 224 Mujer      No       82  10200   16200
## 5 411 Mujer      No       68  10200   16200
## 6 448 Mujer   S\xed       66  10200   16350
emplea2 %>% arrange(desc(salini), salario) %>% head()
##    id   sexo minoria tiempemp salini salario
## 1  29 Hombre      No       96  79980  135000
## 2 343 Hombre      No       73  60000  103500
## 3 205 Hombre      No       83  52500   66750
## 4 160 Hombre      No       86  47490   66000
## 5 431 Hombre      No       66  45000   86250
## 6  32 Hombre      No       96  45000  110625

Podemos resumir valores con summarise():

empleados %>% summarise(sal.med = mean(salario), n = n())
##    sal.med   n
## 1 34419.57 474

Para realizar operaciones con múltiples variables podemos emplear across() (admite selección de variables tidyselect):

empleados %>% summarise(across(where(is.numeric), mean), n = n())
##      id     educ  salario   salini tiempemp  expprev   n
## 1 237.5 13.49156 34419.57 17016.09  81.1097 95.86076 474
# empleados %>% summarise(across(where(is.numeric) & !id, mean), n = n())

NOTA: Esta función sustituye a las “variantes de ámbito” _at(), _if() y _all() de versiones anteriores de dplyr (como summarise_at(), summarise_if(), summarise_all(), mutate_at(), mutate_if()…) y también el uso de vars(). En el caso de filter() se puede emplear if_any() e if_all().

Podemos agrupar casos con group_by():

empleados %>% group_by(sexo, minoria) %>% 
    summarise(sal.med = mean(salario), n = n()) %>%
    ungroup()
## # A tibble: 4 × 4
##   sexo   minoria sal.med     n
##   <fct>  <fct>     <dbl> <int>
## 1 Hombre No       44475.   194
## 2 Hombre S�       32246.    64
## 3 Mujer  No       26707.   176
## 4 Mujer  S�       23062.    40
empleados %>% group_by(sexo, minoria) %>% 
    summarise(sal.med = mean(salario), n = n(), .groups = "drop")
## # A tibble: 4 × 4
##   sexo   minoria sal.med     n
##   <fct>  <fct>     <dbl> <int>
## 1 Hombre No       44475.   194
## 2 Hombre S�       32246.    64
## 3 Mujer  No       26707.   176
## 4 Mujer  S�       23062.    40
# dplyr >= 1.1.0 # packageVersion("dplyr")
# empleados %>% summarise(sal.med = mean(salario), n = n(), 
#                         .by = c(sexo, minoria))

Por defecto la agrupación se mantiene para el resto de operaciones, habría que emplear ungroup() (o el argumento .groups = "drop") para eliminarla (se puede emplear group_vars() o str() para ver la agrupación). Desde dplyr 1.1.0 (2023-01-29) está disponible un parámetro .by/by en mutate(), summarise(), filter() y slice() como alternativa a agrupar y desagrupar posteriormente. Para más detalles ver Per-operation grouping with .by/by.

4.2.4 Datos faltantes

Continuamos con el ejemplo de la Sección @ref{missing}. tidyverse dispone de muchas herramientas para el tratamiento de los datos faltantes.

data("airquality")
datos <- airquality
library(visdat)
vis_dat(airquality)
# vis_miss(airquality)

Visualización (amigable) de la estrutura de datos:

library(naniar)
bind_shadow(airquality)
## # A tibble: 153 × 12
##    Ozone Solar.R  Wind  Temp Month   Day Ozone_NA Solar.R_NA Wind_NA Temp_NA
##    <int>   <int> <dbl> <int> <int> <int> <fct>    <fct>      <fct>   <fct>  
##  1    41     190   7.4    67     5     1 !NA      !NA        !NA     !NA    
##  2    36     118   8      72     5     2 !NA      !NA        !NA     !NA    
##  3    12     149  12.6    74     5     3 !NA      !NA        !NA     !NA    
##  4    18     313  11.5    62     5     4 !NA      !NA        !NA     !NA    
##  5    NA      NA  14.3    56     5     5 NA       NA         !NA     !NA    
##  6    28      NA  14.9    66     5     6 !NA      NA         !NA     !NA    
##  7    23     299   8.6    65     5     7 !NA      !NA        !NA     !NA    
##  8    19      99  13.8    59     5     8 !NA      !NA        !NA     !NA    
##  9     8      19  20.1    61     5     9 !NA      !NA        !NA     !NA    
## 10    NA     194   8.6    69     5    10 NA       !NA        !NA     !NA    
## # ℹ 143 more rows
## # ℹ 2 more variables: Month_NA <fct>, Day_NA <fct>
# nabular(airquality)

Distribución por variables de los datos faltantes:

miss_var_table(airquality) 
## # A tibble: 3 × 3
##   n_miss_in_var n_vars pct_vars
##           <int>  <int>    <dbl>
## 1             0      4     66.7
## 2             7      1     16.7
## 3            37      1     16.7
prop_miss_case(airquality)
## [1] 0.2745098
gg_miss_upset(airquality) 

Distribución conjunta de los valores faltantes para la radiación solar y ozono:

library(naniar)
library(ggplot2)
ggplot(airquality, 
       aes(x = Solar.R, 
           y = Ozone)) + 
  geom_miss_point()

Distribución mensual de los valores faltantes:

# gg_miss_var(airquality)
gg_miss_var(airquality, facet = Month)


  1. Otra alternativa (más rápida) es data.table pero en versiones recientes ya se puede emplear desde dplyr, como se comenta más adelante.↩︎