-
Notifications
You must be signed in to change notification settings - Fork 3
/
06-datos-limpios.Rmd
538 lines (412 loc) · 18.2 KB
/
06-datos-limpios.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
# 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](http://onlinelibrary.wiley.com/book/10.1002/0471448354)
y [NYT's ‘Janitor Work’ Is Key Hurdle to Insights](https://www.nytimes.com/2014/08/18/technology/for-big-data-scientists-hurdle-to-insights-is-janitor-work.html?mcubz=0)),
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.
```{block2, type="caja"}
Los **principios de los datos limpios** [@tidy]
proveen una manera estándar de organizar la información:
1. Cada columna es una variable.
2. Cada renglón es una observación .
3. 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](https://en.wikipedia.org/wiki/Third_normal_form)
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?
<div class="mi-tabla">
| |tratamientoA|tratamientoB
----|------------|---------
Juan Aguirre|- |2
Ana Bernal |16 |11
José López |3 |1
</div>
La tabla anterior también se puede estructurar de la siguiente manera:
<div class="mi-tabla">
||Juan Aguirre| Ana Bernal|José López
--|------------|-----------|----------
tratamientoA|- | 16 | 3
tratamientoB|2 | 11 | 1
</div>
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:
<div class="mi-tabla">
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
</div>
## Limpieza de datos {-}
Los principios de los datos limpios parecen obvios pero la mayor parte de los
datos no los cumplen debido a:
1. La mayor parte de la gente no está familiarizada con los principios y es
difícil derivarlos por uno mismo.
2. 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 a `pivot_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í](https://tereom.github.io/tutoriales/datos_limpios.html) 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](http://www.aire.cdmx.gob.mx)) para los primeros meses del 2019.
```{r}
library(tidyverse)
library(estcomp)
pm25_2019
```
¿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$).
```{r, eval = FALSE, echo = FALSE}
pm25_2019_tidy <- gather(pm25_2019, key = station, value = measurement, -date,
-hour)
head(pm25_2019_tidy)
tail(pm25_2019_tidy)
```
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 con `select()`
en `dplyr`.
* 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.
```{r}
pm25_2019_tidy <- pivot_longer(pm25_2019, cols = AJM:XAL, names_to = "station",
values_to = "measurement")
pm25_2019_tidy
```
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.
```{r}
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?
```{r}
df_edu
```
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):
```{r, eval = FALSE, include = FALSE}
df_edu_tidy <- gather(data = df_edu, grade, percent, preschool:other,
na.rm = TRUE)
glimpse(df_edu_tidy)
```
```{r}
df_edu_tidy <- pivot_longer(data = df_edu, cols = preschool:other,
names_to = "grade", values_to = "percent", values_drop_na = TRUE)
glimpse(df_edu_tidy)
```
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?
```{r}
df_edu_cdmx <- df_edu_tidy |>
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](http://www.enlace.sep.gob.mx/ba/) 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*.
```{r}
data("enlacep_2013")
enlacep_sub_2013 <- enlacep_2013 |>
select(CVE_ENT:PUNT_FCE_6) |>
sample_n(1000)
glimpse(enlacep_sub_2013)
```
![](img/manicule2.jpg) ¿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.
```{r, echo=FALSE, eval=FALSE}
enlacep_long <- gather(enlacep_sub_2013, AREA_GRADO, PUNTAJE,
contains("PUNT"), na.rm = TRUE)
enlacep_long
```
```{r, echo=FALSE}
enlacep_long <- pivot_longer(enlacep_sub_2013, cols = contains("PUNT"),
names_to = c("AREA_GRADO"), values_to = "PUNTAJE")
```
* 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,...).
```{r}
enlacep_tidy <- separate(data = enlacep_long, col = AREA_GRADO,
into = c("AREA", "GRADO"), sep = 9)
enlacep_tidy
# creamos un mejor código de área
enlacep_tidy <- enlacep_tidy |>
mutate(
AREA = substr(AREA, 6, 8),
GRADO = as.numeric(GRADO)
)
glimpse(enlacep_tidy)
```
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.
```{r}
pivot_longer(enlacep_sub_2013, cols = contains("PUNT"),
names_to = c("AREA_GRADO"), values_to = "PUNTAJE",
names_prefix = "PUNT_") |>
glimpse()
```
* `names_sep`: nos permite hacer el pivoteo y separar en una misma operación,
en este caso `names_to` consiste en un vector con más de una entrada y
`names_sep` indica como separar el nombre de las columnas.
```{r}
pivot_longer(enlacep_sub_2013, cols = contains("PUNT"),
names_to = c("AREA", "GRADO"), values_to = "PUNTAJE", names_prefix = "PUNT_",
names_sep = "_") |>
glimpse()
```
* `names_pattern`: similar a `names_sep` pero recibe una expresión regular.
```{r, include=FALSE}
pivot_longer(enlacep_sub_2013, cols = contains("PUNT"),
names_to = c("AREA", "GRADO"), names_pattern = "PUNT_?(.*)_(.*)",
values_to = "PUNTAJE")
```
* `names_ptypes, values_ptypes`: permiten especificar el tipo de las nuevas
columnas.
```{r enlace_prueba, eval=FALSE}
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?
```{r}
data("df_fertility")
df_fertility
```
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.
```{r, eval = FALSE, include=FALSE}
fertility_long <- gather(df_fertility, age_bracket, value, age_15_19:global,
na.rm = TRUE)
fertility_long
```
```{r}
fertility_long <- pivot_longer(df_fertility, cols = age_15_19:global,
names_to = "age_bracket", values_to = "value", names_prefix = "age_")
fertility_long
```
Podemos crear algunas variables adicionales.
```{r}
fertility_vars <- fertility_long |>
mutate(
state_code = str_sub(state, 1, 2),
state_name = str_sub(state, 4)
) |>
select(-state)
fertility_vars
```
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.
```{r, include=FALSE}
fertility_tidy <- spread(data = fertility_vars, key = est, value = value)
```
```{r}
fertility_tidy <- pivot_wider(fertility_vars, names_from = est,
values_from = value)
```
Y podemos mejorar los nombres de las columnas, una opción rápida es usar el paquete janitor.
```{r, message=FALSE, warning=FALSE}
library(janitor)
fertility_tidy |>
clean_names() |>
glimpse()
```
o podemos hacerlo manualmente
```{r}
names(fertility_tidy)[5:6] <- c("est", "std_error")
```
Ahora es inmediato no solo hacer gráficas sino también ajustar un modelo.
```{r}
# ajustamos un modelo lineal donde la variable respuesta es temperatura
# máxima, y la variable explicativa es el mes
fertility_sub <- filter(fertility_tidy, age_bracket != "global")
fertility_lm <- lm(est ~ age_bracket, data = fertility_sub)
summary(fertility_lm)
```
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.
![](img/manicule2.jpg) 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.
```{r, include=FALSE}
fertility_age <- pivot_wider(fertility_tidy, names_from = age_bracket,
values_from = c(est, std_error))
ggplot(fertility_age, aes(est_20_24, est_25_29, color = size_localidad)) +
geom_abline(alpha = 0.5) +
geom_point()
```
### 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:
1. Enlistamos las rutas de los archivos.
2. Leemos cada archivo y agregamos una columna con el nombre del archivo.
3. Combinamos las tablas en un solo data frame.
Veamos un ejemplo, descargamos la carpeta con los datos de varios contaminantes
de RAMA,
```{r, eval=FALSE}
usethis::use_zip("https://github.com/tereom/estcomp/raw/master/data-raw/19RAMA.zip",
"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".
```{r, echo=FALSE, eval=FALSE}
library(here)
dir_rama <- here("data", "19RAMA")
paths <- dir(dir_rama, pattern = "\\.xls$", full.names = TRUE)
paths
```
```{r}
paths <- dir("data/19RAMA", pattern = "\\.xls$", full.names = TRUE)
```
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.
```{r}
paths <- set_names(paths, basename(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.
```{r, error=TRUE}
library(readxl)
rama <- map_df(paths, read_excel, .id = "FILENAME")
# 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_tidy <- rama |>
pivot_longer(names_to = "estacion", values_to = "valor", ACO:AJU) |>
mutate(valor = ifelse(-99, NA, valor))
rama_tidy
```
### 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](https://github.com/rstudio/cheatsheets/raw/master/data-transformation.pdf)
de RStudio.
* [Data Wrangling Cheat Sheet](https://github.com/rstudio/cheatsheets/raw/master/data-import.pdf)
de RStudio.
* Limpiar nombres de columnas, eliminar filas vacías y más, paquete
[janitor](https://github.com/sfirke/janitor).