Capítulo 16 Tidy vs messy data: daticos ordenados

Tidy datasets are all alike, but every messy dataset is messy in its own way (Hadley Wickham, Chief Scientist en RStudio)

Hasta ahora solo le hemos dado importancia al «qué» pero no al «cómo» manejamos los datos. La organización de nuestros datos es fundamental para que su preparación y explotación sea lo más eficiente posible: la limpieza y preprocesamiento puede llevarnos hasta el 80% del tiempo en nuestro análisis si no se hace forma correcta (Dasu and Johnson 2003).

Flujo deseable de datos según Hadley Wickham, extraída de <https://r4ds.had.co.nz/wrangle-intro.html>

Imagen/gráfica 16.1: Flujo deseable de datos según Hadley Wickham, extraída de https://r4ds.had.co.nz/wrangle-intro.html

El concepto tidy data fue introducido por Hadley Wickham (Wickham 2014) como el primer paso del entorno de paquetes que posteriormente se fueron desarrollando bajo el nombre de tidyverse. Los conjuntos tidy o datos ordenados tienen tres objetivos

  • Estandarización en su estructura.
  • Sencillez en su manipulación.
  • Listos para ser modelizados y visualizados.

Para ello, los datos ordenados o tidy data deben cumplir:

  1. Cada variable en una columna.
  2. Cada observación/registro/individuo en una fila diferente.
  3. Cada celda con un único valor.
  4. Cada conjunto o unidad observacional conforma una tabla.
  5. Si contamos con múltiples tablas, debemos tener una columna común en cada una que nos permita cruzarlas.
Infografía con datos ordenados (tidy data) extraída de <https://r4ds.had.co.nz/tidy-data.html>

Imagen/gráfica 16.2: Infografía con datos ordenados (tidy data) extraída de https://r4ds.had.co.nz/tidy-data.html

16.1 Entorno tidyverse

Conocemos ya un formato amable de almacenar los datos como son los data.frame de tipo tibble. Sin embargo muchas veces los datos no los tenemos en el formato deseado, o directamente queremos realizar algunas transformaciones en los mismos, ordenarlos, crear nuevas variables u obtener resúmenes numéricos. Para trabajar con los datos vamos a cargar tidyverse, un entorno de paquetes para el manejo de datos (ver más detalles en Transformando los datos: incursión al universo tidyverse).

install.packages("tidyverse") # SOLO la primera vez
Imagen extraída de <https://sporella.github.io/datos_espaciales_presentacion/#30>

Imagen/gráfica 16.3: Imagen extraída de https://sporella.github.io/datos_espaciales_presentacion/#30

El entorno tidyverse es una de las herramientas más importantes en el manejo de datos en R, una colección de paquetes pensada para el manejo, la exploración, el análisis y la visualización de datos, compartiendo una misma filosofía y gramática.

Imagen extraída de <https://www.storybench.org/getting-started-with-tidyverse-in-r/>

Imagen/gráfica 16.4: Imagen extraída de https://www.storybench.org/getting-started-with-tidyverse-in-r/

  • tidyr: para adecuar los datos a tidy data

  • tibble: mejorando los data.frame para un manejo más eficiente

  • Paquete readr para una carga rápida y eficaz de datos rectangulares (formatos .csv, .tsv, etc). Paquete readxl para importar archivos .xls y .xlsx. Paquete haven para importar archivos desde SPSS, Stata y SAS. Paquete httr para importar desde web. Paquete rvest para web scraping.

  • dplyr: una gramática de manipulación de datos para facilitar su procesamiento.

  • ggplot2: una gramática para la visualización de datos.

  • Paquete stringr para un manejo sencillo de cadenas de texto. Paquete {forcast} para un manejo de variables cualitativas (en R conocidas como factores). Paquete purrr para el manejo de listas y una programación funcional con las mismas. Paquete lubridate para el manejo de fechas.

Puedes ver su documentación completa en en https://www.tidyverse.org/.

 

En este entorno, tendremos un operador clave: el operador pipeline (%>%). Dicho operador lo debemos interpretar como una flecha que une nodos, y nos servirá para concatenar operaciones sobre un conjunto de datos de forma legible. Por ejemplo, si tuviésemos tres funciones first(), second() y third(), la opción más inmediata sería anidar las tres funciones.

third(second(first(x)))

El anidamiento es compacto pero dificulta la lectura posterior del código: con el pipeline %>% podremos escribir (y leer) la concetanción de acciones de izquierda a derecha:

first(x) %>% second(x) %>% third(x)

Dicho operador viene heredado del paquete magrittr, lo que hace que muchos paquetes de tidyverse dependan de él. Para evitar esta dependencia (cuantos menos paquetes tengamos que cargar, mejor), desde la versión 4.1.0 de R, disponemos de un operador pipeline nativo de R, el operador |> (disponible además fuera del entorno tidyverse. En este manual seguiremos usando el operador %>%, pero es muy probable que en cada vez más códigos que busques por la red observes |> en detrimento de %>% (aunque el primero es más rápido, más eficiente y sin necesidad de cargar otros paquetes).

16.2 Messy data: valores en columnas en lugar de variables

Vamos a visualizar la tabla table4a del paquete tidyr (que ya lo tenemos cargado del entorno tidyverse).

table4a
## # A tibble: 3 × 3
##   country     `1999` `2000`
## * <chr>        <int>  <int>
## 1 Afghanistan    745   2666
## 2 Brazil       37737  80488
## 3 China       212258 213766

Si te fijas, tenemos una columna country, representando una variable con el nombre de los países, ¡pero las otras columnas no representan cada una a una sola variable! Ambas son la misma variable, solo que medida en años distintos (que debería ser a su vez otra variable): cada fila está representando dos observaciones (1999, 2000) en lugar de un solo registro. Lo que haremos será incluir una nueva columna llamada year que nos marque el año y otra values que nos diga el valor de la variable de interés en cada uno de esos años.

Con la función pivot_longer del mencionada paquete le indicaremos lo siguiente:

  • cols: el nombre de las columnas que vamos a pivotar (con comillas porque son números, no texto como nombre).
  • names_to: el nombre de la columna a la que vamos a mandar los valores que figuran ahora en los nombres de las columnas.
  • values_to: el nombre de la columna a la que vamos a mandar los valores.
library(tidyr)
table4a %>% pivot_longer(cols = c("1999", "2000"),
                         names_to = "year",
                         values_to = "values")
## # A tibble: 6 × 3
##   country     year  values
##   <chr>       <chr>  <int>
## 1 Afghanistan 1999     745
## 2 Afghanistan 2000    2666
## 3 Brazil      1999   37737
## 4 Brazil      2000   80488
## 5 China       1999  212258
## 6 China       2000  213766

Ahora tenemos un registro por fila, una variable por columna y cada celda representa un único valor. Este ejemplo de messy data lo podemos encontrar muy a menudo cuando construimos rangos de variables pensando que es mejor tener una tabla más compacta (alargar la tabla a lo ancho en lugar de a lo largo). Es el caso de la tabla relig_income.

relig_income
## # A tibble: 18 × 11
##    religion `<$10k` `$10-20k` `$20-30k` `$30-40k` `$40-50k` `$50-75k` `$75-100k`
##    <chr>      <dbl>     <dbl>     <dbl>     <dbl>     <dbl>     <dbl>      <dbl>
##  1 Agnostic      27        34        60        81        76       137        122
##  2 Atheist       12        27        37        52        35        70         73
##  3 Buddhist      27        21        30        34        33        58         62
##  4 Catholic     418       617       732       670       638      1116        949
##  5 Don’t k…      15        14        15        11        10        35         21
##  6 Evangel…     575       869      1064       982       881      1486        949
##  7 Hindu          1         9         7         9        11        34         47
##  8 Histori…     228       244       236       238       197       223        131
##  9 Jehovah…      20        27        24        24        21        30         15
## 10 Jewish        19        19        25        25        30        95         69
## 11 Mainlin…     289       495       619       655       651      1107        939
## 12 Mormon        29        40        48        51        56       112         85
## 13 Muslim         6         7         9        10         9        23         16
## 14 Orthodox      13        17        23        32        32        47         38
## 15 Other C…       9         7        11        13        13        14         18
## 16 Other F…      20        33        40        46        49        63         46
## 17 Other W…       5         2         3         4         2         7          3
## 18 Unaffil…     217       299       374       365       341       528        407
## # … with 3 more variables: $100-150k <dbl>, >150k <dbl>,
## #   Don't know/refused <dbl>

Salvo la primera, el resto de columnas tienen como nombre los valores de una variable en sí misma (ingresos). Para ordenar los datos vamos a razonar de la misma manera solo que ahora, en lugar de indicarle el nombre de todas las columnas que queremos usar de entrada, vamos a indicarle de forma más corta la columna que NO queremos seleccionar.

# No necesitamos las comillas en el nombre de columnas salvo que tengan caracteres que no sean letras
relig_income %>% pivot_longer(-religion, names_to = "ingresos",
                              values_to = "frecuencia")
## # A tibble: 180 × 3
##    religion ingresos           frecuencia
##    <chr>    <chr>                   <dbl>
##  1 Agnostic <$10k                      27
##  2 Agnostic $10-20k                    34
##  3 Agnostic $20-30k                    60
##  4 Agnostic $30-40k                    81
##  5 Agnostic $40-50k                    76
##  6 Agnostic $50-75k                   137
##  7 Agnostic $75-100k                  122
##  8 Agnostic $100-150k                 109
##  9 Agnostic >150k                      84
## 10 Agnostic Don't know/refused         96
## # … with 170 more rows

Lo que hacemos con pivot_longer() es «ampliar» la tabla, haciéndola más larga (más filas) pero con menos columnas.

16.3 Messy data: una observación guardada en varias filas

Vamos a visualizar ahora la tabla table2 del paquete tidyr.

table2
## # A tibble: 12 × 4
##    country      year type            count
##    <chr>       <int> <chr>           <int>
##  1 Afghanistan  1999 cases             745
##  2 Afghanistan  1999 population   19987071
##  3 Afghanistan  2000 cases            2666
##  4 Afghanistan  2000 population   20595360
##  5 Brazil       1999 cases           37737
##  6 Brazil       1999 population  172006362
##  7 Brazil       2000 cases           80488
##  8 Brazil       2000 population  174504898
##  9 China        1999 cases          212258
## 10 China        1999 population 1272915272
## 11 China        2000 cases          213766
## 12 China        2000 population 1280428583

Fíjate en las cuatro primeras filas: los registros con el mismo año deberían ser el mismo, es la misma información, debería estar en la misma fila, pero está dividada en dos. Por un lado una fila para la variable cases y otra para population. Lo que haremos será lo opuesto a antes: con pivot_wider() «ampliaremos» la tabla a lo ancho, haciéndola menos (menos filas) pero con más columnas.

  • names_from: el nombre de la columna de la que vamos a sacar las nuevas columnas que vamos a crear (cases y population).
  • values_from: el nombre de la columna de la que vamos a sacar los valores.
table2 %>% pivot_wider(names_from = type, values_from = count)
## # A tibble: 6 × 4
##   country      year  cases population
##   <chr>       <int>  <int>      <int>
## 1 Afghanistan  1999    745   19987071
## 2 Afghanistan  2000   2666   20595360
## 3 Brazil       1999  37737  172006362
## 4 Brazil       2000  80488  174504898
## 5 China        1999 212258 1272915272
## 6 China        2000 213766 1280428583

Ahora tenemos cada registro en una fila, que nos indica país-año-casos-población.

16.4 Messy data: una celda con múltiples valores

Por último vamos a visualizar la tabla table3 del paquete tidyr.

table3
## # A tibble: 6 × 3
##   country      year rate             
## * <chr>       <int> <chr>            
## 1 Afghanistan  1999 745/19987071     
## 2 Afghanistan  2000 2666/20595360    
## 3 Brazil       1999 37737/172006362  
## 4 Brazil       2000 80488/174504898  
## 5 China        1999 212258/1272915272
## 6 China        2000 213766/1280428583

En la variable rate hay guardados dos valores, separados por /, lo que hace que en una celda no tiene un único valor sino dos. La función separate() del paquete tidyr nos permitirá separar los múltiples valores de la columnarate simplemente indicándole el nombre de las nuevas columnas en el argumento into = ..., creando una nueva columna para cada uno de ellos.

table3 %>% separate(rate, into = c("cases", "population"))
## # A tibble: 6 × 4
##   country      year cases  population
##   <chr>       <int> <chr>  <chr>     
## 1 Afghanistan  1999 745    19987071  
## 2 Afghanistan  2000 2666   20595360  
## 3 Brazil       1999 37737  172006362 
## 4 Brazil       2000 80488  174504898 
## 5 China        1999 212258 1272915272
## 6 China        2000 213766 1280428583

Por defecto lo que hace es localizar como separador cualquier caracter que no sea alfa-numérico. Si queremos un caracter concreto para dividir podemos indicárselo explícitamente

table3 %>% separate(rate, into = c("cases", "population"), sep = "/")
## # A tibble: 6 × 4
##   country      year cases  population
##   <chr>       <int> <chr>  <chr>     
## 1 Afghanistan  1999 745    19987071  
## 2 Afghanistan  2000 2666   20595360  
## 3 Brazil       1999 37737  172006362 
## 4 Brazil       2000 80488  174504898 
## 5 China        1999 212258 1272915272
## 6 China        2000 213766 1280428583

Si usas un separador que no está en los datos te devolverá dichas columnas vacías ya que no ha podido dividirlas.

table3 %>% separate(rate, into = c("cases", "population"), sep = ".")
## Warning: Expected 2 pieces. Additional pieces discarded in 6 rows [1, 2, 3, 4,
## 5, 6].
## # A tibble: 6 × 4
##   country      year cases population
##   <chr>       <int> <chr> <chr>     
## 1 Afghanistan  1999 ""    ""        
## 2 Afghanistan  2000 ""    ""        
## 3 Brazil       1999 ""    ""        
## 4 Brazil       2000 ""    ""        
## 5 China        1999 ""    ""        
## 6 China        2000 ""    ""

De la misma manera que podemos separar columnas también podemos unirlas. Para ello vamos a usar la tabla table5 del ya mencionado paquete. Con la función unite() vamos a unir el siglo (en century) y el año (en year), y al inicio le indicaremos como se llamará la nueva variable (año_completo).

table5 %>% unite(año_completo, century, year)
## # A tibble: 6 × 3
##   country     año_completo rate             
##   <chr>       <chr>        <chr>            
## 1 Afghanistan 19_99        745/19987071     
## 2 Afghanistan 20_00        2666/20595360    
## 3 Brazil      19_99        37737/172006362  
## 4 Brazil      20_00        80488/174504898  
## 5 China       19_99        212258/1272915272
## 6 China       20_00        213766/1280428583

Como pasaba en separate(), tiene un argumento de separador por defecto, en este caso sep = "_". Si queremos cambiarlo podemos hacerlo indicándoselo explícitamente.

table5 %>%
  unite(año_completo, century, year, sep = "")
## # A tibble: 6 × 3
##   country     año_completo rate             
##   <chr>       <chr>        <chr>            
## 1 Afghanistan 1999         745/19987071     
## 2 Afghanistan 2000         2666/20595360    
## 3 Brazil      1999         37737/172006362  
## 4 Brazil      2000         80488/174504898  
## 5 China       1999         212258/1272915272
## 6 China       2000         213766/1280428583

16.5 Consejos

CONSEJOS

 

Convertir variables al procesar

Una opción muy útil que podemos usar al aplicar la separación de los múltiples valores es convertir los datos al tipo adecuado. Los datos unidos en rate eran caracteres ya que tenía el separador / (no podían ser numéricos). Al separarlos, por defecto, aunque ahora ya son solo números, los separa como si fueran textos. Con convert = TRUE podemos indicarle que identifique el tipo de dato y lo convierta (fíjate en la cabecera de las columnas ahora).

table3 %>% 
  separate(rate, into = c("cases", "population"), convert = TRUE)
## # A tibble: 6 × 4
##   country      year  cases population
##   <chr>       <int>  <int>      <int>
## 1 Afghanistan  1999    745   19987071
## 2 Afghanistan  2000   2666   20595360
## 3 Brazil       1999  37737  172006362
## 4 Brazil       2000  80488  174504898
## 5 China        1999 212258 1272915272
## 6 China        2000 213766 1280428583

Dicha función también puede ser usada para dividir cifras, como por ejemplo el año

table3 %>% separate(year,
                    into = c("siglo", "año"),
                    sep = 2)
## # A tibble: 6 × 4
##   country     siglo año   rate             
##   <chr>       <chr> <chr> <chr>            
## 1 Afghanistan 19    99    745/19987071     
## 2 Afghanistan 20    00    2666/20595360    
## 3 Brazil      19    99    37737/172006362  
## 4 Brazil      20    00    80488/174504898  
## 5 China       19    99    212258/1272915272
## 6 China       20    00    213766/1280428583

16.6 📝 Ejercicios

(haz click en las flechas para ver soluciones)

📝Ejercicio 1: convierte en tidy data el siguiente data.frame.

tabla_tb <- tibble("trimestre" = c("T1", "T2", "T3"),
                   "2020" = c(10, 12, 7.5),
                   "2021" = c(8, 0, 9))
  • Solución:

El problema es que las dos columnas con nombres de año son en realidad valores que deberían pasar a ser variables, así que deberíamos disminuir aplicar pivot_longer()

# Aplicamos pivot_longer
tabla_tb %>% pivot_longer(cols = c("2020", "2021"),
                          names_to = "año", values_to = "valores")
## # A tibble: 6 × 3
##   trimestre año   valores
##   <chr>     <chr>   <dbl>
## 1 T1        2020     10  
## 2 T1        2021      8  
## 3 T2        2020     12  
## 4 T2        2021      0  
## 5 T3        2020      7.5
## 6 T3        2021      9

 

📝Ejercicio 2: convierte en tidy data el siguiente data.frame.

tabla_tb <- tibble("año" = c(2019, 2019, 2020, 2020, 2021, 2021),
                   "variable" = c("A", "B", "A", "B", "A", "B"),
                   "valor" = c(10, 9383, 7.58, 10839, 9, 32949))
  • Solución:

El problema es que las filas que comparten año son el mismo registro (pero con dos características que tenemos divididas en dos filas), así que deberíamos disminuir aplicar pivot_wider()

# Aplicamos pivot_wider
tabla_tb %>% pivot_wider(names_from = "variable", values_from = "valor")
## # A tibble: 3 × 3
##     año     A     B
##   <dbl> <dbl> <dbl>
## 1  2019 10     9383
## 2  2020  7.58 10839
## 3  2021  9    32949

 

📝Ejercicio 3: convierte en tidy data la tabla table5 del paquete tidyr.

  • Solución:

Primero uniremos el siglo y las dos últimas cifras del año para obtener el año completo (guardado en año)

table5 %>%
  unite(año, century, year, sep = "")
## # A tibble: 6 × 3
##   country     año   rate             
##   <chr>       <chr> <chr>            
## 1 Afghanistan 1999  745/19987071     
## 2 Afghanistan 2000  2666/20595360    
## 3 Brazil      1999  37737/172006362  
## 4 Brazil      2000  80488/174504898  
## 5 China       1999  212258/1272915272
## 6 China       2000  213766/1280428583

Tras ello deberemos separar el valor del ratio en denominador y numerador (ya que ahora hay dos valores en una celda), y convertiremos el tipo de dato en la salida para que sea número.

table5 %>%
  unite(año, century, year, sep = "") %>%
  separate(rate, c("numerador", "denominador"), convert = TRUE)
## # A tibble: 6 × 4
##   country     año   numerador denominador
##   <chr>       <chr>     <int>       <int>
## 1 Afghanistan 1999        745    19987071
## 2 Afghanistan 2000       2666    20595360
## 3 Brazil      1999      37737   172006362
## 4 Brazil      2000      80488   174504898
## 5 China       1999     212258  1272915272
## 6 China       2000     213766  1280428583