Capítulo 5 Limpieza de datos: Tidyr

En un análisis de datos, gran parte del tiempo que se utiliza es dando un grado de limpieza a estos, para así ya solo obtener información resumen, aplicar modelos o manipularlos para descubrir algo en ellos. De acuerdo a las fuentes que se consulten, esto puede tomar hasta un 80% del tiempo que se dispone, por lo que es evidente la necesidad de tener funciones que ayuden con dicha tarea.

La limpieza de datos varía dependiendo de los fines, pero para Hadley Wickham (Chief Scientist en RStudio y alguien importante en desarrollo y mantenimiento del tidyverse) tener una limpieza en los datos requiere de ciertas características. La información completa se puede encontrar en artículo de Hadley, Tidy Data, pero básicamente se tienen 3 puntos importantes para considerar que los datos son limpios.

  • Cada variable forma una columna.
  • Cada observación forma un renglón.
  • Cada valor debe tener su propia celda.

Las siguientes gráficas representan dichos puntos sobre un subconjunto de Iris.


Con lo anterior establecido y suponiendo que se esta analizando una pequeña base de datos correspondientes a las calificaciones de ciertos alumnos ¿Cuál de las siguientes estructuras es correcta?

  • Colocando las observaciones respecto a los nombres
Matemáticas Química
Juan 8 9
Carlos 9 7
Luis 7 8
Allison 9 9
Leticia 8 9
  • Colocando las observaciones respecto a la materia
Juan Carlos Luis Allison Leticia
Matemáticas 8 9 7 9 8
Química 9 7 8 9 9

De hecho, ninguna de las dos opciones anteriores es correcta. De acuerdo a los principios anteriores para tener datos limpios, cada variable debe formar una columna, lo cual no sucede en este caso, ya que las variables aquí son estudiante o el nombre, la materia y las calificaciones. Es decir, que las configuración correcta es la siguiente

Nombre Asignatura Calificaciones
Juan Matemáticas 8
Carlos Matemáticas 9
Luis Matemáticas 7
Allison Matemáticas 9
Leticia Matemáticas 8
Juan Química 9
Carlos Química 7
Luis Química 8
Allison Química 9
Leticia Química 9

Esto es uno de los tantos ejemplos que se pueden dar cuando se trabajando con datos. En el momento en que se identifican estos problemas, se deberá usar toda la creatividad para resolverlos y obtener una estructura con la que ya se pueda trabajar. El paquete tidyr ayudará con la mayoría de estos.

Como recomendación, se aconseja tener a la mano siempre la respectiva Cheat Sheet que pueda ser de utilidad, en este caso de la librería tidyr se pueden consultar Data import y Data Wrangling; las respectivas traducciones al español se pueden encontrar en la página oficial de Cheat Sheets de RStudio.

Como bien dice Hadley en Tidy Data, hay cinco problemas comunes en los messy datasets:

  • Los encabezados son valores y no nombres de dichas variables.
  • Múltiples variables están contenidas en una sola.
  • Existen variables que están almacenadas tanto en renglones como columnas.
  • Múltiples tipos de observaciones están en la misma tabla.
  • Una sola observación esta en múltiples tablas.

En la sección anterior se presentaron los datos para la población por sexo y entidad federativa según grupos de edad quinquenales de acuerdo a los tabulados básicos de la CONAPO

colnames(Population_FEntity)[1:2] <- c("Entidad Federativa", "Población Total")
Population_FEntity
# A tibble: 33 x 16
   `Entidad Federa… `Población Tota… `0 a 4` `5 a 9` `10 a 14` `15 a 19`
   <chr>                       <dbl>   <dbl>   <dbl>     <dbl>     <dbl>
 1 República Mexic…        103498524    9.31   10.4      11.2      10.1 
 2 Aguascalientes            1066233   10.3    11.0      11.1      10.9 
 3 Baja California           2856361    9.41   10.3      11.1      10.7 
 4 Baja California…           512030    9.00    8.44     10.1      10.8 
 5 Campeche                   755703    8.34   10.4      12.7       9.52
 6 Coahuila                  2501413    9.78   10.1      10.6       9.96
 7 Colima                     568642    8.09    8.43     10.0      11.0 
 8 Chiapas                   4293414   11.4    11.8      13.2      11.0 
 9 Chihuahua                 3241513    7.83   10.2      11.0       9.49
10 Distrito Federal          8737172    7.82    8.82      8.09      8.19
# … with 23 more rows, and 10 more variables: `20 a 24` <dbl>, `25 a 29` <dbl>,
#   `30 a 34` <dbl>, `35 a 39` <dbl>, `40 a 44` <dbl>, `45 a 49` <dbl>, `50 a
#   54` <dbl>, `55 a 59` <dbl>, `60 y más` <dbl>, NE <dbl>

En las primeras dos columnas se cambiaron los nombres con las herramientas que hasta este momento se tienen. Véase que las demás columnas representan rangos de edad, por lo que se tiene el primer problema en los messy datasets. De hecho, en estos datos las variables son la Entidad Federativa, la Población Total, el Rango de Edad y el Porcentaje de la población total en ese rango de edad.

En estos casos se dice los datos son anchos, y lo que se desea es que estos estén en un formato largo (ancho por la cantidad de columnas y largo por la cantidad de observaciones). Para tales casos se utiliza la función tidyr::gather() la cual recibe como parámetros múltiples columnas y colapsa la información de estas en dos variables.

(Population_FEntity <- gather(data = Population_FEntity, key = "Rango_edad", value = "Porcentaje", -c("Entidad Federativa", "Población Total")))
# A tibble: 462 x 4
   `Entidad Federativa` `Población Total` Rango_edad Porcentaje
   <chr>                            <dbl> <chr>           <dbl>
 1 República Mexicana           103498524 0 a 4            9.31
 2 Aguascalientes                 1066233 0 a 4           10.3 
 3 Baja California                2856361 0 a 4            9.41
 4 Baja California Sur             512030 0 a 4            9.00
 5 Campeche                        755703 0 a 4            8.34
 6 Coahuila                       2501413 0 a 4            9.78
 7 Colima                          568642 0 a 4            8.09
 8 Chiapas                        4293414 0 a 4           11.4 
 9 Chihuahua                      3241513 0 a 4            7.83
10 Distrito Federal               8737172 0 a 4            7.82
# … with 452 more rows

Con lo anterior se logro tener una mejor estructura en los datos, con lo cual fácilmente se podrían crear gráficas, modelos y estadísticas de resumen por Entidad Federativa o por rango de edad. Después de tener la información con una estructura adecuada, la imaginación es el único limitante. En el siguiente capítulo se verá como manipular internamente los datos para que se tenga una mejor semántica en ellos.

Si se desea por algún motivo, lo cual puede suceder, tener los datos en formato ancho, se puede usar la función complementaria la cual es tidyr::spread()

Population_FEntity %>% spread(key = "Rango_edad",value = "Porcentaje")
# A tibble: 33 x 16
   `Entidad Federa… `Población Tota… `0 a 4` `10 a 14` `15 a 19` `20 a 24`
   <chr>                       <dbl>   <dbl>     <dbl>     <dbl>     <dbl>
 1 Aguascalientes            1066233   10.3      11.1      10.9       8.27
 2 Baja California           2856361    9.41     11.1      10.7       8.43
 3 Baja California…           512030    9.00     10.1      10.8       8.75
 4 Campeche                   755703    8.34     12.7       9.52      9.23
 5 Chiapas                   4293414   11.4      13.2      11.0       9.16
 6 Chihuahua                 3241513    7.83     11.0       9.49      7.71
 7 Coahuila                  2501413    9.78     10.6       9.96      8.31
 8 Colima                     568642    8.09     10.0      11.0       9.29
 9 Distrito Federal          8737172    7.82      8.09      8.19      7.86
10 Durango                   1509025    9.97     11.8      10.3       8.11
# … with 23 more rows, and 10 more variables: `25 a 29` <dbl>, `30 a 34` <dbl>,
#   `35 a 39` <dbl>, `40 a 44` <dbl>, `45 a 49` <dbl>, `5 a 9` <dbl>, `50 a
#   54` <dbl>, `55 a 59` <dbl>, `60 y más` <dbl>, NE <dbl>

Finalmente:

  • Un equivalente de la función tidyr:: gather() la proporciona el paquete reshape con su función melt().
  • Para la función tidyr:: spread() se tiene la función reshape::dcast().
  • Depende de la versión que se utilice, las funciones gather() y spread() podrían haber sido remplazadas por las funciones tidyr::pivot_longer() y pivot_wider() respectivamente.
  • En otros lenguajes de programación, a estas técnicas se les conoce como pivoteo.

Para ver un ejemplo del segundo punto, supóngase que los datos vistos al inicio donde se relacionaban las calificaciones de ciertos alumnos tienen originalmente la siguiente estructura

# A tibble: 10 x 2
   Nombre  `A/C`
   <chr>   <chr>
 1 Juan    M8   
 2 Carlos  M9   
 3 Luis    M7   
 4 Allison M9   
 5 Leticia M8   
 6 Juan    Q9   
 7 Carlos  Q7   
 8 Luis    Q8   
 9 Allison Q9   
10 Leticia Q9   

En tal caso, la segunda variable contiene la información tanto de la materia como de la calificación. Aquí la función tidyr::separate() es de gran ayuda

(Student_grades <- Student_grades %>% separate(col = "A/C", into = c("Asignatura", "Calificaciones"), sep = 1))
# A tibble: 10 x 3
   Nombre  Asignatura Calificaciones
   <chr>   <chr>      <chr>         
 1 Juan    M          8             
 2 Carlos  M          9             
 3 Luis    M          7             
 4 Allison M          9             
 5 Leticia M          8             
 6 Juan    Q          9             
 7 Carlos  Q          7             
 8 Luis    Q          8             
 9 Allison Q          9             
10 Leticia Q          9             
  • En el argumento sep se da la posición dentro del texto para separar los datos.

En el caso en que se desee tener el caso inverso, se utiliza la función tidyr::unite()

Student_grades %>% unite("A/C", c("Asignatura", "Calificaciones"), sep = "")
# A tibble: 10 x 2
   Nombre  `A/C`
   <chr>   <chr>
 1 Juan    M8   
 2 Carlos  M9   
 3 Luis    M7   
 4 Allison M9   
 5 Leticia M8   
 6 Juan    Q9   
 7 Carlos  Q7   
 8 Luis    Q8   
 9 Allison Q9   
10 Leticia Q9   

Otro ejemplo lo proporciona la Cheat Sheet Data Wrangling.

# A tibble: 10,010 x 11
   name  date   hour   lat  long status category  wind pressure ts_diameter
   <chr> <chr> <dbl> <dbl> <dbl> <chr>  <ord>    <int>    <int>       <dbl>
 1 Amy   1975…     0  27.5 -79   tropi… -1          25     1013          NA
 2 Amy   1975…     6  28.5 -79   tropi… -1          25     1013          NA
 3 Amy   1975…    12  29.5 -79   tropi… -1          25     1013          NA
 4 Amy   1975…    18  30.5 -79   tropi… -1          25     1013          NA
 5 Amy   1975…     0  31.5 -78.8 tropi… -1          25     1012          NA
 6 Amy   1975…     6  32.4 -78.7 tropi… -1          25     1012          NA
 7 Amy   1975…    12  33.3 -78   tropi… -1          25     1011          NA
 8 Amy   1975…    18  34   -77   tropi… -1          30     1006          NA
 9 Amy   1975…     0  34.4 -75.8 tropi… 0           35     1004          NA
10 Amy   1975…     6  34   -74.8 tropi… 0           40     1002          NA
# … with 10,000 more rows, and 1 more variable: hu_diameter <dbl>
separate(storms, col = date ,into =  c("year", "month", "day"), sep = c(4, 5))
# A tibble: 10,010 x 13
   name  year  month day    hour   lat  long status category  wind pressure
   <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl> <chr>  <ord>    <int>    <int>
 1 Amy   1975  6     27        0  27.5 -79   tropi… -1          25     1013
 2 Amy   1975  6     27        6  28.5 -79   tropi… -1          25     1013
 3 Amy   1975  6     27       12  29.5 -79   tropi… -1          25     1013
 4 Amy   1975  6     27       18  30.5 -79   tropi… -1          25     1013
 5 Amy   1975  6     28        0  31.5 -78.8 tropi… -1          25     1012
 6 Amy   1975  6     28        6  32.4 -78.7 tropi… -1          25     1012
 7 Amy   1975  6     28       12  33.3 -78   tropi… -1          25     1011
 8 Amy   1975  6     28       18  34   -77   tropi… -1          30     1006
 9 Amy   1975  6     29        0  34.4 -75.8 tropi… 0           35     1004
10 Amy   1975  6     29        6  34   -74.8 tropi… 0           40     1002
# … with 10,000 more rows, and 2 more variables: ts_diameter <dbl>,
#   hu_diameter <dbl>
  • Se tiene un equivalente para separar en renglones: tidyr::separate_rows()
  • Un equivalente la función separate() esta dada por reshape::colsplit().

En el caso que se tenga el problema donde algunas variables esten almacenadas tanto en renglones como columnas se debe tratar el problema como lo anterior visto, primero abstraerse al problema de juntar las columnas necesarias en dos variables con la función gather() para posteriormente separar las variables que lo requieran con la función separate().

Para los últimos dos casos se necesita hacer uso de unas funciones del paquete dplyr, el cual ser verá en la siguiente sección. Por el momento, se pueden ver otras funciones útiles de tidyr, como aquellas que ayudan en la obtención de diferentes operaciones de conjuntos en los datos.

df <- data_frame(x = 1:2, y = 2:1)
tidyr::expand_grid(df, z = 1:3)
# A tibble: 6 x 3
      x     y     z
  <int> <int> <int>
1     1     2     1
2     1     2     2
3     1     2     3
4     2     1     1
5     2     1     2
6     2     1     3

La función tidyr::expand_grid() crea un tibble de todas las combinaciones de sus inputs, estos pueden ser data frames o tibbles, matrices y hasta vectores. Se puede entender a esta función como el producto cartesiano de dos conjuntos.

x <- data_frame(x = letters[5:7])
y <- data_frame(c(3,3,1))
crossing(x, y)
# A tibble: 6 x 2
  x     `c(3, 3, 1)`
  <chr>        <dbl>
1 e                1
2 e                3
3 f                1
4 f                3
5 g                1
6 g                3

La función tidyr::crossing() regresa un tibble y tiene un comportamiento similar a expand_grid() con la diferencia de eliminar registros duplicados.

set.seed(20)
x <- data_frame(sample(letters[1:3], size = 5, replace = T))
y <- data_frame(y = sample(1:3, size = 5, replace = T))
tidyr::nesting(x, y)
# A tibble: 4 x 2
  `sample(letters[1:3], size = 5, replace = T)`     y
  <chr>                                         <int>
1 a                                                 1
2 b                                                 2
3 c                                                 1
4 c                                                 2

La función tidyr::nesting() encuentra las posibles combinaciones entre los datos, es decir, las combinaciones entre los datos de entrada si estos se unieran en uno solo.

set.seed(20)
reduced_iris <- iris %>% head()
df <- reduced_iris[sample(1:6, size = 10, replace = T),]
df %>% tidyr::expand(Petal.Length)
# A tibble: 3 x 1
  Petal.Length
         <dbl>
1          1.3
2          1.4
3          1.7

La función tidyr::expand() genera todas las combinaciones de variables que se encuentran en un data set.

  • ¿Qué sucede al ejecutar df %>% tidyr::expand(Sepal.Width, Petal.Width)?
  • ¿Y al ejecutar df %>% tidyr::expand(nesting(Sepal.Width, Petal.Width))?

Otras funciones útiles son las siguientes

  • tidyr::nest(). Dicha función anida un data frame o un subconjunto de este en listas y coloca estos como observaciones; esto puede ser muy útil en la aplicación de modelos.
  • tidyr::unnest(). Función inversa a tidyr::nest().
  • tidyr::replace_na(). Dicha función encuentra los valores perdidos en un data set y los cambia por un valor dado.

Ejercicios

  1. En la sección Limpieza de datos: Tidyr se vieron varios ejemplos donde se mencionaba que estos venían como sugerencias de las respectivas Cheat Sheets. Dichos datos fueron manipulados para ver el uso de las funciones que se estaban presentando. Obtén la estructura de los datos a los cuales se les aplicaron dichas funciones.

  2. Investigar otras funciones de tidyr como chop(), complete() y pack().