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 flexiblefiltro
: extrae un subconjunto de filas de un data.frame basado en condiciones lógicasarrange
: reordenar las filas de un data.framerename
: cambia el nombre de las variables en un data.framemutate
: agregar nuevas variables/columnas o transformar variables existentessummarise
/summarize
: generar estadísticas resumidas de diferentes variables en el data.frame, posiblemente dentro de los estratos.%>%
: el operador “tubería” opipeline
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:
El primer argumento es un data.frame.
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).
El resultado de retorno de una función es un nuevo data.frame.
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:
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
## date pm25tmean2
## 1 1987-01-01 NA
## 2 1987-01-02 NA
## 3 1987-01-03 NA
y las últimas filas.
## 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()
.
Mirando las primeras tres y últimas tres filas se muestran las fechas en orden descendente.
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
Tomando el ejemplo anterior donde se calculó la media de o3
y no2
dentro de los quintiles de pm25
. Se tuvo que:
- crea una nueva variable
pm25.quint
- dividir el data.frame por esa nueva variable.
- calcular la media de
o3
yno2
en los subgrupos definidos porpm25.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 paquetedata.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.