13 Dplyr (Data Manipulation)

13.1 Data Frames

El Data Frames es una estructura de datos clave en estadísticas y en R. La estructura básica de un data.frame es que hay una observación por fila y cada columna que representa una variable, una medida, característica o característica de esa observación. R tiene una implementación interna de tramas de datos que probablemente sea la que usará con más frecuencia. Sin embargo, hay paquetes en CRAN que implementan data frames a través de cosas como bases de datos relacionales que le permiten operar en data frames muy grandes.

Dada la importancia de administrar data frames, es importante que se tengan buenas herramientas para manejarlos. En capítulos anteriores ya se ha discutido algunas herramientas como la función subset() y el uso de operadores [ y $ para extraer subconjuntos de data frames. Sin embargo, otras operaciones, como filtrar, reordenar y colapsar, a menudo pueden ser operaciones tediosas en R cuya sintaxis no es muy intuitiva. El paquete dplyr está diseñado para mitigar muchos de estos problemas y proporcionar un conjunto de rutinas altamente optimizado específicamente para tratar con tramas de datos.

13.2 El paquete dplyr

El paquete dplyr no proporciona ninguna funcionalidad “nueva” a R per se, en el sentido de que todo lo que hace dplyr ya se podría hacer con R base, pero en gran medida simplifica la funcionalidad existente en R.

Una contribución importante del paquete dplyr es que proporciona una “gramática” (en particular, verbos) para la manipulación de datos y para operar en tramas de datos. Con esta gramática, puede comunicar con sensatez qué es lo que está haciendo en un data.frame que otras personas pueden entender (suponiendo que también conozcan la gramática). Esto es útil porque proporciona una abstracción para la manipulación de datos que antes no existía.

13.3 Gramática

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

  • select : devuelve un subconjunto de las columnas de un data.frame, usando una notación flexible

  • filtro : extrae un subconjunto de filas de un data.frame basado en condiciones lógicas

  • arrange : reordenar las filas de un data.frame

  • rename : cambia el nombre de las variables en un data.frame

  • mutate : agregar nuevas variables/columnas o transformar variables existentes

  • summarise / summarize : generar estadísticas resumidas de diferentes variables en el data.frame, posiblemente dentro de los estratos.

  • %>% : el operador “tubería” o pipeline se usa para conectar múltiples acciones verbales en un solo código como una tubería.

El paquete dplyr como un número de sus propios tipos de datos que aprovecha. Por ejemplo, hay un práctico método de print que impide imprimir una gran cantidad de datos en la consola. La mayoría de las veces, estos tipos de datos adicionales son transparentes para el usuario y no es necesario preocuparse por ellos.

13.3.1 Propiedades comunes de la función dplyr

Todas las funciones que se discutiran tendrán algunas características comunes. En particular:

  1. El primer argumento es un data.frame.

  2. Los argumentos subsiguientes describen qué hacer con el data.frame especificado en el primer argumento, y puede hacer referencia a las columnas en el data.frame directamente sin usar el operador $ (solo usando los nombres de las columnas).

  3. El resultado de retorno de una función es un nuevo data.frame.

  4. Los data frames deben estar correctamente formateados y anotados para que todo esto sea útil. En particular, los datos deben estar ordenados. En resumen, debe haber una observación por fila y cada columna debe representar un rasgo o característica de esa observación.

13.4 Instalar el paquete dplyr

El paquete dplyr se puede instalar desde CRAN o desde GitHub usando el paquete devtools y la función install_github() . El repositorio de GitHub normalmente contendrá las últimas actualizaciones del paquete y la versión de desarrollo.

Para instalar desde CRAN, simplemente se ejecuta.

instalar.paquetes ( "dplyr" )

Para instalar desde GitHub puedes ejecutar

devtools::install_github("hadley/dplyr ")

Después de instalar el paquete, es importante cargarlo en la sesión de R con la función library() .

Es posible que se reciban algunas advertencias cuando se carga el paquete porque hay funciones en el paquete dplyr que tienen el mismo nombre que funciones en otros paquetes. Por ahora se pueden ignorar las advertencias.

13.5 select()

Se usarán un conjunto de datos que contiene datos de temperatura y contaminación del aire para la [ciudad de Chicago] (http://www.biostat.jhsph.edu/~rpeng/leanpub/rprog/chicago_data.zip) en EE. UU. El conjunto de datos está disponible en mi sitio web.

Después de descomprimir el archivo, puede cargar los datos en R usando la función readRDS() .

chicago <- readRDS("Bases/chicago.rds")

Se peden ver algunas características básicas del conjunto de datos 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" "1987-01-03" "1987-01-04" ...
##  $ 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 ...

La función select() se puede usar para seleccionar columnas de un data.frame en el que se desea enfocar. A menudo, se tendrá un data.frame que contendrá “muchos datos”, pero cualquier análisis dado solo se puede usar un subconjunto de variables u observaciones. La función select() permite obtener las pocas columnas que pueda necesitar.

Supongamos que se quisiera tomar solo las primeras 3 columnas. Hay algunas maneras de hacer esto. Se podría, por ejemplo, utilizar índices numéricos. Pero también se puede usar los nombres directamente.

names(chicago)[1:3]
## [1] "city" "tmpd" "dptp"

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

subset <- select(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

Tenga en cuenta que : normalmente no se puede usar con nombres o cadenas, pero dentro de la función select() see puede usarla para especificar un rango de nombres de variables.

También se pueden omitir variables usando la función select() usando el signo negativo. Con select() se puede hacer.

select(chicago, -(city:dptp))

Lo que indica que se deben incluir todas las variables excepto las variables city a dptp . El código equivalente en base R base sería:

i <- match("city", names(chicago))
j <- match("dptp", names(chicago))
head(chicago[, -(i:j)])

No es muy relamente intuitivo.

La función select() también permite una sintaxis especial que permite especificar nombres de variables basados en patrones. Por ejemplo, si se quisiera mantener todas las variables que terminan en “2”, se podría 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 se quisiera mantener todas las variables que comienzan con una “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" "1987-01-03" "1987-01-04" ...

También se pueden usar expresiones regulares más generales si es necesario. Consulte la página de ayuda ( ?select ) para obtener más detalles.

13.6 filter()

La función filter() se utiliza para extraer subconjuntos de filas de un data.frame. Esta función es similar a la función subset() existente en R, pero es un poco más rápida e intuitiva.

Suponemos que se quisiera extraer las filas del data.frame chicago donde los niveles de PM2.5 son superiores a 30 (que es un nivel razonablemente alto), se podría 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" "1998-04-30" "1998-05-01" ...
##  $ 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 ...

Se puede ver que ahora solo hay filas 194 en el data.frame y la distribución de los valores 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

Se puede colocar una secuencia lógica arbitrariamente compleja dentro de filter() , por lo que se podría extraer las filas donde PM2.5 es superior a 30 y la temperatura es superior a 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

Ahora solo hay observaciones 17 donde se cumplen ambas condiciones.

13.7 arrange()

La función arrange() se usa para reordenar las filas de un data.frame de acuerdo con una de las variables/columnas. Reordenar filas de un data.frame (manteniendo el orden correspondiente de otras columnas) normalmente es una molestia en R. La función arrange() simplifica bastante el proceso.

Aquí se puede ordenar las filas del data.frame por fecha, de modo que la primera fila sea la observación más antigua (la más antigua) y la última fila sea la observación más reciente (la más reciente).

chicago <- arrange(chicago, date)

Ahora se puedne comprobar las primeras filas

head(select(chicago, date, pm25tmean2), 3)
##         date pm25tmean2
## 1 1987-01-01         NA
## 2 1987-01-02         NA
## 3 1987-01-03         NA

y las últimas filas.

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 organizar en orden descendente mediante el uso del operador especial desc() .

chicago <- arrange(chicago, desc(date))

Mirando las primeras tres y últimas tres filas se muestran las fechas 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

13.8 rename()

Cambiar el nombre de una variable en un data.frame en R es sorprendentemente difícil de hacer. La función rename() está diseñada para facilitar este proceso.

Aquí se pueden ver los nombres de las primeras cinco variables en el data.frame 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

Se supone que la columna dptp representa la temperatura del punto de rocío y la columna pm25tmean2 proporciona los datos de PM2.5. Sin embargo, estos nombres son bastante oscuros o incómodos y probablemente se les cambie el nombre a algo más sensato.

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 anterior en el lado derecho.

Lo dejo como ejercicio para que el lector descubra cómo hacer esto en base R sin dplyr .

13.9 mutate()

La función mutate() existe para calcular transformaciones de variables en un data.frame. A menudo, desea crear nuevas variables derivadas de variables existentes y mutate() proporciona una interfaz limpia para hacerlo.

Por ejemplo, con los datos de contaminación del aire, a menudo se desea eliminar la tendencia de los datos restando la media de los datos. De esa manera, se puede ver si el nivel de contaminación del aire de un día determinado es mayor o menor que el promedio (en lugar de ver su nivel absoluto).

Se crea 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 pm25detrend
## 1 chic   35     30.1 2005-12-31 15.00000       23.5  2.531250  13.25000   -1.230958
## 2 chic   36     31.0 2005-12-30 15.05714       19.2  3.034420  22.80556   -1.173815
## 3 chic   35     29.4 2005-12-29  7.45000       23.5  6.794837  19.97222   -8.780958
## 4 chic   37     34.5 2005-12-28 17.75000       27.5  3.260417  19.28563    1.519042
## 5 chic   40     33.6 2005-12-27 23.56000       27.0  4.468750  23.50000    7.329042
## 6 chic   35     29.6 2005-12-26  8.40000        8.5 14.041667  16.81944   -7.830958

También está la función transmute() relacionada , que hace lo mismo que mutate() pero luego elimina todas las variables no transformadas .

Aquí se elimina 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

Teniendo en cuenta que solo hay dos columnas en el data.frame transmutado.

13.10 group_by()

La función group_by() se utiliza para generar estadísticas de resumen a partir del data.frame dentro de los estratos definidos por una variable. Por ejemplo, en este conjunto de datos de contaminación del aire, es posible que se desee saber cuál es el nivel promedio anual de PM2.5. Así que el estrato es el año, y eso es algo que se puede derivar de la variable date . Junto con la función group_by() ,a menudo se usa la función summarize().

La operación general aquí es una combinación de dividir un data.frame en partes separadas definidas por una variable o grupo de variables por group_by(), y luego aplicar una función de resumen a través de esos subconjuntos, utilizando la función summarise().

Primero, se puede crear una variable year usando as.POSIXlt() .

chicago <- mutate(chicago, year = as.POSIXlt(date)$year + 1900)

Ahora se puede crear un data.frame separado que divida el data.frame original por año.

years <- group_by(chicago, year)

Finalmente, se calculan las estadísticas resumidas para cada año en el data.frame con la función summarise() .

summarise(years, 
           pm25 = mean(pm25, na.rm = TRUE), 
           o3 = max(o3tmean2, na.rm = TRUE), 
           no2 = median(no2tmean2, na.rm = TRUE),
           .groups = "drop")
## # 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

summarise() devuelve un data.frame con año como la primera columna, y luego los promedios anuales de pm25 , o3 y no2 .

En un ejemplo un poco más complicado, se podría querer saber cuáles son los niveles promedio de ozono ( o3 ) y dióxido de nitrógeno ( no2 ) dentro de los quintiles de pm25 . Una forma más ingeniosa de hacer esto sería a través de un modelo de regresión, pero en realidad se puede hacerlo rápidamente con group_by() y summarize() .

Primero, se puede crear una variable categórica de pm25 dividida en quintiles.

qq <- quantile(chicago$pm25, seq(0, 1, 0.2), na.rm = TRUE)
chicago <- mutate(chicago, pm25.quint = cut(pm25, qq))

Ahora se puede agrupar el data.frame por la variable pm25.quint.

quint <- group_by(chicago, pm25.quint)

Finalmente, se puede calcular la media de o3 y no2 dentro de los quintiles de pm25 .

summarize(quint, o3 = mean(o3tmean2, na.rm = TRUE), 
          no2 = mean(no2tmean2, na.rm = TRUE),
          .groups = "drop")
## # 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

De la tabla, parece que no hay una fuerte relación entre pm25 y o3 , pero parece haber una correlación positiva entre pm25 y no2 . Los modelos estadísticos más sofisticados pueden ayudar a proporcionar respuestas precisas a estas preguntas, pero una simple aplicación de las funciones dplyr a menudo puede ayudarlo a llegar hasta allí.

13.11 %>%

El operador de canalización %>% es muy útil para encadenar varias funciones dplyr en una secuencia de operaciones. Notesé que arriba que cada vez que queríamos aplicar más de una función, la secuencia queda enterrada en una secuencia de llamadas a funciones anidadas que es difícil de leer, es decir.

third(second(first(x)))

Este anidamiento no es una forma natural de pensar en una secuencia de operaciones. El operador %>% permite encadenar operaciones de izquierda a derecha, es decir

first(x) %>% second %>% third

Tomando el ejemplo anterior donde se calculó la media de o3 y no2 dentro de los quintiles de pm25. Se tuvo que:

  1. crea una nueva variable pm25.quint
  2. dividir el data.frame 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 R.

chicago %>% 
  mutate(., pm25.quint = cut(pm25, qq)) %>%    
   group_by(pm25.quint) %>% 
    summarize(o3 = mean(o3tmean2, na.rm = TRUE), 
               no2 = mean(no2tmean2, na.rm = TRUE),
                .groups = "drop")
## # 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

De esta manera, no se tiene que crear un conjunto de variables temporales en el camino o crear una secuencia anidada masiva de llamadas a funciones.

Observesé en el código anterior que paso el data.frame chicago a la primera llamada y después se uso mutate(), pero luego no se tuvo que pasar el primer argumento a group_by() o summarize() . Una vez que se viaja por la tubería con %>% , el primer argumento se toma como la salida del elemento anterior en la tubería.

Otro ejemplo podría ser calcular el nivel promedio de contaminantes por mes. Esto podría ser útil para ver si hay tendencias estacionales 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),
                  .groups = "drop")
## # 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í se puede ver que o3 tiende a ser bajo en los meses de invierno y alto en verano, mientras que no2 es más alto en invierno y más bajo en verano.

13.12 Resumen

El paquete dplyr proporciona un conjunto conciso de operaciones para administrar data frames. Con estas funciones se puede realizar una serie de operaciones complejas en tan solo unas pocas líneas de código. En particular, a menudo se puede realizar los comienzos de un análisis exploratorio con la poderosa combinación de group_by() y summarize() .

Una vez que aprende la gramática dplyr , hay algunos beneficios adicionales;

  • dplyr puede funcionar con otros “backends” de data frames, como bases de datos SQL. Hay una interfaz SQL para bases de datos relacionales a través del paquete DBI.

  • dplyr se puede integrar con el paquete data.table para tablas grandes y rápidas.

El paquete dplyr es una forma práctica de simplificar y acelerar el código de administración de data frames.

  • También existen múltiples paqueterías que permiten la incorporación de dplyr en los modelos estadísticos.