Uso de la función `gather` para preparar datos

MP

2018/10/02

Este post acompaña al ejercicio 5.2. La resolución de una de las consignas requería un tipo de transformación de datos que sirve para ese caso y para muchos otros.

Formato ancho y formato largo

En la clase hemos hablado de los datos tidy o prolijos. Se trata de datos que tienen dos propiedades:

  1. Son rectangulares. Es decir, todas la columnas tienen el mismo largo y todas las filas tienen el mismo largo1. Si hay valores faltantes se rellenan, generalmente con NA.

  2. Cada columna es una variable y cada fila una observación.

A manera de ejemplo, los datos de la base de la Encuesta Nacional de Migración tienen este formato.

library(foreign)
library(tidyverse)
library(knitr)

# R permite camino hacia los datos como URL, es decir, podemos leer directamente un archivo desde una dirección a través de http.
# Sin embargo es recomendable crear una copia local de los datos 1) por velocidad y 2) para no abusar del servidor que nos ofrece los datos, 3) por si los datos desaparecen o cambian de ubicación.

migracion <- read.spss("http://www.losmexicanos.unam.mx/migracion/encuesta_nacional/base_datos/Encuesta_Nacional_de_Migracion.sav", to.data.frame = TRUE)
diccionario <- tibble(nombre = names(migracion), 
                    etiquetas = str_trim(attr(migracion, "variable.labels")))
migracion <- as.tibble(migracion)
migracion %>% 
  sample_n(3) %>% 
  select(edo, starts_with("p7_"), Pondi2) %>% 
  kable(caption = "Cada columna es una variable, cada fila una observación.")
Table 1: Cada columna es una variable, cada fila una observación.
edo p7_1 p7_2 p7_3 p7_4 p7_5 p7_6 p7_7 p7_8 p7_9 p7_10 Pondi2
15 No No No No No No No No No No 38032
15 No 65612
26 Sí, en parte 27811

Esta formato de datos es el más usado y tener una convención para su creación y uso podemos tener expectativas razonables sobre como se van a comportar los inputs y outputs. Sin embargo para algunas operaciones es más práctico dar a los datos un tipo de diferente de formato: pares2 de claves y valores. El formato básico de claves y valores tiene dos columnas –y sólo dos columnas, independientemente de la cantidad de variables con la que estemos tratando. Una columna registra las claves, en este caso los nombres de cada variable. La otra registra el valor para esa variable. En este contexto usamos de manera intercambiable “pares de clave-valor” y “formato largo”.

migracion %>% 
  sample_n(3) %>% 
  select(edo, starts_with("p7_"), Pondi2) %>% 
  gather() %>% 
  kable(caption = "Los nombre de columna ahora están en filas.")
Table 2: Los nombre de columna ahora están en filas.
key value
edo 28
edo 16
edo 15
p7_1
p7_1
p7_1
p7_2
p7_2
p7_2
p7_3
p7_3
p7_3
p7_4
p7_4
p7_4 No
p7_5
p7_5
p7_5
p7_6 No
p7_6
p7_6 No
p7_7
p7_7
p7_7
p7_8
p7_8
p7_8
p7_9
p7_9
p7_9
p7_10
p7_10
p7_10 No
Pondi2 190556
Pondi2 37800
Pondi2 31209

Los datos son exactamente los mismos y podemos revertir el proceso, regresando los datos al formato original. Lo que hicimos fue pasar de unos datos en formato ancho –muchas columnas, pocas filas- a uno largo –pocas columnas, muchas filas, la información de los nombres de columa ahora pasó a una fila. Como se ve en el código lo hicimos con la función gather(), del paquete tidyr que se encuentra a su vez en el metapaquete tidyverse.

¿Para qué usar formato largo?

El formato largo es muy útil en varios escenarios en los que necesitamos hacer explícita la información sobre los niveles de agrupamiento de nuestros datos. En la práctica transformamos nuestros datos a formato largo antes de aplicar operaciones agrupadas con group_by a través de mutate() o summarise() o para gráficos de ggplot en los que vamos a usar las propiedades de agrupación de los datos con facet_wrap(), fill() o color. Las funciones que mencionamos aprovechan muy bien a los datos largos, ya que podemos utilizar a la/s columna/s de clave/s para crear grupos y aplicar funciones sobre esos grupos.

Aplicación al ejercicio 5.2.

En el caso de los gráficos es muy útil cuando nos interesa graficar más de una variable y esas variables se encuentran originalmente es más de una columna. En lugar de graficar cada columna por separado las unimos alargándolas y las graficamos como una sólo columna (los valores) separada en grupos por las claves. En este caso en lugar de generar una par de claves y valores vamos a necesitar un trío de claves y valores: como estamos trabajando con datos ponderados es necesario convervar los ponderadores cuando hacemos la transformación a formato largo.

Formato original de los datos del ejercicio 5.2.

En el formato original los datos son prolijos: cada fila una observación, cada columna una variable. En este caso fila es una persona encuestada, cada columna una pregunta y cada intersección de fila/columna la respuesta de una persona encuestada a una pregunta.

migracion %>% 
  head() %>% 
  select(starts_with("p7_"), Pondi2) %>% 
  kable()
p7_1 p7_2 p7_3 p7_4 p7_5 p7_6 p7_7 p7_8 p7_9 p7_10 Pondi2
58676
NS NS NS NS NS NS NS NS NS NS 11725
NS No No No No No 34727
Sí, en parte 56490
Sí, en parte Sí, en parte 113882
58275

Sin embargo para hacer el gráfico aprovechando los agrupamientos necesitamos que los datos tengan un formato con tres columnas: una para las preguntas, otra para las respuestas y una tercera para el ponderador. De este modo podemos posteriormente agrupar por preguntas y respuestas y generar todos los conteos de manera prolija, obteniendo el resultado en un data.frame listo para pasar a ggplot.

Formato largo: toma 1.

La primera aproximación es pasar gather con los argumentos por defecto3 y ver que pasa. Reduciré el número de preguntas para que el resultado sea más facil de visualizar y comprender.

migracion %>% 
  head %>%                         #La cabeza, sólo las primeras filas
  select(p7_1, p7_2, Pondi2) %>%  
  gather() %>% 
  kable(caption = "El ponderador no va ahí")
Table 3: El ponderador no va ahí
key value
p7_1
p7_1 NS
p7_1
p7_1 Sí, en parte
p7_1
p7_1
p7_2
p7_2 NS
p7_2
p7_2
p7_2
p7_2
Pondi2 58676
Pondi2 11725
Pondi2 34727
Pondi2 56490
Pondi2 113882
Pondi2 58275

¿Lo logramos? Sí, en parte. Las preguntas ahora están en la columna key^[clave, es el nombre en inglés que por defecto utiliza gather) y las respuestas en value4. Sin embargo el ponderador también está en filas a aparte. Para poder aplicarlo a momento de hacer los conteos necesitamos que el ponderador sea una columna separada, aparte de key y value, que repita su magnitud para cada par de pregunta/respuesta. Necesitamos un trío de claves y valores. gather contempla esta situación y tiene un operador muy simple para indicarle que no queremos que una variable se convierta en pares de clave-valor y se conserve en la estructura original, repitíendose para da grupo. El operador es el signo menos -, que antecede el nombre de columna o columnas para las que queremos este comportamiento.

Por defecto gather utiliza los nombres key y value para las columnas de claves y valor. Sin embargo es buena idea no usar estos nombres genéricos no son muy útiles –podrían ser cualquier cosa- y es buena práctica usar nombres significativos, relacionados con nuestros datos. En este caso podrías llamar a esas columnas Pregunta y Respuesta, ya que contienen preguntas y respuestas.

Cuando queremos excluir una columna de la transformación a formato largo es necesario suministrar nombres para las columnas de claves y valores. El operador - no funciona si no suministramos los nombres. Los nombres de clave y valor son los dos primeros argumentos de la función, las variables excluidas con - son los argumentos sucesivos.

Formato largo: toma 2.

migracion %>% 
  head %>% 
  select(p7_1, p7_2, Pondi2) %>%  
  gather(Pregunta, Respuesta, -Pondi2) %>% 
  kable(caption = "El ponderador va en columna aparte.")
Table 4: El ponderador va en columna aparte.
Pondi2 Pregunta Respuesta
58676 p7_1
11725 p7_1 NS
34727 p7_1
56490 p7_1 Sí, en parte
113882 p7_1
58275 p7_1
58676 p7_2
11725 p7_2 NS
34727 p7_2
56490 p7_2
113882 p7_2
58275 p7_2

Conteos agrupados y ponderados

Con la estructura de datos correcta podemos hacer los conteos de las respuestas a cada pregunta aplicando el ponderador muestal. Seguimos con el ejemplo con pocas columnas para facilitar la lectura de las salidas, pero podemos escalar este procedimiento a tantas columnas como querramos, la disponibilidad de memoria RAM es el límite.

Pasos:

  1. Aplicamos los agrupamientos a los transformados. En este caso queremos un grupo para cada combinación de pregunta/respuesta.
  2. Aplicamos el conteo indicando a Pondi2 como wt= o ponderador muestral.
  3. Asignamos un nombre al data.frame con los conteos. No es estrictamente necesario, pero en este caso corta el código y hace más legible la sintaxis posterior del gráfico.
migracion %>% 
  select(p7_1, p7_2, Pondi2) %>%  
  gather(Pregunta, Respuesta, -Pondi2) %>% 
  group_by(Pregunta, Respuesta) %>% 
  count(wt = Pondi2) -> conteos_p7
kable(conteos_p7, caption = "Conteos finales, casi listos para graficar")
Table 5: Conteos finales, casi listos para graficar
Pregunta Respuesta n
p7_1 NC 715096
p7_1 No 13580012
p7_1 NS 2021106
p7_1 43552653
p7_1 Sí, en parte 19951882
p7_2 NC 835828
p7_2 No 11550564
p7_2 NS 2964105
p7_2 42893890
p7_2 Sí, en parte 21576362

Etiquetas de las variables 1

En el bootstrap del ejercicio 5 se crea un diccionario de variables con la siguiente estructura:

diccionario %>% 
  filter(str_detect(nombre, "p7_")) %>% 
  kable(caption = "Relación de nombres cortos y largos del bloque p7.")
Table 6: Relación de nombres cortos y largos del bloque p7.
nombre etiquetas
p7_1 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra religión?
p7_2 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra raza (negro, chino,etc.)?
p7_3 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas indígenas?
p7_4 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas gais?
p7_5 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas con ideas políticas distintas a las suyas?
p7_6 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas enfermas de sida?
p7_7 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas personas con alguna discapacidad?
p7_8 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otro país (extranjeras)?
p7_9 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas con una cultura distinta?
p7_10 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas lesbianas?

Los datos largos tienen otra características interesante, están listos para hacer joins de datos. De este podemos reemplazar facilmente los códigos de las preguntas (p*_*) por los nombres largos, mucho más significativos para quién vaya a leer nuestro reporte/paper/tesis.

Pasos:

  1. Cambiar el nombre de la columna Pregunta de conteos_p7 a nombre, de modo que coincida con la variable con igual información en diccionario.
  2. Hacer un left_join, de modo que se conserven solamente las filas de la izquierda, es decir, de conteos_p7.
conteos_p7 %>% 
  rename("nombre" = Pregunta) %>% 
  kable(caption = "Al cambiar el nombre aquí no necesito especificar claves de unión en el join")
Table 7: Al cambiar el nombre aquí no necesito especificar claves de unión en el join
nombre Respuesta n
p7_1 NC 715096
p7_1 No 13580012
p7_1 NS 2021106
p7_1 43552653
p7_1 Sí, en parte 19951882
p7_2 NC 835828
p7_2 No 11550564
p7_2 NS 2964105
p7_2 42893890
p7_2 Sí, en parte 21576362
conteos_p7 %>% 
  rename("nombre" = Pregunta) %>% 
  left_join(diccionario) %>% 
  kable(caption = "La columna etiqueta tiene los nombres largos que usaré para graficar")
Table 7: La columna etiqueta tiene los nombres largos que usaré para graficar
nombre Respuesta n etiquetas
p7_1 NC 715096 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra religión?
p7_1 No 13580012 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra religión?
p7_1 NS 2021106 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra religión?
p7_1 43552653 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra religión?
p7_1 Sí, en parte 19951882 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra religión?
p7_2 NC 835828 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra raza (negro, chino,etc.)?
p7_2 No 11550564 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra raza (negro, chino,etc.)?
p7_2 NS 2964105 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra raza (negro, chino,etc.)?
p7_2 42893890 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra raza (negro, chino,etc.)?
p7_2 Sí, en parte 21576362 7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de otra raza (negro, chino,etc.)?

Etiquetas de variables 2

Los nombres largos de las variables –las preguntas tal como fueron formuladas- son muy informativas. Quizás demasiado. En el gráfico van a superponerse y van a ser difíciles de leer.

Aproximación 1: eliminar el texto repetido.

Como la primera parte de la pregunta se repite una aproximación a este problema es eliminarla y mencionarla una vez para todo el gráfico, conservando solamente la parte que cambia.

Advertencia vamos a usar expresiones regulares. La RegEx no se entienden, se usan.

Pasos:

  1. Usando mutate aplicamos la función str_remove(), que elimina la cadena de caracteres (patrón o pattern en help(str_remove)) de la columna. En este caso vamos a eliminar la cadena “7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de”
  2. Algunos símbolos (?*.^|& entre otros, están reservados por las expresiones regulares y es necesario “escaparlos”. Un caracter se escapa usando la barra invertida \. Como la barra invertida es a su vez un caracter reservado por R tenemos que escaparlo, así que utilizamos la doble barra invertida \\. Como markdown reserva las barras invertidas para que en este documento salgan 2 tuve que escribir 4. Y así hasta que se torna peligroso
conteos_p7 %>% 
  rename("nombre" = Pregunta) %>% 
  left_join(diccionario) %>%         #Hasta acá repito lo que hice en el bloque anterior.
  mutate(etiquetas = str_remove(etiquetas, "7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas de ")) %>% 
  kable(caption = "Más legible")
Table 8: Más legible
nombre Respuesta n etiquetas
p7_1 NC 715096 otra religión?
p7_1 No 13580012 otra religión?
p7_1 NS 2021106 otra religión?
p7_1 43552653 otra religión?
p7_1 Sí, en parte 19951882 otra religión?
p7_2 NC 835828 otra raza (negro, chino,etc.)?
p7_2 No 11550564 otra raza (negro, chino,etc.)?
p7_2 NS 2964105 otra raza (negro, chino,etc.)?
p7_2 42893890 otra raza (negro, chino,etc.)?
p7_2 Sí, en parte 21576362 otra raza (negro, chino,etc.)?

Aproximación 2: agregar saltos de línea.

Pasos:

  1. Usando mutate() aplicamos la función str_wrap(), que agrega saltos de líneas cada determinado número caracteres respetando los límites de palabra. De ese modo en lugar de tener una línea muy larga tenemos varias líneas cortas. La expresión literal de una salto de línea en R es \\n, una barra invertida antes de una n minúscula.
  • A str_wrap se le puede indicar el ancho aproximado para los saltos de línea: cuanto más pequeño el ancho más frecuentes los saltos de línea.
conteos_p7 %>% 
  rename("nombre" = Pregunta) %>% 
  left_join(diccionario) %>%         #Hasta acá repito lo que hice en el bloque anterior.
  mutate(etiquetas = str_wrap(etiquetas, 20)) 
## # A tibble: 10 x 4
## # Groups:   nombre, Respuesta [10]
##    nombre Respuesta           n etiquetas                                 
##    <chr>  <chr>           <dbl> <chr>                                     
##  1 p7_1   NC             715096 "7 ¿Estaría dispuesto\no no estaría\ndisp…
##  2 p7_1   No           13580012 "7 ¿Estaría dispuesto\no no estaría\ndisp…
##  3 p7_1   NS            2021106 "7 ¿Estaría dispuesto\no no estaría\ndisp…
##  4 p7_1   Sí           43552653 "7 ¿Estaría dispuesto\no no estaría\ndisp…
##  5 p7_1   Sí, en parte 19951882 "7 ¿Estaría dispuesto\no no estaría\ndisp…
##  6 p7_2   NC             835828 "7 ¿Estaría dispuesto\no no estaría\ndisp…
##  7 p7_2   No           11550564 "7 ¿Estaría dispuesto\no no estaría\ndisp…
##  8 p7_2   NS            2964105 "7 ¿Estaría dispuesto\no no estaría\ndisp…
##  9 p7_2   Sí           42893890 "7 ¿Estaría dispuesto\no no estaría\ndisp…
## 10 p7_2   Sí, en parte 21576362 "7 ¿Estaría dispuesto\no no estaría\ndisp…

Gráfico

Ya tenemos todo preparado para hacer el gráfico.

  1. Los conteos agrupados y ponderados.
  2. Los nombre de etiqueta larga propiamente formateados para que hacerlos legibles.

Pasos:

  1. Ubicar la etiqueta de cada pregunta en el eje x y los conteos de respuestas en el eje y.
  2. Utilizar el argumento fill = de ggplot para que cada respuesta tenga un color de relleno5 igual
  3. Especificar position = "dodge"6 a geom_bar para barras lado a lado. Por defecto position = "stack" y se generan barras apiladas.
  4. Ubicar en el subtítulo del gráfico la parte repetida de la pregunta.
library(hrbrthemes)
migracion %>% 
  select(starts_with("p7_"), Pondi2) %>%  
  gather(Pregunta, Respuesta, -Pondi2) %>% 
  group_by(Pregunta, Respuesta) %>% 
  count(wt = Pondi2) %>% 
  rename("nombre" = Pregunta) %>% 
  left_join(diccionario) %>%
  mutate(etiquetas = str_remove(etiquetas, 
                                "7 ¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas "), 
         etiquetas = str_wrap(etiquetas, 15)) %>% 
  ungroup ()%>% 
  mutate(Respuesta = factor(Respuesta, levels = c("Sí", "Sí, en parte", "No", "NS", "NC"))) -> 
           conteos_p7_etiquetados

conteos_p7_etiquetados %>% 
  ggplot(aes(x = etiquetas, y = n, fill = Respuesta)) + 
    geom_col(position = "dodge") + 
    labs(title = "Actitudes de tolerancia en México", 
       subtitle = "¿Estaría dispuesto o no estaría dispuesto a permitir que en su casa vivieran personas...",
       caption = "Elaboración propia\nDatos Encuesta Nacional de Migración", 
       x = NULL, 
       y = NULL) + 
    scale_y_continuous(labels = scales::comma) + 
    theme_ipsum_rc() 

Descargar la versión para impresión de este gráfico.


  1. Aunque esto no significa que filas y columnas tienen el mismo, en ese caso serían datos cuadrados. Todos los datos cuadrados son rectangulares pero no todos los datos rectangulares son cuadrados.

  2. O tríos, cuartetos, etc.

  3. Para conocer los argumentos por defecto de gather use help(gather).

  4. Valor, ibid.

  5. El argumento color = controla el color del contorno, no del relleno.

  6. Sí, yo también detesto esa sintaxis con comillas, pero es lo que hay