7  Pivotación de tablas con reshape2

En contra de la opinión generalizada, una parte fundamental del trabajo con R consiste en manipular y transformar datos para darles la forma necesaria para su uso en un análisis posterior. En las secciones anteriores hemos explorado ya algunas técnicas básicas que pueden usarse para este fin. En esta y alguna de las siguientes vamos a presentar ciertas metatransformaciones de datos, i.e., operaciones genéricas que subyacen a muchas transformaciones específicas.

En concreto, en esta sección exploraremos el paquete reshape2 que contiene, esencialmente, dos funciones: melt y dcast1. La primera, a partir de una tabla, crea una versión en formato largo, arreglado2 o tidy. La función dcast es una inversa —y algo más— de melt. Entre otras cosas, dcast incorpora a R operaciones análogas a las pivot tables de Excel.

7.1 La función melt y datos en formato largo

Una tabla está en formato largo cuando cada fila contiene un dato y el resto de las columnas son etiquetas que le dan contexto. Además de ser más claras conceptualmente, suele ser más sencillo procesar tablas en formato largo. A los interesados en saber más sobre este tipo de datos se les recomienda leer el artículo Tidy Data3.

Vamos a comenzar leyendo un fichero de datos que contiene la población en 2014 de las provincias aragonesas por sexo. Se trata de un conjunto de datos simple y minúsculo pero que, gracias a eso, permite visualizar las transformaciones que vamos a realizar.

pob_aragon_2014 <- read.table("data/pob_aragon_2014.csv",
                              header = T, sep = "\t")
pob_aragon_2014
  Provincia Hombres Mujeres
1    Huesca  113840  111069
2    Teruel   71449   68916
3  Zaragoza  471675  488436

Es habitual encontrar conjuntos de datos (aunque típicamente más grandes) en formatos similares. Estos datos no están en formato largo o arreglado. En tablas arregladas, las columnas representan necesariamente variables. Sin embargo, en pob_aragon_2014 existe una variable implícita, sexo de la que Hombres y Mujeres son niveles (o valores).

Podemos usar la función melt para pivotar la tabla y arreglar los datos, i.e., disponerlos en formato largo:

library(reshape2)
melt(pob_aragon_2014)
  Provincia variable  value
1    Huesca  Hombres 113840
2    Teruel  Hombres  71449
3  Zaragoza  Hombres 471675
4    Huesca  Mujeres 111069
5    Teruel  Mujeres  68916
6  Zaragoza  Mujeres 488436

Es evidente que la información contenida en ambos conjuntos de datos es la misma. Pero en la nueva tabla cada fila tiene un único valor. El resto de las columnas, Provincia y variable —como R desconoce que el nombre ideal de la nueva variable sería sexo, por defecto, la denomina variable— contextualizan dicha cifra.

Un ejemplo un poco más intersante es este:

pob_aragon <- read.table("data/pob_aragon.csv",
                         header = T, sep = "\t")
pob_aragon
  Provincia Periodo Hombres Mujeres
1    Huesca    2014  113840  111069
2    Huesca    2004  107961  104940
3    Teruel    2014   71449   68916
4    Teruel    2004   71073   68260
5  Zaragoza    2014  471675  488436
6  Zaragoza    2004  441840  455510

El nuevo conjunto de datos tiene una variable adicional, Periodo. Si se le aplica melt directamente, el resultado dista del esperado:

melt(pob_aragon)
   Provincia variable  value
1     Huesca  Periodo   2014
2     Huesca  Periodo   2004
3     Teruel  Periodo   2014
4     Teruel  Periodo   2004
5   Zaragoza  Periodo   2014
6   Zaragoza  Periodo   2004
7     Huesca  Hombres 113840
8     Huesca  Hombres 107961
9     Teruel  Hombres  71449
10    Teruel  Hombres  71073
11  Zaragoza  Hombres 471675
12  Zaragoza  Hombres 441840
13    Huesca  Mujeres 111069
14    Huesca  Mujeres 104940
15    Teruel  Mujeres  68916
16    Teruel  Mujeres  68260
17  Zaragoza  Mujeres 488436
18  Zaragoza  Mujeres 455510

Efectivamente, R confunde Periodo con un nivel más de la nueva variable, junto a Hombres y Mujeres. Pero, en realidad, tanto la provincia como el periodo forman la clave de la tabla. Esto se le puede indicar a melt así:

melt(pob_aragon, id.vars = c("Provincia", "Periodo"))
   Provincia Periodo variable  value
1     Huesca    2014  Hombres 113840
2     Huesca    2004  Hombres 107961
3     Teruel    2014  Hombres  71449
4     Teruel    2004  Hombres  71073
5   Zaragoza    2014  Hombres 471675
6   Zaragoza    2004  Hombres 441840
7     Huesca    2014  Mujeres 111069
8     Huesca    2004  Mujeres 104940
9     Teruel    2014  Mujeres  68916
10    Teruel    2004  Mujeres  68260
11  Zaragoza    2014  Mujeres 488436
12  Zaragoza    2004  Mujeres 455510

Los datos en formato largo están muy relacionados con los arrays (conocidos en algunos contextos empresariales como cubos multidimensionales). Un ejemplo de ellos es la tabla Titanic de R, que se refiere a los viajeros del Titanic, y que tiene cuatro dimensiones: la clase, el sexo, el grupo de edad y si sobrevivieron o no al hundimiento:

dimnames(Titanic)
$Class
[1] "1st"  "2nd"  "3rd"  "Crew"

$Sex
[1] "Male"   "Female"

$Age
[1] "Child" "Adult"

$Survived
[1] "No"  "Yes"

La función as.data.frame aplicada a un array multidimensional produce una tabla en formato largo:

tmp <- as.data.frame(Titanic)
rbind(head(tmp), tail(tmp))
   Class    Sex   Age Survived Freq
1    1st   Male Child       No    0
2    2nd   Male Child       No    0
3    3rd   Male Child       No   35
4   Crew   Male Child       No    0
5    1st Female Child       No    0
6    2nd Female Child       No    0
27   3rd   Male Adult      Yes   75
28  Crew   Male Adult      Yes  192
29   1st Female Adult      Yes  140
30   2nd Female Adult      Yes   80
31   3rd Female Adult      Yes   76
32  Crew Female Adult      Yes   20

Ejercicio 7.1 La función melt también funciona con matrices. Una de ellas es VADeaths. Usa melt para disponer esa matriz en formato largo. Verás que aparece una columna con valores del tipo Rural Male: construye a partir de ella dos columnas (sex y residency). Nota: existe una función auxiliar en reshape2, colsplit que realiza precisamente esa operación.

Ejercicio 7.2 Toma el conjunto de datos airquality y disponlo en formato largo. Ten cuidado con la definición de la clave de la tabla.

Ejercicio 7.3 Cambia el nombre a la variable variable creada automáticamente por melt en el ejercicio anterior.

Ejercicio 7.4 Calcula el valor mediano (median) de las variables de airquality (después de haberlo dispuesto en formato largo). Usa la función tapply.

En general, disponer datos en formato largo facilita muchas operaciones de manipulación de datos. En el ejercicio anterior, en lugar de calcular la mediana variable a variable, ha sido posible obtenerla en dos líneas de código. Que son independientes, además, del número de indicadores de la tabla.

7.2 La función dcast y datos en formato ancho

A partir del formato largo se puede pasar a distintos tipos de formatos no largos (o anchos) usando la función dcast. Por ejemplo, a partir de los datos

pob_aragon_2014_largo <- melt(pob_aragon_2014)
pob_aragon_2014_largo
  Provincia variable  value
1    Huesca  Hombres 113840
2    Teruel  Hombres  71449
3  Zaragoza  Hombres 471675
4    Huesca  Mujeres 111069
5    Teruel  Mujeres  68916
6  Zaragoza  Mujeres 488436

se pueden poner las pronvicias en filas

dcast(pob_aragon_2014_largo, Provincia ~ variable)
  Provincia Hombres Mujeres
1    Huesca  113840  111069
2    Teruel   71449   68916
3  Zaragoza  471675  488436

o en columnas:

dcast(pob_aragon_2014_largo, variable ~ Provincia)
  variable Huesca Teruel Zaragoza
1  Hombres 113840  71449   471675
2  Mujeres 111069  68916   488436

En su versión más simple, dcast necesita:

  • la tabla sobre la que operar
  • qué columnas disponer en filas (generalmente varias, separadas por +, delante de ~)
  • qué columnas disponer en columnas (generalmente una, detrás de ~)

La función dcast utiliza una serie de reglas simples para determinar cuál es la columna de la que extraer los datos que incluir en la tabla resultante. Pero, a veces, como en el ejemplo que sigue, se equivoca. Por ejemplo, vamos a intentar medir el incremento en las horas de sueño (extra) producidas por el tratamiento (o group) 2 sobre el 1 en el conjunto de datos sleep. Este conjunto de datos se refiere a individuos (ID) que fueron sometidos a dos tratamientos distintos. Podemos hacer

dcast(sleep, ID ~ group)
Using ID as value column: use value.var to override.
   ID  1  2
1   1  1  1
2   2  2  2
3   3  3  3
4   4  4  4
5   5  5  5
6   6  6  6
7   7  7  7
8   8  8  8
9   9  9  9
10 10 10 10

para intentar colocar los dos valores correspondientes a cada individuo en la misma fila para poder restarlos después. Sin embargo, el efecto no es el deseado y el warning de R es muy informativo sobre el problema: que dcast ha elegido como variable de valor ID en lugar de extra. Además, indica cómo forzar una alternativa a la que usa por defecto, i.e., usando value.var:

dcast(sleep, ID ~ group, value.var = "extra")
   ID    1    2
1   1  0.7  1.9
2   2 -1.6  0.8
3   3 -0.2  1.1
4   4 -1.2  0.1
5   5 -0.1 -0.1
6   6  3.4  4.4
7   7  3.7  5.5
8   8  0.8  1.6
9   9  0.0  4.6
10 10  2.0  3.4

Ahora es posible añadir la columna adicional de interés haciendo

tmp <- dcast(sleep, ID ~ group, value.var = "extra")
tmp$diff <- tmp[, "2"] - tmp[, "1"]
head(tmp, 3)
  ID    1   2 diff
1  1  0.7 1.9  1.2
2  2 -1.6 0.8  2.4
3  3 -0.2 1.1  1.3

Ejercicio 7.5 as.data.frame(Titanic) es una tabla en formato largo que contiene información sobre los viajeros que sobrevivieron o perecieron el naufragio del Titanic. Calcula la proporción de viajeros que sobrevivieron por clase, sexo y grupo de edad. Nota: recuerda que para indicar varias filas, ya sea en filas o columnas (aunque lo segundo no se recomienda), los nombres de columna tienen que indicarse separados del signo +.

No obstante, al usar dcast pueden ocurrir situaciones como que ilustra el ejemplo siguiente. En primer lugar construimos una versión larga de iris:

iris_long <- melt(iris)
head(iris_long, 4)
  Species     variable value
1  setosa Sepal.Length   5.1
2  setosa Sepal.Length   4.9
3  setosa Sepal.Length   4.7
4  setosa Sepal.Length   4.6

Hemos hecho la operación melt(iris) para crear un conjunto de datos de práctica. Sin embargo, en esta operación se pierde información: ya no sabemos a qué fila de la tabla original corresponden las de la versiòn arreglada. Por lo tanto, la reconstrucción de los datos originales es imposible.

Ahora, al hacer

dcast(iris_long, Species ~ variable)
Aggregation function missing: defaulting to length
     Species Sepal.Length Sepal.Width Petal.Length Petal.Width
1     setosa           50          50           50          50
2 versicolor           50          50           50          50
3  virginica           50          50           50          50

obtenemos un resultado extraño (además de un warning). Se debe a que en cada posición de la tabla resultante hay más de un valor candidato; de hecho, 50 de ellos. La función dcast, por defecto, combina esos valores usando la función length. Sin embargo, es posible especificar otras:

dcast(iris_long, Species ~ variable, fun.aggregate = mean)
     Species Sepal.Length Sepal.Width Petal.Length Petal.Width
1     setosa        5.006       3.428        1.462       0.246
2 versicolor        5.936       2.770        4.260       1.326
3  virginica        6.588       2.974        5.552       2.026

La función dcast admite otras invocaciones equivalentes. Por ejemplo, como hemos visto más arriba, se puede especificar explícitamente la variable sobre la que opera la nueva función:

dcast(iris_long, Species ~ variable, fun.aggregate = mean, value.var = "value")

No obstante, la opción más sucinta es

dcast(iris_long, Species ~ variable, mean)

Ejercicio 7.6 Calcula la media de cada variable meteorológica de airquality por mes.

Para los ejercicios siguientes vamos a utilizar una tabla descargada del INE que contiene información sobre la situación laboral de los ciudadanos de todas las provincias de España por periodo, provincia y sexo.

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

Los valores que aparecen en la tabla son valores absolutos: es el número de personas en miles.

Los datos están en formato largo, pero se pueden pivotar, p.e., así:

paro_alt <- dcast(paro, Gender + Provinces + Periodo ~ Situation)

Ejercicio 7.7 Añade a paro_alt una columna adicional con la tasa de paro (desempleados entre población activa).

El ejercicio anterior demuestra cómo en ocasiones es conveniente crear un determinado tipo de formato ancho para operar sobre los datos más fácilmente. En concreto, para colocar en la misma fila valores sobre los que realizar operaciones; en nuestro caso, un cociente.

Ejercicio 7.8 Identifica las provincias y periodos en los que la tasa de paro masculina supera a la femenina.

Si se utilizan todas las variables que forman parte de la clave de una tabla en la fórmula de dcast, en cada casilla de la tabla resultante aparece una única observación. Si se omite una o más, comienzan a aparecer valores repetidos y eso permite utilizar funciones de agregación para realizar operaciones similares a un group by de SQL.

Ejercicio 7.9 Obtén totales para toda España (i.e., ignorando la provincia y el sexo) usando dcast.

No obstante, en una sección posterior veremos una manera alternativa, la recomendada, para realizar este tipo de agregaciones.

7.3 Resumen y referencias

En esta sección hemos aprendido a distinguir entre datos en formato largo (o arreglados) y ancho. Los dos tipos tienen sus ventajas e inconvenientes. Los datos arreglados son conceptualmente más simples de describir y comprender. Además, facilitan ciertas transformaciones, como las operaciones por bloques o el filtrado (selección de un determinado subconjunto de las filas).

Muchas transformaciones de datos implican operar sobre valores que, en una tabla arreglada, estarían en filas distintas. Para ello es necesario yuxtaponerlos, i.e., colocarlos en una misma fila. Para ello es necesario pivotar la tabla. Suele ser habitual volver a arreglar los datos después de realizadas estas transformaciones.

De hecho, existen flujos de trabajo de manipulación de datos que consisten en una secuencia de pivotaciones entre el formato largo y distintas versiones de formatos anchos en los que se realizan determinadas operaciones.

Estas operaciones de pivotación no son sencillas con otras herramientas habituales, como SQL o Spark. En R existe una función de serie, reshape, que permite realizar estas transformaciones. Sin embargo, no se recomienda su uso: es muy mejorable, particularmente, en lo concerniente a su usabilidad.

Debido a esas limitaciones, se introdujo en R el paquete reshape2 (que es, a su vez, una evolución de un paquete anterior, reshape). Este paquete contiene esencialmente dos funciones fundamentales, melt y dcast, que son las que hemos tratado aquí.

El autor de este paquete teorizó más tarde sobre los datos en formatos anchos y largos en el artículo, muy recomendable, Tidy Data. Además, a raíz de ese artículo, desarrolló un paquete adicional, tidyr. El paquete tidyr tiene una función similar a melt, gather y otra similar a dcast, spread. En http://www.milanor.net/blog/reshape-data-r-tidyr-vs-reshape2/ se comparan ambos paquetes.

7.4 Ejercicios adicionales

Ejercicio 7.10 Identifica las provincias en las que más crece la tasa de paro en 2014.

Ejercicio 7.11 Lee este fichero (puedes hacerlo pasándole directamente la URL a read.table) y dispónlo en formato largo.

Ejercicio 7.12 Lee este fichero que contiene datos de casos de tuberculosis por país y año partido por sexos (m/f) y grupos de edad (u es desconocido). Ponlo en formato largo descomponiendo las variables que combinan edad y sexo convenientemente. Después, calcula el número de casos:

  • por país y año
  • por país, año y sexo
  • por país, año y grupo de edad

Ejercicio 7.13 Usa el conjunto de datos de los casos de tuberculosis y repite el ejercicio de la agregación pero solo para algún país concreto.


  1. La traducción de estos dos verbos es, respectivamente, fundir y moldear; es conviente recordarlo.↩︎

  2. De acuerdo con el DRAE, arreglado significa reducido o sujeto a regla; efectivamente, veremos cómo los datos en formato largo siguen una serie de reglas que se echan mucho de menos cuando no se cumplen.↩︎

  3. Cuyo autor lo es también del paquete reshape2, el tema de esta sección↩︎