library(plyr)
<- read.table("data/paro.csv", header = T, sep = "\t") paro
10 Procesamiento de tablas por trozos con plyr
En esta sección vamos a aprender a realizar operaciones avanzadas de manipulación de datos con el paquete plyr
. Muchas transformaciones de datos sobre tablas, conceptualmente, se reducen a:
- Partir una tabla en trozos definidos por provincias, tramos de edad, categorías de productos, etc.
- Realizar una operación sobre esos trozos individuales; p.e., en un caso muy simple, calcular unas medias.
- Recoger los resultados calculados en cada trozo y juntarlos en una única tabla final.
Lenguajes como SQL o Python —en concreto, usando pandas
— permiten realizar operaciones de manipulación de datos similares. Incluso en R, como se verá, el paquete dplyr
permite realizar muchas de estas operaciones usando una sintaxis distinta. Por eso, en este capítulo es particularmente importante distinguir entre los aspectos conceptuales de las transformaciones y la sintaxis particular que es necesario aplicar para llevarlas a cabo.
10.1 Resúmenes de datos por bloques
Comenzaremos por cargando el paquete (y tendrás que instalarlo si no lo has hecho ya) y leer unos datos de ejemplo:
La función fundamental del paquete plyr
es ddply
que, típicamente, se invoca así:
<- ddply(paro,
res
.(Gender, Periodo, Situation),total = sum(value)) summarize,
La tabla resultante contiene el número de españoles (en miles) por sexo, periodo y situación laboral agregando los valores correspondientes a todas las provincias.
Los elementos que componen la expresión anterior son:
ddply
: es una función que transforma una tabla en otra tabla; de hecho,ddply
siempre opera sobre tablas y siempre devuelve tablas.paro
: la tabla sobre la que opera..(Gender, Periodo, Situation)
: variales de la tabla de entrada cuyos niveles definen los trozos sobre los que se opera individualmente. La tabla de entrada se parte en tantas subtablas como combinaciones distintas de valores deGender
,Periodo
ySituation
existan.summarize
: una función que opera sobre tablas y devuelve tablas; veremos después ejemplos de otras funciones que pueden usarse en su lugar.total = ...
: argumentos de la función anterior. Igual que ocurría contapply
, los argumentos que siguen a la función anterior se aplican addply
sino sobre aquella.
La función ddply
encuentra las subtablas que definen los niveles distintos de las variables indicadas en el segundo argumento y se las pasa una a una a la función que indica el tercer argumento. Esta función opera sobre cada uno de los trozos por separado y, finalmente, ddply
apila los resultados en una única tabla final.
ddply
es otro ejemplo de una función de orden superior. Anteriormente habíamos introducido tapply
, que es similar pero mucho menos potente: tapply
opera solo sobre vectores, pero ddply
admite tablas como argumentos.
La función summarize
es muy simple: aplicada sobre una tabla, devuelve otra con una única fila que contiene los resúmenes (medias, etc.) que se le pasan como argumentos adicionales. Por ejemplo,
summarize(iris,
media.pl = mean(Petal.Length),
media.pw = mean(Petal.Width))
media.pl media.pw
1 3.758 1.199333
devuelve las longitudes y anchuras medias de los pétalos de iris
. Por sí sola, tal y como ilustra el ejemplo anterior, summarize
no es una función particularmente útil. Pero en combinación con ddply
sirve para implementar lo equivalente a los group by de SQL. Si quisiéramos realizar esta operación por especies, podríamos envolver la expresión anterior en una llamada a ddply
así:
ddply(iris, .(Species),
summarize,media.pl = mean(Petal.Length),
media.pw = mean(Petal.Width))
Species media.pl media.pw
1 setosa 1.462 0.246
2 versicolor 4.260 1.326
3 virginica 5.552 2.026
Ejercicio 10.1 Usa summarize
para calcular la media y la mediana de la temperatura en airquality
.
Ejercicio 10.2 Dispón airquality
en formato largo y extrae la media y la mediana de cada variable por mes.
La función summarize
no es, por supuesto, la única que admite ddply
; le vale cualquier función que reciba una tabla y devuelva otra. Por ejemplo,
<- function(x) lm(Temp ~ Solar.R, data = x)$coefficients
foo ddply(airquality, .(Month), foo)
Month (Intercept) Solar.R
1 5 61.08143 0.02651712
2 6 73.64522 0.02868418
3 7 80.18086 0.01719467
4 8 77.68639 0.03528288
5 9 74.72485 0.01299114
crea una regresión lineal que modela la temperatura en función de la irradiación solar para cada mes y devuelve los coeficientes de dichos modelos en una tabla. Dentro de la expresión anterior hemos creado una función, foo
, que toma unos datos, construye una regresión lineal con ellos y devuelve los coeficientes del modelo; ddply
permite aplicar esa función a los subconjuntos de airquality
correspondientes a cada uno de los meses de los que contiene datos.
Ejercicio 10.3 Usa ddply
para crear la tabla que en SQL se construiría de la forma select Species, mean(Petal.Length) from iris group by Species
.
Ejercicio 10.4 Carga el conjunto de datos lmm_data.txt
y calcula el valor medio de las variables numéricas del fichero por colegio y clase.
Ejercicio 10.5 Repite el ejercicio anterior pero disponiendo previamente los datos en formato largo (usa la función melt
del paquete reshape2
).
Ejercicio 10.6 Obtén las tres filas de iris
correspondientes a las observaciones de con la mayor longitud del pétalo dentro de cada especie. Pista: puedes crear una función que ordene una tabla por una columna y luego seleccione su primera fila.
10.2 Otras funciones similares a ddply de plyr
En el código
<- dlply(airquality, .(Month), function(x) lm(Temp ~ Solar.R, data = x))
res lapply(res, coefficients)
ldply(res, coefficients)
introducimos varias novedades. La primera, una función anónima: en lugar de crearla fuera de la llamada y asignarle un nombre, podemos simplemente utilizar una función de usar y tirar. En segundo lugar, hemos usado la función dlply
que es similar a ddply
y que solo se diferencia de ella en que devuelve una lista en lugar de una tabla. Finalmente hemos usado la función ldply
que recorre esa lista y la recompone en una tabla. Otras funciones de uso más esporádico son llaply
. laply
, alply
e, incluso, d_ply
.
Las funciones xyply
toman un elemento del tipo indicado por x
, lo trocean convenientemente, aplican una función a cada trozo y tratan de recomponer el resultado en un objeto de tipo indicado por y
. Por eso, ddply
opera sobre tablas y devuelve tablas; ldply
opera sobre listas (elemento a elemento) y devuelve tablas; llply
opera sobre listas y devuelve listas, etc. El prefijo a
corresponde a arrays (no cubiertos en este curso) y _
a nada: se usa para funciones que no devuelven ningún objeto sino que, tal vez, escriben resultados en disco o generan gráficos.
Ejercicio 10.7 Usa dlply
para partir iris
por especies. Es decir, crear una lista que contenga los tres bloques de iris
correspondientes a sus tres especies. Nota: la función I
, la identidad, puede serte útil.
10.3 Transformaciones de datos por bloques
La función summarize
devuelve un único registro por bloque. Sin embargo, en ocasiones interesa devolver la tabla entera transfromada de alguna manera: por ejemplo, con alguna columna adicional calculada con el resto. Reemplazando summarize
por transform
se puede hacer, por ejemplo
<- dcast(paro, Gender + Provinces + Periodo ~ Situation)
tasa.paro <- transform(tasa.paro, tasa.paro = unemployed / active)
tasa.paro <- tasa.paro[, c("Gender", "Provinces", "Periodo", "tasa.paro")]
tasa.paro
<- ddply(tasa.paro, .(Gender, Provinces),
tmp rank = rank(-tasa.paro, ties = "random"))
transform, <- tmp[tmp$rank == 1,] res
que obtiene el periodo en el que la tasa de paro tocó techo por provincia y sexo. Para ello es fundamental que la función rank
se aplique a cada uno de los bloques definidos por Gender
y Provinces
, es decir, que comience a contar desde 1 para cada uno de ellos.
Ejercicio 10.8 Selecciona, para cada periodo, las tres provincias en las que fue menor la tasa de paro tanto para hombres como para mujeres (por separado). Repite el ejercicio teniendo en cuenta la tasa global (o conjunta para hombres y mujeres) de paro.
Ejercicio 10.9 Dispón airquality
en formato largo y para cada variable y cada mes, extrae el día en que cada variable tuvo el máximo valor.
En el siguiente ejemplo vamos a crear un modelo a trozos. Usando estos datos, vamos a
- crear un modelo lineal para cada colegio (con el que predecir la variable
extro
en función de otras) - asignar a cada sujeto dicha predicción
<- read.table("data/lmm_data.txt", header = T, sep = ",")
dat <- ddply(dat, .(school), transform,
dat.preds pred = predict(lm(extro ~ open + agree + social + class)))
El código anterior funciona porque la función transform
captura cada bloque (definido por la variable school
) y pasa las variables extro
, etc. a la expresión predict(lm(extro ~ open + agree + social + class))
. Es posible realizar la misma operación sin transform
, i.e., explícitamente, así:
<- function(x){
foo <- lm(extro ~ open + agree + social + class, data = x)
modelo <- x
res $preds <- predict(modelo)
res
res
}<- ddply(dat, .(school), foo) dat.preds
Es evidente que transform
, aunque por sí sola no sea una función muy útil, usada en conjunción con ddply
simplifica muchas operaciones.
Ejercicio 10.10 Usando los datos del paro (en formato largo), calcula para cada provincia, periodo y situación laboral el porcentaje de los sujetos que son hombres y mujeres.
Pista: Puedes hacerlo en una única línea con ddply
en combinación con transform
. La clave consiste en que puedes calcular el total (sumando hombres y mujeres) por bloques y dividir por esa cantidad.
Ejercicio 10.11 Usando de nuevo los datos del paro, crea una tabla en la que, para cada periodo, figure el porcentaje de los parados de España que viven en cada provincia.
10.4 Más allá de summarize y transform
Aunque es frecuente usar ddply
junto con las funciones summarize
o transform
, también pueden usarse otras: la única condición es que acepten tablas y devuelvan igualmente tablas (con la misma estructura).
En esta sección vamos a construir un ejemplo para el que resultará útil (aunque no estrictamente necesaria) la función arrange
. Esta función es una de las que, más allá de ddply
y similares, contiene el paquete plyr
y permite ordenar una tabla por una o varias columnas:
arrange(paro, Provinces, Periodo, Gender, Situation)
Ejercicio 10.12 Busca en ?arrange
cómo ordenar descendentemente.
El problema que queremos resolver es el siguiente: de cada especie de iris
extraer los tres ejemplares con el pétalo más largo. Evidentemente, este proceso tiene que realizarse especie a especie. Para cada una de ellas, hay que ordenar por la longitud del pétalo y quedarse con las tres últimas filas. Podemos proceder así:
<- function(x)
extract.top3 tail(arrange(x, Petal.Length), 3)
ddply(iris, .(Species), extract.top3)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 5.1 3.3 1.7 0.5 setosa
2 4.8 3.4 1.9 0.2 setosa
3 5.1 3.8 1.9 0.4 setosa
4 6.3 2.5 4.9 1.5 versicolor
5 6.7 3.0 5.0 1.7 versicolor
6 6.0 2.7 5.1 1.6 versicolor
7 7.7 3.8 6.7 2.2 virginica
8 7.7 2.8 6.7 2.0 virginica
9 7.7 2.6 6.9 2.3 virginica
En este caso ddply
ha distribuido la función extract.top3
que acepta una tabla, la ordena y selecciona sus tres últimas filas.
Ejercicio 10.13 En una sección anterior habíamos resuelto el ejercicio consistente en seleccionar, para cada periodo, las tres provincias en las que fue menor la tasa de paro tanto para hombres como para mujeres (por separado) usando transform
. Repítelo utilizando una versión de la función introducida en esta sección.
10.5 Cruces de tablas
Con la función merge
pueden hacerse cruces de tablas. Para ilustrar los distintos tipos de cruce, crearemos unos datos pequeños de ejemplo:
<- data.frame(id = 1:3, nombre = c("Carlos", "Sara", "Raquel"))
clientes <- data.frame(fecha = c(1, 1, 1, 2, 2, 3, 3, 3, 4),
ventas id = c(1,2,3,2,3, 1, 2, 3, 3),
total = 100 * runif(9))
Para cruzarlas por defecto, se usa la función merge
:
merge(clientes, ventas)
id nombre fecha total
1 1 Carlos 1 75.94081
2 1 Carlos 3 88.70361
3 2 Sara 3 99.76208
4 2 Sara 2 27.99456
5 2 Sara 1 24.59069
6 3 Raquel 2 97.82162
7 3 Raquel 1 22.01408
8 3 Raquel 3 49.37646
9 3 Raquel 4 52.79386
Esta función crea una nueva tabla juntando filas de otras dos. El resultado es el mismo que la consulta de SQL
select *
from clientes a, ventas b
where a.id = b.id
merge
junta las filas que tienen el mismo valor en los campos de cruce. Por defecto, los campos de cruce son aquellos que se llaman igual en ambas tablas. Esto es a la vez cómodo (evita tener que especificarlas explícitamente) y peligroso. En el ejercicio siguiente se te pide que investigues cómo modificar este funcionamiento por defecto de merge
.
Ejercicio 10.14 ¿Y si las variables de cruce no se llaman igual? ¿Y si no quieres cruzar por todas ellas?
merge
también permite realizar left joins. Nótese la diferencia entre las dos tablas creadas por merge
en el ejemplo siguiente.
.2 <- ventas[ventas$fecha == 2,]
ventasmerge(clientes, ventas.2)
id nombre fecha total
1 2 Sara 2 27.99456
2 3 Raquel 2 97.82162
merge(clientes, ventas.2, all.x = T)
id nombre fecha total
1 1 Carlos NA NA
2 2 Sara 2 27.99456
3 3 Raquel 2 97.82162
La consulta de SQL equivalente a la última expresión es
select *
from clientes a left join ventas.2 b
on a.id = b.id
Para el ejercicio siguiente vamos a crear una tabla simulada,
<- 10000
n <- 1000
n.clientes
<- data.frame(
contracts customer = sample(n.clientes, n, replace = T),
contract = sample(n, n),
amount = 1000 * exp(runif(n))
)
que contiene clientes, contratos y el importe de cada contrato.
Ejercicio 10.15 Calcula el porcentaje que cada contrato de un cliente supone dentro de su facturación total. P.e., si un cliente tiene dos contratos de 200 y 800 euros, a la primera fila le asociaremos un 20% y, a la segunda, 80%. Una manera de hacerlo consiste en:
- Crear una tabla que agregue el total de los importes por cliente
- Cruzar esta nueva tabla con la original (por cliente)
- Dividir el importe original por el total y multiplicarlo por 100.
merge
es la función por defecto para cruzar tablas en R. Sin embargo, existe una colección de funciones (left_join
, inner_join
, etc.) en el paquete dplyr
que pueden resultar más familiares a quienes tengan experiencia con SQL1.
10.6 Resumen y referencias
Esta sección ha cubierto el uso del paquete plyr
para realizar operaciones de las conocidas como split, apply, combine (parte, aplica y recombina) sobre conjuntos de datos, típicamente tabulares. Muchísimas de las operaciones habituales de transformación de datos se reducen a variantes del esquema anterior. Por eso las encontramos también en SQL, en Python/Pandas y otros lenguajes de progración.
En el mismo R existen otros dos dialectos alternativos a plyr
que permiten también realizar este tipo de operaciones: dplyr
y data.table
. El segundo es sumamente veloz y el más adecuado para datos muy grandes; de hecho, plyr
es inadecuado para procesar datos con tamaños del orden del millón (y, por supuesto, más) de registros. Sobre dplyr
se hablará brevemente en un capítulo posterior.
Los cruces de tablas (bajo las denominaciones habituales de merge
o join
, según el lenguaje) también son ubicuas en todas las aplicaciones de análisis de datos. SQL, por ejemplo, permite realizar cruces de más de dos tablas, combinando inner, left y right joins y utilizando condiciones complejas: igualdades, desigualdades, similitudes, etc. Por eso, si conoces SQL a fondo, merge
te habrá parecido una herramienta limitada. Si por otro lado no has trabajado con SQL, deberías ponerte con ello cuanto antes. No obstante, tanto SQL en sí mismo como los procedimientos para conectar R con bases de datos relacionales —algo muy común en la práctica del análisis de datos— quedan fuera del alcance de este libro.
10.7 Ejercicios adicionales
Ejercicio 10.16 Toma la tabla contracts
definida en la sección anterior y calcula ese total en una única línea usando ddply
.
Ejercicio 10.17 Extrae el mayor contrato (por importe) de cada cliente en contracts
.
Ejercicio 10.18 El fichero Olive.txt
contiene información sobre una muestra de aceites italianos. En particular, su región y área de procedencia y la concentración química de diversos ácidos.
- Calcula la media de esas concentraciones por región (nota: es probable que quieras trabajar sobre una versión arreglada o larga del fichero).
- Construye un gráfico que muestre qué compuesto distingue mejor las regiones (o, al menos, alguna de ellas del resto).
- Repite el ejercicio anterior normalizando (puedes usar
scale
) las concentraciones. De nuevo, es probable que quieras comenzar con los datos en formato largo.
Ejercicio 10.19 Lee esta tabla con los nombres más comunes por sexos en EE.UU. durante los últimos años y extrae los tres más populares por año y sexo.
Ejercicio 10.20 Descarga ficheros de actividad contractual del portal de datos abiertos del ayuntamiento de Madrid, importa alguno de ellos a R y realiza agregaciones de interés: por departamento, por tipo de contrato, por empresa adjudicataria. Busca también cuál es el mayor contrato por departamento municipal. Trata de pensar en otras preguntas de interés sobre esos datos.