Sesión 6 Datos limpios
Es sabido que limpieza y preparación de datos ocupan gran parte del tiempo del análisis de datos (Dasu y Johnson, 2003 y NYT’s ‘Janitor Work’ Is Key Hurdle to Insights), es por ello que vale la pena dedicar un tiempo a aprender técnicas que faciliten estas tareas, y entender que estructura en los datos es más conveniente para trabajar.
Es así, que una vez que importamos datos a R es conveniente limpiarlos, esto implica almacenarlos de una manera consisistente que nos permita enfocarnos en responder preguntas de los datos en lugar de estar luchando con los datos.
Datos limpios son datos que facilitan las tareas del análisis de datos:
Visualización: Resúmenes de datos usando gráficas, análisis exploratorio, o presentación de resultados.
Manipulación: Manipulación de variables como agregar, filtrar, reordenar, transformar.
Modelación: Ajustar modelos es sencillo si los datos están en la forma correcta.
Los principios de los datos limpios (Hadley Wickham 2014) proveen una manera estándar de organizar la información:
- Cada columna es una variable.
- Cada renglón es una observación .
- Cada celda es un único valor.
Vale la pena notar que los principios de los datos limpios se pueden ver como teoría de algebra relacional para estadísticos, estós principios junto con cada tipo de unidad observacional forma una tabla equivalen a la tercera forma normal de Codd con enfoque en una sola tabla de datos en lugar de muchas conectadas en bases de datos relacionales.
Veamos un ejemplo:
La mayor parte de las tablas en estadística tienen forma rectangular, ¿cuántas variables tiene la siguiente tabla?
tratamientoA | tratamientoB | |
---|---|---|
Juan Aguirre | - | 2 |
Ana Bernal | 16 | 11 |
José López | 3 | 1 |
La tabla anterior también se puede estructurar de la siguiente manera:
Juan Aguirre | Ana Bernal | José López | |
---|---|---|---|
tratamientoA | - | 16 | 3 |
tratamientoB | 2 | 11 | 1 |
Si vemos los principios (cada variable forma una columna, cada observación forma un renglón, cada tipo de unidad observacional forma una tabla), ¿las tablas anteriores cumplen los principios?
Para responder la pregunta identifiquemos primero cuáles son las variables y cuáles las observaciones de esta pequeña base. Las variables son: persona/nombre, tratamiento y resultado. Entonces, siguiendo los principios de datos limpios obtenemos la siguiente estructura:
nombre | tratamiento | resultado |
---|---|---|
Juan Aguirre | a | - |
Ana Bernal | a | 16 |
José López | a | 3 |
Juan Aguirre | b | 2 |
Ana Bernal | b | 11 |
José López | b | 1 |
Limpieza de datos
Los principios de los datos limpios parecen obvios pero la mayor parte de los datos no los cumplen debido a:
- La mayor parte de la gente no está familiarizada con los principios y es
difícil derivarlos por uno mismo.
- Los datos suelen estar organizados para facilitar otros aspectos que no son análisis, por ejemplo, la captura.
Algunos de los problemas más comunes en las tablas que no están limpias son:
- Los encabezados de las columnas son valores y no nombres de variables.
- Más de una variable por columna.
- Las variables están organizadas tanto en filas como en columnas.
- Más de un tipo de observación en una tabla.
- Una misma unidad observacional está almacenada en múltiples tablas.
La mayor parte de estos problemas se pueden arreglar con pocas herramientas,
a continuación veremos como limpiar datos usando 2 funciones del paquete
tidyr
:
pivot_longer()
: recibe múltiples columnas y las convierte en pares de valores y nombres de tal manera que alarga los datos.
pivot_wider()
: el opuesto apivot_longer()
recibe columnas que separa haciendo los datos más anchos.
Repasaremos los problemas más comunes que se encuentran en conjuntos de datos sucios y mostraremos como se puede manipular la tabla de datos (usando las funciones de pivoteo) con el fin de estructurarla para que cumpla los principios de datos limpios.
Nota: Quizá has visto código de tidyr
usando las funciones gather()
y
spread()
, estas son versiones anteriores a las funciones de pivoteo, sin
embargo, se les seguirá dando mantenimiento puesto que son muy populares, aquí puedes encontrar
una versión de las notas usando que utilizan gather()
y spread()
.
Los encabezados de las columanas son valores
Usaremos ejemplos para entender los conceptos más facilmente. Comenzaremos con una tabla de datos que contiene las mediciones de partículas suspendidas PM2.5 de la red automática de monitoreo atmosférico (RAMA) para los primeros meses del 2019.
library(tidyverse)
library(estcomp)
pm25_2019#> # A tibble: 5,088 × 26
#> date hour AJM AJU BJU CAM CCA COY FAR GAM HGM INN
#> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <lgl> <lgl> <dbl> <dbl> <dbl>
#> 1 2019-01-01 1 19 35 62 90 66 NA NA NA 56 22
#> 2 2019-01-01 2 17 24 88 104 84 NA NA NA 61 14
#> 3 2019-01-01 3 14 20 107 140 95 NA NA NA 74 8
#> 4 2019-01-01 4 6 15 101 162 97 NA NA NA 90 9
#> 5 2019-01-01 5 4 8 121 133 88 NA NA NA 90 3
#> 6 2019-01-01 6 7 7 93 106 77 NA NA NA 106 3
#> 7 2019-01-01 7 12 8 84 98 51 NA NA NA 115 NA
#> 8 2019-01-01 8 15 7 101 82 39 NA NA NA 90 NA
#> 9 2019-01-01 9 24 3 89 54 26 NA NA NA 90 NA
#> 10 2019-01-01 10 24 NA 88 76 26 NA NA NA 99 2
#> # … with 5,078 more rows, and 14 more variables: MER <dbl>, MGH <dbl>,
#> # MON <dbl>, MPA <lgl>, NEZ <dbl>, PED <dbl>, SAC <lgl>, SAG <dbl>,
#> # SFE <dbl>, SJA <lgl>, TLA <dbl>, UAX <dbl>, UIZ <dbl>, XAL <dbl>
#> # ℹ Use `print(n = ...)` to see more rows, and `colnames()` to see all variable names
¿Cuáles son las variables en estos datos?
Esta base de datos tiene 4 variables: fecha, hora, estación y medición (en microgramos por metro cúbico \(\mu g/m^3\)).
Al alargar los datos desaparecerán las columnas que se agrupan y darán
lugar a dos nuevas columnas: la correspondiente a estación y la
correspondiente a medición. Entonces, usamos la función pivot_longer()
que
recibe los argumentos:
- data:
data.frame
que vamos a pivotear, alargar.
- cols: columnas que vamos a pivotear (apilar), la
notación para seleccionarlas es
tidyselect
, la misma que usamos conselect()
endplyr
. - names_to: nombre (
string
: en comillas ““) de la nueva columna que almacenará los nombres de las columnas en los datos.
- values_to: nombre (
string
: en comillas ““) de la nueva columna que almacenará los valores en los datos.
<- pivot_longer(pm25_2019, cols = AJM:XAL, names_to = "station",
pm25_2019_tidy values_to = "measurement")
pm25_2019_tidy#> # A tibble: 122,112 × 4
#> date hour station measurement
#> <date> <dbl> <chr> <dbl>
#> 1 2019-01-01 1 AJM 19
#> 2 2019-01-01 1 AJU 35
#> 3 2019-01-01 1 BJU 62
#> 4 2019-01-01 1 CAM 90
#> 5 2019-01-01 1 CCA 66
#> 6 2019-01-01 1 COY NA
#> 7 2019-01-01 1 FAR NA
#> 8 2019-01-01 1 GAM NA
#> 9 2019-01-01 1 HGM 56
#> 10 2019-01-01 1 INN 22
#> # … with 122,102 more rows
#> # ℹ Use `print(n = ...)` to see more rows
Observemos que en la tabla original teníamos bajo la columna AJM, en el renglón correspondiente a 2019-01-01 hora 1 un valor de 19, y podemos ver que este valor en la tabla larga se almacena bajo la columna measurement y corresponde a la estación AJM.
La nueva estructura de la base de datos nos permite, por ejemplo, hacer fácilmente una gráfica donde podemos comparar las diferencias en las frecuencias.
|>
pm25_2019_tidy mutate(
missing = is.na(measurement),
station = reorder(station, missing, sum)
|>
) ggplot(aes(x = date, y = hour, fill = is.na(measurement))) +
geom_raster(alpha = 0.8) +
facet_wrap(~ station) +
scale_fill_manual("faltante",
values = c("TRUE" = "salmon", "FALSE" = "gray"))
Otro ejemplo, veamos los datos df_edu
, ¿cuántas variables tenemos?
df_edu#> # A tibble: 7,371 × 16
#> state_c…¹ munic…² region state…³ state…⁴ munic…⁵ sex pop_15 no_sc…⁶ presc…⁷
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 01 001 01001 Aguasc… AGS Aguasc… Total 631064 2.66 0.173
#> 2 01 001 01001 Aguasc… AGS Aguasc… Homb… 301714 2.36 0.175
#> 3 01 001 01001 Aguasc… AGS Aguasc… Muje… 329350 2.94 0.172
#> 4 01 002 01002 Aguasc… AGS Asient… Total 31013 4.01 0.258
#> 5 01 002 01002 Aguasc… AGS Asient… Homb… 14991 4.39 0.294
#> 6 01 002 01002 Aguasc… AGS Asient… Muje… 16022 3.66 0.225
#> 7 01 003 01003 Aguasc… AGS Calvil… Total 38678 6.36 0.165
#> 8 01 003 01003 Aguasc… AGS Calvil… Homb… 18499 7.06 0.184
#> 9 01 003 01003 Aguasc… AGS Calvil… Muje… 20179 5.71 0.149
#> 10 01 004 01004 Aguasc… AGS Cosío Total 10478 3.68 0.439
#> # … with 7,361 more rows, 6 more variables: elementary <dbl>, secondary <dbl>,
#> # highschool <dbl>, higher_edu <dbl>, other <dbl>, schoolyrs <dbl>, and
#> # abbreviated variable names ¹state_code, ²municipio_code, ³state_name,
#> # ⁴state_abbr, ⁵municipio_name, ⁶no_school, ⁷preschool
#> # ℹ Use `print(n = ...)` to see more rows, and `colnames()` to see all variable names
Notemos que el nivel de escolaridad esta guardado en 6 columnas (preschool, elementary, …, other), este tipo de almacenamiento no es limpio aunque puede ser útil al momento de ingresar la información o para presentarla.
Para tener datos limpios apilamos los niveles de escolaridad de manera que sea una sola columna (nuevamente alargamos los datos):
<- pivot_longer(data = df_edu, cols = preschool:other,
df_edu_tidy names_to = "grade", values_to = "percent", values_drop_na = TRUE)
glimpse(df_edu_tidy)
#> Rows: 44,226
#> Columns: 12
#> $ state_code <chr> "01", "01", "01", "01", "01", "01", "01", "01", "01", "…
#> $ municipio_code <chr> "001", "001", "001", "001", "001", "001", "001", "001",…
#> $ region <chr> "01001", "01001", "01001", "01001", "01001", "01001", "…
#> $ state_name <chr> "Aguascalientes", "Aguascalientes", "Aguascalientes", "…
#> $ state_abbr <chr> "AGS", "AGS", "AGS", "AGS", "AGS", "AGS", "AGS", "AGS",…
#> $ municipio_name <chr> "Aguascalientes", "Aguascalientes", "Aguascalientes", "…
#> $ sex <chr> "Total", "Total", "Total", "Total", "Total", "Total", "…
#> $ pop_15 <dbl> 631064, 631064, 631064, 631064, 631064, 631064, 301714,…
#> $ no_school <dbl> 2.662329, 2.662329, 2.662329, 2.662329, 2.662329, 2.662…
#> $ schoolyrs <dbl> 10.211152, 10.211152, 10.211152, 10.211152, 10.211152, …
#> $ grade <chr> "preschool", "elementary", "secondary", "highschool", "…
#> $ percent <dbl> 0.17335801, 20.15247265, 29.31144860, 23.31823714, 24.2…
El parámetro values_drop_na = TRUE
se utiliza para eliminar los
renglones con valores faltantes en la columna de porcentaje, esto es, eliminamos
aquellas observaciones que tenían NA
en la columnas de nivel de escolaridad de
la tabla ancha. En este caso optamos por que los faltantes sean implícitos, la
conveniencia de tenerlos implícitos/explícitos dependerá de la aplicación.
Con los datos limpios es facil hacer manipulaciones y grfiacs, ¿cómo habrían hecho la siguiente gráfica antes de la limpieza?
<- df_edu_tidy |>
df_edu_cdmx filter(state_abbr == "CDMX", sex != "Total", grade != "other") |>
mutate(municipio_name = reorder(municipio_name, percent, last))
ggplot(df_edu_cdmx, aes(x = grade,
y = percent, group = sex, color = sex)) +
geom_path() +
facet_wrap(~municipio_name) +
theme(axis.text.x = element_text(angle = 60, hjust = 1)) +
scale_x_discrete(limits = c("preschool", "elementary",
"secondary", "highschool", "higher_edu"))
Una columna asociada a más de una variable
Utilizaremos un subconjunto de los datos de la prueba ENLACE a nivel primaria, la prueba ENLACE evaluaba a todos los alumnos de tercero a sexto de primaria y a los alumnos de secundaria del país en 3 áreas: español, matemáticas y formación cívica y ética.
data("enlacep_2013")
<- enlacep_2013 |>
enlacep_sub_2013 select(CVE_ENT:PUNT_FCE_6) |>
sample_n(1000)
glimpse(enlacep_sub_2013)
#> Rows: 1,000
#> Columns: 22
#> $ CVE_ENT <chr> "30", "26", "07", "13", "21", "10", "07", "32", "08", "30",…
#> $ NOM_ENT <chr> "VERACRUZ", "SONORA", "CHIAPAS", "HIDALGO", "PUEBLA", "DURA…
#> $ CCT <chr> "30DPB0754N", "26DPR1428N", "07KPR1987T", "13DPR1278T", "21…
#> $ TURNO <chr> "MATUTINO", "VESPERTINO", "MATUTINO", "MATUTINO", "MATUTINO…
#> $ ESCUELA <chr> "16 DE SEPTIEMBRE", "GUADALUPE FERNANDEZ DE COLOSIO", "PROG…
#> $ TIPO <chr> "INDêGENA", "GENERAL", "CONAFE", "GENERAL", "CONAFE", "GENE…
#> $ CVE_MUN <chr> "201", "036", "053", "001", "089", "012", "005", "048", "00…
#> $ NOM_MUN <chr> "ZONGOLICA", "MAGDALENA", "MAZAPA DE MADERO", "ACATLAN", "J…
#> $ CVE_LOC <chr> "0001", "0001", "0006", "0013", "0036", "0026", "0154", "00…
#> $ NOM_LOC <chr> "ZONGOLICA", "MAGDALENA DE KINO", "PROGRESO", "LOMA LARGA",…
#> $ PUNT_ESP_3 <dbl> 541, 596, NA, 491, 598, 445, NA, 670, 615, 487, 607, 651, 5…
#> $ PUNT_MAT_3 <dbl> 586, 625, NA, 554, 584, 441, NA, 725, 655, 465, 655, 655, 5…
#> $ PUNT_FCE_3 <dbl> 501, 538, NA, 477, 399, 378, NA, 618, 542, 405, 519, 588, 5…
#> $ PUNT_ESP_4 <dbl> 482, 696, 454, 548, 371, 463, 455, 599, 594, 417, 490, 595,…
#> $ PUNT_MAT_4 <dbl> 531, 717, 463, 600, 400, 468, 432, 746, 686, 418, 504, 606,…
#> $ PUNT_FCE_4 <dbl> 444, 621, 499, 513, 393, 422, 470, 546, 559, 414, 434, 609,…
#> $ PUNT_ESP_5 <dbl> 493, 511, 524, 606, 452, 389, 443, 539, 510, 447, 541, 504,…
#> $ PUNT_MAT_5 <dbl> 498, 549, 525, 626, 602, 402, 536, 573, 570, 464, 542, 601,…
#> $ PUNT_FCE_5 <dbl> 469, 519, 481, 496, 406, 415, 442, 512, 539, 436, 482, 425,…
#> $ PUNT_ESP_6 <dbl> 481, 579, NA, 531, 462, 399, NA, NA, 542, 415, 461, 525, NA…
#> $ PUNT_MAT_6 <dbl> 492, 659, NA, 611, 573, 414, NA, NA, 581, 460, 550, 592, NA…
#> $ PUNT_FCE_6 <dbl> 460, 512, NA, 469, 485, 362, NA, NA, 511, 376, 422, 471, NA…
¿Cuántas variables tiene este subconjunto de los datos?
De manera similar a los ejemplos anteriores, utiliza la función
pivot_longer
para apilar las columnas correspondientes a área-grado.Piensa en como podemos separar la “variable” área-grado en dos columnas.
Ahora separaremos las variables área y grado de la columna AREA_GRADO
,
para ello debemos pasar a la función separate()
, esta recibe como parámetros:
el nombre de la base de datos,
el nombre de la variable que deseamos separar en más de una,
la posición de donde deseamos “cortar” (hay más opciones para especificar como separar, ver
?separate
). El default es separar valores en todos los lugares que encuentre un caracter que no es alfanumérico (espacio, guión,…).
<- separate(data = enlacep_long, col = AREA_GRADO,
enlacep_tidy into = c("AREA", "GRADO"), sep = 9)
enlacep_tidy#> # A tibble: 12,000 × 13
#> CVE_ENT NOM_ENT CCT TURNO ESCUELA TIPO CVE_MUN NOM_MUN CVE_LOC NOM_LOC
#> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 30 VERACRUZ 30DPB07… MATU… 16 DE … INDê… 201 ZONGOL… 0001 ZONGOL…
#> 2 30 VERACRUZ 30DPB07… MATU… 16 DE … INDê… 201 ZONGOL… 0001 ZONGOL…
#> 3 30 VERACRUZ 30DPB07… MATU… 16 DE … INDê… 201 ZONGOL… 0001 ZONGOL…
#> 4 30 VERACRUZ 30DPB07… MATU… 16 DE … INDê… 201 ZONGOL… 0001 ZONGOL…
#> 5 30 VERACRUZ 30DPB07… MATU… 16 DE … INDê… 201 ZONGOL… 0001 ZONGOL…
#> 6 30 VERACRUZ 30DPB07… MATU… 16 DE … INDê… 201 ZONGOL… 0001 ZONGOL…
#> 7 30 VERACRUZ 30DPB07… MATU… 16 DE … INDê… 201 ZONGOL… 0001 ZONGOL…
#> 8 30 VERACRUZ 30DPB07… MATU… 16 DE … INDê… 201 ZONGOL… 0001 ZONGOL…
#> 9 30 VERACRUZ 30DPB07… MATU… 16 DE … INDê… 201 ZONGOL… 0001 ZONGOL…
#> 10 30 VERACRUZ 30DPB07… MATU… 16 DE … INDê… 201 ZONGOL… 0001 ZONGOL…
#> # … with 11,990 more rows, and 3 more variables: AREA <chr>, GRADO <chr>,
#> # PUNTAJE <dbl>
#> # ℹ Use `print(n = ...)` to see more rows, and `colnames()` to see all variable names
# creamos un mejor código de área
<- enlacep_tidy |>
enlacep_tidy mutate(
AREA = substr(AREA, 6, 8),
GRADO = as.numeric(GRADO)
) glimpse(enlacep_tidy)
#> Rows: 12,000
#> Columns: 13
#> $ CVE_ENT <chr> "30", "30", "30", "30", "30", "30", "30", "30", "30", "30", "3…
#> $ NOM_ENT <chr> "VERACRUZ", "VERACRUZ", "VERACRUZ", "VERACRUZ", "VERACRUZ", "V…
#> $ CCT <chr> "30DPB0754N", "30DPB0754N", "30DPB0754N", "30DPB0754N", "30DPB…
#> $ TURNO <chr> "MATUTINO", "MATUTINO", "MATUTINO", "MATUTINO", "MATUTINO", "M…
#> $ ESCUELA <chr> "16 DE SEPTIEMBRE", "16 DE SEPTIEMBRE", "16 DE SEPTIEMBRE", "1…
#> $ TIPO <chr> "INDêGENA", "INDêGENA", "INDêGENA", "INDêGENA", "INDêGENA", "I…
#> $ CVE_MUN <chr> "201", "201", "201", "201", "201", "201", "201", "201", "201",…
#> $ NOM_MUN <chr> "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOLICA…
#> $ CVE_LOC <chr> "0001", "0001", "0001", "0001", "0001", "0001", "0001", "0001"…
#> $ NOM_LOC <chr> "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOLICA…
#> $ AREA <chr> "ESP", "MAT", "FCE", "ESP", "MAT", "FCE", "ESP", "MAT", "FCE",…
#> $ GRADO <dbl> 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 3, 3, 3, 4, 4, 4, 5, 5, 5,…
#> $ PUNTAJE <dbl> 541, 586, 501, 482, 531, 444, 493, 498, 469, 481, 492, 460, 59…
Conforme nos habituemos a las funciones podemos sacar provecho de sus argumentos adicionales:
names_prefix
: recibe una expresión regular para eliminar el texto que coincida del inicio de una variable.
pivot_longer(enlacep_sub_2013, cols = contains("PUNT"),
names_to = c("AREA_GRADO"), values_to = "PUNTAJE",
names_prefix = "PUNT_") |>
glimpse()
#> Rows: 12,000
#> Columns: 12
#> $ CVE_ENT <chr> "30", "30", "30", "30", "30", "30", "30", "30", "30", "30",…
#> $ NOM_ENT <chr> "VERACRUZ", "VERACRUZ", "VERACRUZ", "VERACRUZ", "VERACRUZ",…
#> $ CCT <chr> "30DPB0754N", "30DPB0754N", "30DPB0754N", "30DPB0754N", "30…
#> $ TURNO <chr> "MATUTINO", "MATUTINO", "MATUTINO", "MATUTINO", "MATUTINO",…
#> $ ESCUELA <chr> "16 DE SEPTIEMBRE", "16 DE SEPTIEMBRE", "16 DE SEPTIEMBRE",…
#> $ TIPO <chr> "INDêGENA", "INDêGENA", "INDêGENA", "INDêGENA", "INDêGENA",…
#> $ CVE_MUN <chr> "201", "201", "201", "201", "201", "201", "201", "201", "20…
#> $ NOM_MUN <chr> "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOL…
#> $ CVE_LOC <chr> "0001", "0001", "0001", "0001", "0001", "0001", "0001", "00…
#> $ NOM_LOC <chr> "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOL…
#> $ AREA_GRADO <chr> "ESP_3", "MAT_3", "FCE_3", "ESP_4", "MAT_4", "FCE_4", "ESP_…
#> $ PUNTAJE <dbl> 541, 586, 501, 482, 531, 444, 493, 498, 469, 481, 492, 460,…
names_sep
: nos permite hacer el pivoteo y separar en una misma operación, en este casonames_to
consiste en un vector con más de una entrada ynames_sep
indica como separar el nombre de las columnas.
pivot_longer(enlacep_sub_2013, cols = contains("PUNT"),
names_to = c("AREA", "GRADO"), values_to = "PUNTAJE", names_prefix = "PUNT_",
names_sep = "_") |>
glimpse()
#> Rows: 12,000
#> Columns: 13
#> $ CVE_ENT <chr> "30", "30", "30", "30", "30", "30", "30", "30", "30", "30", "3…
#> $ NOM_ENT <chr> "VERACRUZ", "VERACRUZ", "VERACRUZ", "VERACRUZ", "VERACRUZ", "V…
#> $ CCT <chr> "30DPB0754N", "30DPB0754N", "30DPB0754N", "30DPB0754N", "30DPB…
#> $ TURNO <chr> "MATUTINO", "MATUTINO", "MATUTINO", "MATUTINO", "MATUTINO", "M…
#> $ ESCUELA <chr> "16 DE SEPTIEMBRE", "16 DE SEPTIEMBRE", "16 DE SEPTIEMBRE", "1…
#> $ TIPO <chr> "INDêGENA", "INDêGENA", "INDêGENA", "INDêGENA", "INDêGENA", "I…
#> $ CVE_MUN <chr> "201", "201", "201", "201", "201", "201", "201", "201", "201",…
#> $ NOM_MUN <chr> "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOLICA…
#> $ CVE_LOC <chr> "0001", "0001", "0001", "0001", "0001", "0001", "0001", "0001"…
#> $ NOM_LOC <chr> "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOLICA", "ZONGOLICA…
#> $ AREA <chr> "ESP", "MAT", "FCE", "ESP", "MAT", "FCE", "ESP", "MAT", "FCE",…
#> $ GRADO <chr> "3", "3", "3", "4", "4", "4", "5", "5", "5", "6", "6", "6", "3…
#> $ PUNTAJE <dbl> 541, 586, 501, 482, 531, 444, 493, 498, 469, 481, 492, 460, 59…
names_pattern
: similar anames_sep
pero recibe una expresión regular.names_ptypes, values_ptypes
: permiten especificar el tipo de las nuevas columnas.
pivot_longer(enlacep_sub_2013, cols = contains("PUNT"),
names_to = c("AREA", "GRADO"), values_to = "PUNTAJE", names_prefix = "PUNT_",
names_sep = "_", names_ptypes = list(GRADO = integer())) |>
glimpse()
Variables almacenadas en filas y columnas
El problema más difícil es cuando las variables están tanto en filas como en columnas, veamos una base de datos de fertilidad. ¿Cuáles son las variables en estos datos?
data("df_fertility")
df_fertility#> # A tibble: 306 × 11
#> state size_…¹ est age_1…² age_2…³ age_2…⁴ age_3…⁵ age_3…⁶ age_4…⁷ age_4…⁸
#> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 01 Agu… Menos … Valor 74.2 175. 175. 102. 59.0 23.0 4.49
#> 2 01 Agu… Menos … Erro… 6.71 11.0 9.35 8.05 7.29 6.30 1.91
#> 3 01 Agu… 2 500-… Valor 82.5 171. 140. 103. 72.0 15.2 3.52
#> 4 01 Agu… 2 500-… Erro… 9.79 12.5 10.4 8.76 9.08 3.81 2.44
#> 5 01 Agu… 15 000… Valor 72.6 146. 147. 99.0 58.7 10.2 0.819
#> 6 01 Agu… 15 000… Erro… 7.07 10.8 10.5 8.11 7.37 2.87 0.821
#> 7 01 Agu… 100 00… Valor 66.3 120. 102. 84.2 53.9 10.6 5.35
#> 8 01 Agu… 100 00… Erro… 7.57 8.66 8.98 8.59 6.61 3.57 2.56
#> 9 02 Baj… Menos … Valor 89.6 158. 117. 86.0 42.0 8.02 0
#> 10 02 Baj… Menos … Erro… 15.8 17.2 13.2 12.3 9.64 4.72 0
#> # … with 296 more rows, 1 more variable: global <dbl>, and abbreviated variable
#> # names ¹size_localidad, ²age_15_19, ³age_20_24, ⁴age_25_29, ⁵age_30_34,
#> # ⁶age_35_39, ⁷age_40_44, ⁸age_45_49
#> # ℹ Use `print(n = ...)` to see more rows, and `colnames()` to see all variable names
Estos datos tienen variables en columnas individuales (state, size_localidad), en múltiples columnas (grupo de edad, age_15_19,..) y en filas (Valor y Error estándar).
Comencemos por apilar las columnas.
<- pivot_longer(df_fertility, cols = age_15_19:global,
fertility_long names_to = "age_bracket", values_to = "value", names_prefix = "age_")
fertility_long#> # A tibble: 2,448 × 5
#> state size_localidad est age_bracket value
#> <chr> <chr> <chr> <chr> <dbl>
#> 1 01 Aguascalientes Menos de 2 500 habitantes Valor 15_19 74.2
#> 2 01 Aguascalientes Menos de 2 500 habitantes Valor 20_24 175.
#> 3 01 Aguascalientes Menos de 2 500 habitantes Valor 25_29 175.
#> 4 01 Aguascalientes Menos de 2 500 habitantes Valor 30_34 102.
#> 5 01 Aguascalientes Menos de 2 500 habitantes Valor 35_39 59.0
#> 6 01 Aguascalientes Menos de 2 500 habitantes Valor 40_44 23.0
#> 7 01 Aguascalientes Menos de 2 500 habitantes Valor 45_49 4.49
#> 8 01 Aguascalientes Menos de 2 500 habitantes Valor global 3.06
#> 9 01 Aguascalientes Menos de 2 500 habitantes Error estándar 15_19 6.71
#> 10 01 Aguascalientes Menos de 2 500 habitantes Error estándar 20_24 11.0
#> # … with 2,438 more rows
#> # ℹ Use `print(n = ...)` to see more rows
Podemos crear algunas variables adicionales.
<- fertility_long |>
fertility_vars mutate(
state_code = str_sub(state, 1, 2),
state_name = str_sub(state, 4)
|>
) select(-state)
fertility_vars#> # A tibble: 2,448 × 6
#> size_localidad est age_bracket value state_c…¹ state…²
#> <chr> <chr> <chr> <dbl> <chr> <chr>
#> 1 Menos de 2 500 habitantes Valor 15_19 74.2 01 Aguasc…
#> 2 Menos de 2 500 habitantes Valor 20_24 175. 01 Aguasc…
#> 3 Menos de 2 500 habitantes Valor 25_29 175. 01 Aguasc…
#> 4 Menos de 2 500 habitantes Valor 30_34 102. 01 Aguasc…
#> 5 Menos de 2 500 habitantes Valor 35_39 59.0 01 Aguasc…
#> 6 Menos de 2 500 habitantes Valor 40_44 23.0 01 Aguasc…
#> 7 Menos de 2 500 habitantes Valor 45_49 4.49 01 Aguasc…
#> 8 Menos de 2 500 habitantes Valor global 3.06 01 Aguasc…
#> 9 Menos de 2 500 habitantes Error estándar 15_19 6.71 01 Aguasc…
#> 10 Menos de 2 500 habitantes Error estándar 20_24 11.0 01 Aguasc…
#> # … with 2,438 more rows, and abbreviated variable names ¹state_code,
#> # ²state_name
#> # ℹ Use `print(n = ...)` to see more rows
Finalmente, la columna est no es una variable, sino que almacena el nombre
de 2 variables: Valor y Error Estándar la operación que debemos aplicar
(pivot_wider()
) es el inverso de apilar (pivot_longer
), sus argumentos son:
- data:
data.frame
que vamos a pivotear.
- names_from: nombre o nombres de las columnas (sin comillas) de los cuáles obtendremos los nombres de las nuevas columnas.
- values_from: nombre o nombres de las columnas (sin comillas) de los cuáles obtendremos los valores que llenarán las nuevas columnas.
<- pivot_wider(fertility_vars, names_from = est,
fertility_tidy values_from = value)
Y podemos mejorar los nombres de las columnas, una opción rápida es usar el paquete janitor.
library(janitor)
|>
fertility_tidy clean_names() |>
glimpse()
#> Rows: 1,224
#> Columns: 6
#> $ size_localidad <chr> "Menos de 2 500 habitantes", "Menos de 2 500 habitantes…
#> $ age_bracket <chr> "15_19", "20_24", "25_29", "30_34", "35_39", "40_44", "…
#> $ state_code <chr> "01", "01", "01", "01", "01", "01", "01", "01", "01", "…
#> $ state_name <chr> "Aguascalientes", "Aguascalientes", "Aguascalientes", "…
#> $ valor <dbl> 74.2032276, 175.0281396, 174.5274362, 101.5836230, 58.9…
#> $ error_estandar <dbl> 6.70671255, 11.00329648, 9.34594033, 8.04764573, 7.2860…
o podemos hacerlo manualmente
names(fertility_tidy)[5:6] <- c("est", "std_error")
Ahora es inmediato no solo hacer gráficas sino también ajustar un modelo.
# ajustamos un modelo lineal donde la variable respuesta es temperatura
# máxima, y la variable explicativa es el mes
<- filter(fertility_tidy, age_bracket != "global")
fertility_sub <- lm(est ~ age_bracket, data = fertility_sub)
fertility_lm summary(fertility_lm)
#>
#> Call:
#> lm(formula = est ~ age_bracket, data = fertility_sub)
#>
#> Residuals:
#> Min 1Q Median 3Q Max
#> -55.060 -5.778 -0.383 6.874 55.133
#>
#> Coefficients:
#> Estimate Std. Error t value Pr(>|t|)
#> (Intercept) 66.505 1.143 58.21 <2e-16 ***
#> age_bracket20_24 70.675 1.616 43.74 <2e-16 ***
#> age_bracket25_29 58.881 1.616 36.44 <2e-16 ***
#> age_bracket30_34 22.910 1.616 14.18 <2e-16 ***
#> age_bracket35_39 -20.312 1.616 -12.57 <2e-16 ***
#> age_bracket40_44 -53.346 1.616 -33.01 <2e-16 ***
#> age_bracket45_49 -64.797 1.616 -40.10 <2e-16 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Residual standard error: 14.13 on 1064 degrees of freedom
#> Multiple R-squared: 0.922, Adjusted R-squared: 0.9215
#> F-statistic: 2096 on 6 and 1064 DF, p-value: < 2.2e-16
Vale la pena notar que aunque los datos limpios facilitan las tareas de análisis, distintas funciones o tareas requieren los datos en distintos formas y saber pivotear las tablas es muy útil.
Grafica el valor estimado de fertilidad del grupo de edad 20-24 contra 25-29. ¿Qué transformación debes hacer? Tip: elimina la columna que corresponde al error estándar antes de ensanchar los datos.
Una misma unidad observacional está almacenada en múltiples tablas
También es común que los valores sobre una misma unidad observacional estén separados en muchas tablas o archivos, es común que estas tablas esten divididas de acuerdo a una variable, de tal manera que cada archivo representa a una persona, año o ubicación. Para juntar los archivos hacemos lo siguiente:
- Enlistamos las rutas de los archivos.
- Leemos cada archivo y agregamos una columna con el nombre del archivo.
- Combinamos las tablas en un solo data frame.
Veamos un ejemplo, descargamos la carpeta con los datos de varios contaminantes de RAMA,
::use_zip("https://github.com/tereom/estcomp/raw/master/data-raw/19RAMA.zip",
usethis"data")
ésta contiene 9 archivos de excel que almacenan información de monitoreo de contaminantes. Cada archivo contiene información de un contaminante y el nombre del archivo indica el contaminante.
Los pasos en R (usando el paquete purrr
), primero creamos un vector con los
nombres de los archivos en un directorio, eligiendo aquellos que contengan las
letras “.csv”.
<- dir("data/19RAMA", pattern = "\\.xls$", full.names = TRUE) paths
Después le asignamos el nombre del archivo al nombre de cada elemento del vector. Este paso se realiza para preservar los nombres de los archivos ya que estos los asignaremos a una variable mas adelante.
<- set_names(paths, basename(paths)) paths
La función map_df
itera sobre cada dirección, lee el archivo excel de dicha
dirección y los combina en un data frame.
library(readxl)
<- map_df(paths, read_excel, .id = "FILENAME")
rama
# eliminamos la basura del id
<- rama |>
rama mutate(PARAMETRO = str_remove(FILENAME, "2019") |> str_remove(".xls")) |>
select(PARAMETRO, FECHA:AJU)
# y apilamos para tener una columna por estación
<- rama |>
rama_tidy pivot_longer(names_to = "estacion", values_to = "valor", ACO:AJU) |>
mutate(valor = ifelse(-99, NA, valor))
rama_tidy#> # A tibble: 1,648,512 × 5
#> PARAMETRO FECHA HORA estacion valor
#> <chr> <dttm> <dbl> <chr> <lgl>
#> 1 CO 2019-01-01 00:00:00 1 ACO NA
#> 2 CO 2019-01-01 00:00:00 1 AJM NA
#> 3 CO 2019-01-01 00:00:00 1 ATI NA
#> 4 CO 2019-01-01 00:00:00 1 BJU NA
#> 5 CO 2019-01-01 00:00:00 1 CAM NA
#> 6 CO 2019-01-01 00:00:00 1 CCA NA
#> 7 CO 2019-01-01 00:00:00 1 CHO NA
#> 8 CO 2019-01-01 00:00:00 1 CUA NA
#> 9 CO 2019-01-01 00:00:00 1 FAC NA
#> 10 CO 2019-01-01 00:00:00 1 FAR NA
#> # … with 1,648,502 more rows
#> # ℹ Use `print(n = ...)` to see more rows
Otras consideraciones
En las buenas prácticas es importante tomar en cuenta los siguientes puntos:
Incluir un encabezado con el nombre de las variables.
Los nombres de las variables deben ser entendibles (e.g. AgeAtDiagnosis es mejor que AgeDx).
En general los datos se deben guardar en un archivo por tabla.
Escribir un script con las modificaciones que se hicieron a los datos crudos (reproducibilidad).
Otros aspectos importantes en la limpieza de datos son: selección del tipo de variables (por ejemplo fechas), datos faltantes, typos y detección de valores atípicos.
Recursos adicionales
Data Transformation Cheat Sheet de RStudio.
Data Wrangling Cheat Sheet de RStudio.
Limpiar nombres de columnas, eliminar filas vacías y más, paquete janitor.