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:

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:

library(plyr)
paro <- read.table("data/paro.csv", header = T, sep = "\t")

La función fundamental del paquete plyr es ddply que, típicamente, se invoca así:

res <- ddply(paro,
            .(Gender, Periodo, Situation),
            summarize, total = sum(value))

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 de Gender, Periodo y Situation 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 con tapply, los argumentos que siguen a la función anterior se aplican a ddply 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,

foo <- function(x) lm(Temp ~ Solar.R, data = x)$coefficients
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

res <- dlply(airquality, .(Month), function(x) lm(Temp ~ Solar.R, data = x))
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

tasa.paro <- 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")]

tmp <- ddply(tasa.paro, .(Gender, Provinces),
             transform, rank = rank(-tasa.paro, ties = "random"))
res <- tmp[tmp$rank == 1,]

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
dat <- read.table("data/lmm_data.txt", header = T, sep = ",")
dat.preds <- ddply(dat, .(school), transform,
                   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í:

foo <- function(x){
  modelo <- lm(extro ~ open + agree + social + class, data = x)
  res <- x
  res$preds <- predict(modelo)
  res
}
dat.preds <- ddply(dat, .(school), foo)

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í:

extract.top3 <- function(x)
  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:

clientes <- data.frame(id = 1:3, nombre = c("Carlos", "Sara", "Raquel"))
ventas <- data.frame(fecha = c(1, 1, 1, 2, 2, 3, 3, 3, 4),
                     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.

ventas.2 <- ventas[ventas$fecha == 2,]
merge(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,

n <- 10000
n.clientes <- 1000

contracts <- data.frame(
  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.


  1. Puede consultarse un tutorial de estas funciones aquí↩︎