Manipulación de data frames con dplyr
Resumen
Enseñando: 40 min
Ejercicios: 15 minPreguntas
¿Cómo manipular data frames sin repetir lo mismo una y otra vez?
Objectivos
Ser capaces de usar las seis principales acciones de manipulación de data frames con pipes en
dplyr
.Comprender cómo combinar
group_by()
ysummarize()
para obtener resúmenes de datasets.Ser capaces de analizar un subconjunto de datos usando un filtrado lógico.
Palabras clave
Comando : Traducción
filter
: filtrar
select
: seleccionar
summarize
: resumir
count
: contar
mean
: media
mutate
: mutar
La manipulación de data frames significa distintas cosas para distintos investigadores. A veces queremos seleccionar ciertas observaciones (filas) o variables (columnas), otras veces deseamos agrupar los datos en función de una o más variables, o queremos calcular valores estadísticos de un conjunto. Podemos hacer todo ello usando las habituales operaciones básicas de R:
mean(gapminder[gapminder$continent == "Africa", "gdpPercap"])
[1] 2193.755
mean(gapminder[gapminder$continent == "Americas", "gdpPercap"])
[1] 7136.11
mean(gapminder[gapminder$continent == "Asia", "gdpPercap"])
[1] 7902.15
Pero esto no es muy elegante porque hay demasiada repetición. Repetir cosas cuesta tiempo, tanto en el momento de hacerlo como en el futuro, y aumenta la probabilidad de que se produzcan desagradables bugs (errores).
El paquete dplyr
Afortunadamente, el paquete dplyr
proporciona un conjunto de funciones extremadamente útiles para manipular data frames y así reducir el número de repeticiones, la probabilidad de cometer errores y el número de caracteres que hay que escribir. Como valor extra, puedes encontrar que la gramática de dplyr
es más fácil de entender.
Aquí vamos a revisar 6 de sus funciones más usadas, así como a usar los pipes (%>%
) para combinarlas.
select()
filter()
group_by()
summarize()
mutate()
Si no has instalado antes este paquete, hazlo del siguiente modo:
install.packages('dplyr')
Ahora vamos a cargar el paquete:
library("dplyr")
Usando select()
Si por ejemplo queremos continuar el trabajo con sólo unas pocas de las variables de nuestro data frame podemos usar la función select()
. Esto guardará sólo las variables que seleccionemos.
year_country_gdp <- select(gapminder,year,country,gdpPercap)
Si ahora investigamos year_country_gdp
veremos que sólo contiene el año, el país y la renta per cápita. Arriba hemos usado la gramática ‘normal’, pero la fortaleza de dplyr
consiste en combinar funciones usando pipes. Como la gramática de las pipes es distinta a todo lo que hemos visto antes en R, repitamos lo que hemos hecho arriba, pero esta vez usando pipes.
year_country_gdp <- gapminder %>% select(year,country,gdpPercap)
Para ayudarte a entender por qué lo hemos escrito así, vamos a revisarlo por partes. Primero hemos llamado al data frame “gapminder” y se lo hemos pasado al siguiente paso, que es la función select()
, usando el símbolo del pipe %>%
. En este caso no especificamos qué objeto de datos vamos a usar en la función select()
porque esto se obtiene del resultado de la instrucción previa a el pipe. Dato curioso: es muy posible que te hayas encontrado con pipes antes en la terminal de unix. En R el símbolo del pipe es %>%
, mientras que en la terminal es |
, pero el concepto es el mismo.
Usando filter()
Si ahora queremos continuar con lo de arriba, pero sólo con los países europeos, podemos combinar select
y filter
.
year_country_gdp_euro <- gapminder %>%
filter(continent=="Europe") %>%
select(year,country,gdpPercap)
Reto 1
Escribe un único comando (que puede ocupar varias líneas e incluir pipes) que produzca un data frame y que tenga los valores africanos correspondientes a
lifeExp
,country
yyear
, pero no de los otros continentes. ¿Cuántas filas tiene dicho data frame y por qué?Solución al Reto 1
year_country_lifeExp_Africa <- gapminder %>% filter(continent=="Africa") %>% select(year,country,lifeExp)
Al igual que la vez anterior, primero le pasamos el data frame “gapminder” a la función filter()
y luego le pasamos la versión filtrada del data frame a la función select()
. Nota: El orden de las operaciones es muy importante en este caso. Si usamos primero select()
, la función filter()
no habría podido encontrar la variable “continent” porque la habríamos eliminado en el paso previo.
Usando group_by() y summarize()
Se suponía que teníamos que reducir las repeticiones causantes de errores de lo que se puede hacer con el R básico, pero hasta ahora no lo hemos conseguido porque tendríamos que repetir lo escrito arriba para cada continente. En lugar de filter()
, que solamente deja pasar las observaciones que se ajustan a tu criterio (continent = Europe
en lo escrito arriba), podemos usar group_by()
, que esencialmente usará cada uno de los criterios únicos que podrías haber usado con filter()
.
str(gapminder)
'data.frame': 1704 obs. of 6 variables:
$ country : Factor w/ 142 levels "Afghanistan",..: 1 1 1 1 1 1 1 1 1 1 ...
$ year : int 1952 1957 1962 1967 1972 1977 1982 1987 1992 1997 ...
$ pop : num 8425333 9240934 10267083 11537966 13079460 ...
$ continent: Factor w/ 5 levels "Africa","Americas",..: 3 3 3 3 3 3 3 3 3 3 ...
$ lifeExp : num 28.8 30.3 32 34 36.1 ...
$ gdpPercap: num 779 821 853 836 740 ...
str(gapminder %>% group_by(continent))
Classes 'grouped_df', 'tbl_df', 'tbl' and 'data.frame': 1704 obs. of 6 variables:
$ country : Factor w/ 142 levels "Afghanistan",..: 1 1 1 1 1 1 1 1 1 1 ...
$ year : int 1952 1957 1962 1967 1972 1977 1982 1987 1992 1997 ...
$ pop : num 8425333 9240934 10267083 11537966 13079460 ...
$ continent: Factor w/ 5 levels "Africa","Americas",..: 3 3 3 3 3 3 3 3 3 3 ...
$ lifeExp : num 28.8 30.3 32 34 36.1 ...
$ gdpPercap: num 779 821 853 836 740 ...
- attr(*, "vars")= chr "continent"
- attr(*, "drop")= logi TRUE
- attr(*, "indices")=List of 5
..$ : int 24 25 26 27 28 29 30 31 32 33 ...
..$ : int 48 49 50 51 52 53 54 55 56 57 ...
..$ : int 0 1 2 3 4 5 6 7 8 9 ...
..$ : int 12 13 14 15 16 17 18 19 20 21 ...
..$ : int 60 61 62 63 64 65 66 67 68 69 ...
- attr(*, "group_sizes")= int 624 300 396 360 24
- attr(*, "biggest_group_size")= int 624
- attr(*, "labels")='data.frame': 5 obs. of 1 variable:
..$ continent: Factor w/ 5 levels "Africa","Americas",..: 1 2 3 4 5
..- attr(*, "vars")= chr "continent"
..- attr(*, "drop")= logi TRUE
Se puede observar que la estructura del data frame obtenido por group_by()
(grouped_df
) no es la misma que la del data frame original gapminder
(data.fram
). Se puede pensar en un grouped_df
como en una list
donde cada item in la list
es un data.frame
que contiene únicamente las filas que corresponden a un valor particular de continent
(en el ejemplo mostrado).
Usando summarize()
Lo visto arriba no es muy sofisticado, pero group_by()
es más interesante y útil si se usa en conjunto con summarize()
. Esto nos permitirá crear nuevas variables usando funciones que se aplican a cada uno de los data frames específicos para cada continente. Es decir, usando la función group_by()
dividimos nuestro data frame original en varias partes, a las que luego podemos aplicarles funciones (por ejemplo, mean()
o sd()
) independientemente con summarize()
.
gdp_bycontinents <- gapminder %>%
group_by(continent) %>%
summarize(mean_gdpPercap=mean(gdpPercap))
continent mean_gdpPercap
<fctr> <dbl>
1 Africa 2193.755
2 Americas 7136.110
3 Asia 7902.150
4 Europe 14469.476
5 Oceania 18621.609
Esto nos ha permitido calcular la renta per cápita media para cada continente, pero puede ser todavía mucho mejor.
Reto 2
Calcula la esperanza de vida media por país. ¿Qué país tiene la esperanza de vida media mayor y cuál la menor?
Solución al Reto 2
lifeExp_bycountry <- gapminder %>% group_by(country) %>% summarize(mean_lifeExp=mean(lifeExp)) lifeExp_bycountry %>% filter(mean_lifeExp == min(mean_lifeExp) | mean_lifeExp == max(mean_lifeExp))
# A tibble: 2 x 2 country mean_lifeExp <fct> <dbl> 1 Iceland 76.5 2 Sierra Leone 36.8
Otro modo de hacer esto es usando la función
arrange()
del paquetedplyr
, que distribuye las filas de un data frame en función del orden de una o más variables del data frame. Tiene una sintaxis similar a otras funciones del paquetedplyr
. Se puede usardesc()
dentro dearrange()
para ordenar de modo descendente.lifeExp_bycountry %>% arrange(mean_lifeExp) %>% head(1)
# A tibble: 1 x 2 country mean_lifeExp <fct> <dbl> 1 Sierra Leone 36.8
lifeExp_bycountry %>% arrange(desc(mean_lifeExp)) %>% head(1)
# A tibble: 1 x 2 country mean_lifeExp <fct> <dbl> 1 Iceland 76.5
La función group_by()
nos permite agrupar en función de varias variables. Vamos a agrupar por year
y continent
.
gdp_bycontinents_byyear <- gapminder %>%
group_by(continent,year) %>%
summarize(mean_gdpPercap=mean(gdpPercap))
Esto ya es bastante potente, pero puede ser incluso mejor. Puedes definir más de una variable en summarize()
.
gdp_pop_bycontinents_byyear <- gapminder %>%
group_by(continent,year) %>%
summarize(mean_gdpPercap=mean(gdpPercap),
sd_gdpPercap=sd(gdpPercap),
mean_pop=mean(pop),
sd_pop=sd(pop))
count() y n()
Una operación muy habitual es contar el número de observaciones que hay en cada grupo. El paquete dplyr
tiene dos funciones relacionadas muy útiles para ello.
Por ejemplo, si queremos comprobar el número de países que hay en el conjunto de datos para el año 2002 podemos usar la función count()
. Dicha función toma el nombre de una o más columnas que contienen los grupos en los que estamos interesados y puede opcionalmente ordenar los resultados en modo descendente si añadimos sort = TRUE
.
gapminder %>%
filter(year == 2002) %>%
count(continent, sort = TRUE)
# A tibble: 5 x 2
continent n
<fct> <int>
1 Africa 52
2 Asia 33
3 Europe 30
4 Americas 25
5 Oceania 2
Si necesitamos usar en nuestros cálculos el número de observaciones obtenidas, la función n()
es muy útil. Por ejemplo, si queremos obtener el error estándar de la esperanza de vida por continente:
gapminder %>%
group_by(continent) %>%
summarize(se_pop = sd(lifeExp)/sqrt(n()))
# A tibble: 5 x 2
continent se_pop
<fct> <dbl>
1 Africa 0.366
2 Americas 0.540
3 Asia 0.596
4 Europe 0.286
5 Oceania 0.775
También se pueden encadenar juntas varias operaciones de resumen, como en el caso siguiente, en el que calculamos el minimum
, maximum
, mean
y se
de la esperanza de vida por país para cada continente:
gapminder %>%
group_by(continent) %>%
summarize(
mean_le = mean(lifeExp),
min_le = min(lifeExp),
max_le = max(lifeExp),
se_le = sd(lifeExp)/sqrt(n()))
# A tibble: 5 x 5
continent mean_le min_le max_le se_le
<fct> <dbl> <dbl> <dbl> <dbl>
1 Africa 48.9 23.6 76.4 0.366
2 Americas 64.7 37.6 80.7 0.540
3 Asia 60.1 28.8 82.6 0.596
4 Europe 71.9 43.6 81.8 0.286
5 Oceania 74.3 69.1 81.2 0.775
Usando mutate()
También se pueden crear nuevas variables antes (o incluso después) de resumir la información usando mutate()
.
gdp_pop_bycontinents_byyear <- gapminder %>%
mutate(gdp_billion=gdpPercap*pop/10^9) %>%
group_by(continent,year) %>%
summarize(mean_gdpPercap=mean(gdpPercap),
sd_gdpPercap=sd(gdpPercap),
mean_pop=mean(pop),
sd_pop=sd(pop),
mean_gdp_billion=mean(gdp_billion),
sd_gdp_billion=sd(gdp_billion))
Conectando mutate con filtrado lógico: ifelse
La creación de nuevas variables se puede conectar con una condición lógica. Una simple combinación de mutate
y ifelse
facilita el filtrado solo allí donde se necesita: en el momento de crear algo nuevo. Esta combinación es fácil de leer y es un modo rápido y potente de descartar ciertos datos (incluso sin cambiar la dimensión conjunta del data frame) o para actualizar valores dependiendo de la condición utilizada.
## manteniendo todos los datos pero "filtrando" según una determinada condición
# calcular renta per cápita sólo para gente con una esperanza de vida por encima de 25
gdp_pop_bycontinents_byyear_above25 <- gapminder %>%
mutate(gdp_billion = ifelse(lifeExp > 25, gdpPercap * pop / 10^9, NA)) %>%
group_by(continent, year) %>%
summarize(mean_gdpPercap = mean(gdpPercap),
sd_gdpPercap = sd(gdpPercap),
mean_pop = mean(pop),
sd_pop = sd(pop),
mean_gdp_billion = mean(gdp_billion),
sd_gdp_billion = sd(gdp_billion))
## actualizando sólo si se cumple una determinada condición
# para esperanzas de vida por encima de 40 años, el GDP que se espera en el futuro es escalado
gdp_future_bycontinents_byyear_high_lifeExp <- gapminder %>%
mutate(gdp_futureExpectation = ifelse(lifeExp > 40, gdpPercap * 1.5, gdpPercap)) %>%
group_by(continent, year) %>%
summarize(mean_gdpPercap = mean(gdpPercap),
mean_gdpPercap_expected = mean(gdp_futureExpectation))
Combinando dplyr
y ggplot2
En la función de creación de gráficas vimos cómo hacer una figura con múltiples paneles añadiendo una capa de paneles separados (facet panels). Aquí está el código que usamos (con algunos comentarios extra):
# Obtener la primera letra de cada país
starts.with <- substr(gapminder$country, start = 1, stop = 1)
# Filtrar países que empiezan con "A" o "Z"
az.countries <- gapminder[starts.with %in% c("A", "Z"), ]
# Construir el gráfico
ggplot(data = az.countries, aes(x = year, y = lifeExp, color = continent)) +
geom_line() + facet_wrap( ~ country)
Este código construye la gráfica correcta, pero también crea algunas variables starts.with
y az.countries
) que podemos no querer usar para nada más. Del mismo modo que usamos %>%
para pasar datos con pipes a lo largo de una cadena de funciones dplyr
, podemos usarlo para pasarle datos a ggplot()
. Como %>%
sustituye al primer argumento de una función, no necesitamos especificar el argumento data=
de la función ggplot()
. Combinando funciones de los paquetes dplyr
y ggplot
podemos hacer la misma figura sin crear ninguna nueva variable y sin modificar los datos.
gapminder %>%
# Get the start letter of each country
mutate(startsWith = substr(country, start = 1, stop = 1)) %>%
# Filter countries that start with "A" or "Z"
filter(startsWith %in% c("A", "Z")) %>%
# Make the plot
ggplot(aes(x = year, y = lifeExp, color = continent)) +
geom_line() +
facet_wrap( ~ country)
Las funciones del paquete dplyr
también nos ayudan a simplificar las cosas, por ejemplo, combinando los primeros dos pasos:
gapminder %>%
# Filter countries that start with "A" or "Z"
filter(substr(country, start = 1, stop = 1) %in% c("A", "Z")) %>%
# Make the plot
ggplot(aes(x = year, y = lifeExp, color = continent)) +
geom_line() +
facet_wrap( ~ country)
Reto Avanzado
Calcula la esperanza de vida media en 2002 de dos países seleccionados al azar para cada continente. Luego distribuye los nombres de los continentes en orden inverso. Pista: Usa las funciones
arrange()
ysample_n()
del paquetedplyr
, tienen una sintaxis similar a las demás funciones del paquetedplyr
.Solución al Reto Avanzado
lifeExp_2countries_bycontinents <- gapminder %>% filter(year==2002) %>% group_by(continent) %>% sample_n(2) %>% summarize(mean_lifeExp=mean(lifeExp)) %>% arrange(desc(mean_lifeExp))
Más información
- R for Data Science
- Data Wrangling Cheat sheet
- Introduction to dplyr
- Data wrangling with R and RStudio
Puntos Clave
Usar el paquete
dplyr
para manipular data frames.Usar
select()
para seleccionar variables de un data frame.Usar
filter()
para seleccionar datos basándose en los valores.Usar
group_by()
ysummarize()
para trabajar con subconjuntos de datos.Usar
mutate()
para crear nuevas variables.