3.2 Datos limpios

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. Entonces, 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 datos limpios (Hadley Wickham 2014) proveen una manera estándar de organizar la información:

  1. Cada variable forma una columna.
  2. Cada observación forma un renglón.
  3. Cada tipo de unidad observacional forma una tabla.

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 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 bases de datos 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 bases 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 bases de datos 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:

  • gather: recibe múltiples columnas y las junta en pares de valores y nombres, convierte los datos anchos en largos.
  • spread: recibe 2 columnas y las 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 gather y spread) con el fin de estructurarla para que cumpla los principios de datos limpios.

Los encabezados de las columanas son valores

Usaremos ejemplos para entender los conceptos más facilmente. La primer base de datos está basada en una encuesta de Pew Research que investiga la relación entre ingreso y afiliación religiosa.

¿Cuáles son las variables en estos datos?

library(tidyverse)
pew <- read_delim("http://stat405.had.co.nz/data/pew.txt", "\t", 
  escape_double = FALSE, trim_ws = TRUE)
#> Parsed with column specification:
#> cols(
#>   religion = col_character(),
#>   `<$10k` = col_double(),
#>   `$10-20k` = col_double(),
#>   `$20-30k` = col_double(),
#>   `$30-40k` = col_double(),
#>   `$40-50k` = col_double(),
#>   `$50-75k` = col_double(),
#>   `$75-100k` = col_double(),
#>   `$100-150k` = col_double(),
#>   `>150k` = col_double(),
#>   `Don't know/refused` = col_double()
#> )
pew
#> # A tibble: 9 x 11
#>   religion `<$10k` `$10-20k` `$20-30k` `$30-40k` `$40-50k` `$50-75k`
#>   <chr>      <dbl>     <dbl>     <dbl>     <dbl>     <dbl>     <dbl>
#> 1 Jewish        19        19        25        25        30        95
#> 2 Mainlin…     289       495       619       655       651      1107
#> 3 Mormon        29        40        48        51        56       112
#> 4 Muslim         6         7         9        10         9        23
#> 5 Orthodox      13        17        23        32        32        47
#> 6 Other C…       9         7        11        13        13        14
#> 7 Other F…      20        33        40        46        49        63
#> 8 Other W…       5         2         3         4         2         7
#> 9 Unaffil…     217       299       374       365       341       528
#> # ... with 4 more variables: `$75-100k` <dbl>, `$100-150k` <dbl>,
#> #   `>150k` <dbl>, `Don't know/refused` <dbl>

Esta base de datos tiene 3 variables: religión, ingreso y frecuencia. Para limpiarla es necesario apilar las columnas (alargar los datos). Notemos que al alargar los datos desapareceran las columnas que se agrupan y dan lugar a dos nuevas columnas: la correspondiente a clave y la correspondiente a valor. Entonces, para alargar una base de datos usamos la función gather que recibe los argumentos:

  • data: base de datos que vamos a reestructurar.
  • key: nombre de la nueva variable que contiene lo que fueron los nombres de columnas que apilamos.
  • value: nombre de la variable que almacenará los valores que corresponden a cada key.
  • …: lo último que especificamos son las columnas que vamos a apilar, la notación para seleccionarlas es la misma que usamos con select().
pew_tidy <- gather(data = pew, income, frequency, -religion)
pew_tidy
#> # A tibble: 90 x 3
#>    religion              income  frequency
#>    <chr>                 <chr>       <dbl>
#>  1 Jewish                <$10k          19
#>  2 Mainline Prot         <$10k         289
#>  3 Mormon                <$10k          29
#>  4 Muslim                <$10k           6
#>  5 Orthodox              <$10k          13
#>  6 Other Christian       <$10k           9
#>  7 Other Faiths          <$10k          20
#>  8 Other World Religions <$10k           5
#>  9 Unaffiliated          <$10k         217
#> 10 Jewish                $10-20k        19
#> # ... with 80 more rows

Observemos que en la tabla ancha teníamos bajo la columna <$10k, en el renglón correspondiente a Agnostic un valor de 27, y podemos ver que este valor en la tabla larga se almacena bajo la columna frecuencia y corresponde a religión Agnostic, income <$10k. También es importante ver que en este ejemplo especificamos las columnas a apilar identificando la que no vamos a alargar con un signo negativo: es decir apila todas las columnas menos religión.

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.

Nota: En esta sección no explicaremos las funciones de graficación pues estas se cubren en las notas introductorias a R. En esta parte nos queremos concentrar en como limpiar datos y ejemplificar lo sencillo que es trabajar con datos limpios, esto es, una vez que los datos fueron reestructurados es fácil construir gráficas y resúmenes.

ggplot(pew_tidy, aes(x = income, y = frequency, color = religion, group = religion)) +
  geom_line() + 
  theme(axis.text.x = element_text(angle = 90, hjust = 1))

Podemos hacer gráficas más interesantes si creamos nuevas variables:

by_religion <- group_by(pew_tidy, religion)
pew_tidy_2 <- pew_tidy %>%
  filter(income != "Don't know/refused") %>%
  group_by(religion) %>%
  mutate(percent = frequency / sum(frequency)) %>% 
  filter(sum(frequency) > 1000)

head(pew_tidy_2)
#> # A tibble: 6 x 4
#> # Groups:   religion [2]
#>   religion      income  frequency percent
#>   <chr>         <chr>       <dbl>   <dbl>
#> 1 Mainline Prot <$10k         289  0.0471
#> 2 Unaffiliated  <$10k         217  0.0698
#> 3 Mainline Prot $10-20k       495  0.0806
#> 4 Unaffiliated  $10-20k       299  0.0961
#> 5 Mainline Prot $20-30k       619  0.101 
#> 6 Unaffiliated  $20-30k       374  0.120
income_levels <- unique(pew_tidy$income)[1:9]
ggplot(pew_tidy_2, aes(x = income, y = percent, group = religion)) +
  facet_wrap(~ religion, nrow = 1) +
  geom_bar(stat = "identity", fill = "darkgray") + 
  theme(axis.text.x = element_text(angle = 90, hjust = 1)) +
    scale_x_discrete(limits = income_levels)

En el código de arriba utilizamos las funciones group_by, filter y mutate que estudiaremos más adelante. Por ahora concentremonos en gather y spread.

Otro ejemplo, veamos los datos de Billboard, aquí se registra la fecha en la que una canción entra por primera vez al top 100 de Billboard.

billboard <- read_csv("data/billboard.csv")
#> Parsed with column specification:
#> cols(
#>   .default = col_double(),
#>   artist = col_character(),
#>   track = col_character(),
#>   time = col_time(format = ""),
#>   date.entered = col_date(format = ""),
#>   wk66 = col_logical(),
#>   wk67 = col_logical(),
#>   wk68 = col_logical(),
#>   wk69 = col_logical(),
#>   wk70 = col_logical(),
#>   wk71 = col_logical(),
#>   wk72 = col_logical(),
#>   wk73 = col_logical(),
#>   wk74 = col_logical(),
#>   wk75 = col_logical(),
#>   wk76 = col_logical()
#> )
#> See spec(...) for full column specifications.
billboard
#> # A tibble: 317 x 81
#>     year artist track time  date.entered   wk1   wk2   wk3   wk4   wk5
#>    <dbl> <chr>  <chr> <tim> <date>       <dbl> <dbl> <dbl> <dbl> <dbl>
#>  1  2000 2 Pac  Baby… 04:22 2000-02-26      87    82    72    77    87
#>  2  2000 2Ge+h… The … 03:15 2000-09-02      91    87    92    NA    NA
#>  3  2000 3 Doo… Kryp… 03:53 2000-04-08      81    70    68    67    66
#>  4  2000 3 Doo… Loser 04:24 2000-10-21      76    76    72    69    67
#>  5  2000 504 B… Wobb… 03:35 2000-04-15      57    34    25    17    17
#>  6  2000 98^0   Give… 03:24 2000-08-19      51    39    34    26    26
#>  7  2000 A*Tee… Danc… 03:44 2000-07-08      97    97    96    95   100
#>  8  2000 Aaliy… I Do… 04:15 2000-01-29      84    62    51    41    38
#>  9  2000 Aaliy… Try … 04:03 2000-03-18      59    53    38    28    21
#> 10  2000 Adams… Open… 05:30 2000-08-26      76    76    74    69    68
#> # ... with 307 more rows, and 71 more variables: wk6 <dbl>, wk7 <dbl>,
#> #   wk8 <dbl>, wk9 <dbl>, wk10 <dbl>, wk11 <dbl>, wk12 <dbl>, wk13 <dbl>,
#> #   wk14 <dbl>, wk15 <dbl>, wk16 <dbl>, wk17 <dbl>, wk18 <dbl>,
#> #   wk19 <dbl>, wk20 <dbl>, wk21 <dbl>, wk22 <dbl>, wk23 <dbl>,
#> #   wk24 <dbl>, wk25 <dbl>, wk26 <dbl>, wk27 <dbl>, wk28 <dbl>,
#> #   wk29 <dbl>, wk30 <dbl>, wk31 <dbl>, wk32 <dbl>, wk33 <dbl>,
#> #   wk34 <dbl>, wk35 <dbl>, wk36 <dbl>, wk37 <dbl>, wk38 <dbl>,
#> #   wk39 <dbl>, wk40 <dbl>, wk41 <dbl>, wk42 <dbl>, wk43 <dbl>,
#> #   wk44 <dbl>, wk45 <dbl>, wk46 <dbl>, wk47 <dbl>, wk48 <dbl>,
#> #   wk49 <dbl>, wk50 <dbl>, wk51 <dbl>, wk52 <dbl>, wk53 <dbl>,
#> #   wk54 <dbl>, wk55 <dbl>, wk56 <dbl>, wk57 <dbl>, wk58 <dbl>,
#> #   wk59 <dbl>, wk60 <dbl>, wk61 <dbl>, wk62 <dbl>, wk63 <dbl>,
#> #   wk64 <dbl>, wk65 <dbl>, wk66 <lgl>, wk67 <lgl>, wk68 <lgl>,
#> #   wk69 <lgl>, wk70 <lgl>, wk71 <lgl>, wk72 <lgl>, wk73 <lgl>,
#> #   wk74 <lgl>, wk75 <lgl>, wk76 <lgl>

Notemos que el rank en cada semana (una vez que entró a la lista) está guardado en 75 columnas wk1 a wk75, este tipo de almacenamiento no es limpio pero puede ser útil al momento de ingresar la información.

Para tener datos limpios apilamos las semanas de manera que sea una sola columna (nuevamente alargamos los datos):

billboard_long <- gather(billboard, week, rank, wk1:wk76, na.rm = TRUE)
billboard_long
#> # A tibble: 5,307 x 7
#>     year artist        track                time  date.entered week   rank
#>  * <dbl> <chr>         <chr>                <tim> <date>       <chr> <dbl>
#>  1  2000 2 Pac         Baby Don't Cry (Kee… 04:22 2000-02-26   wk1      87
#>  2  2000 2Ge+her       The Hardest Part Of… 03:15 2000-09-02   wk1      91
#>  3  2000 3 Doors Down  Kryptonite           03:53 2000-04-08   wk1      81
#>  4  2000 3 Doors Down  Loser                04:24 2000-10-21   wk1      76
#>  5  2000 504 Boyz      Wobble Wobble        03:35 2000-04-15   wk1      57
#>  6  2000 98^0          Give Me Just One Ni… 03:24 2000-08-19   wk1      51
#>  7  2000 A*Teens       Dancing Queen        03:44 2000-07-08   wk1      97
#>  8  2000 Aaliyah       I Don't Wanna        04:15 2000-01-29   wk1      84
#>  9  2000 Aaliyah       Try Again            04:03 2000-03-18   wk1      59
#> 10  2000 Adams, Yolan… Open My Heart        05:30 2000-08-26   wk1      76
#> # ... with 5,297 more rows

Notemos que en esta ocasión especificamos las columnas que vamos a apilar indicando el nombre de la primera de ellas seguido de : y por último el nombre de la última variable a apilar. Por otra parte, la instrucción na.rm = TRUE se utiliza para eliminar los renglones con valores faltantes en la columna de value (rank), esto es, eliminamos aquellas observaciones que tenían NA en la columnas wknum de la tabla ancha. Ahora realizamos una limpieza adicional creando mejores variables de fecha.

billboard_tidy <- billboard_long %>%
  mutate(
    week = parse_number(week),
    date = date.entered + 7 * (week - 1), 
    rank = as.numeric(rank)
    ) %>%
    select(-date.entered)
billboard_tidy
#> # A tibble: 5,307 x 7
#>     year artist         track                 time   week  rank date      
#>    <dbl> <chr>          <chr>                 <tim> <dbl> <dbl> <date>    
#>  1  2000 2 Pac          Baby Don't Cry (Keep… 04:22     1    87 2000-02-26
#>  2  2000 2Ge+her        The Hardest Part Of … 03:15     1    91 2000-09-02
#>  3  2000 3 Doors Down   Kryptonite            03:53     1    81 2000-04-08
#>  4  2000 3 Doors Down   Loser                 04:24     1    76 2000-10-21
#>  5  2000 504 Boyz       Wobble Wobble         03:35     1    57 2000-04-15
#>  6  2000 98^0           Give Me Just One Nig… 03:24     1    51 2000-08-19
#>  7  2000 A*Teens        Dancing Queen         03:44     1    97 2000-07-08
#>  8  2000 Aaliyah        I Don't Wanna         04:15     1    84 2000-01-29
#>  9  2000 Aaliyah        Try Again             04:03     1    59 2000-03-18
#> 10  2000 Adams, Yolanda Open My Heart         05:30     1    76 2000-08-26
#> # ... with 5,297 more rows

Nuevamente, podemos hacer gráficas facilmente.

tracks <- filter(billboard_tidy, track %in% 
    c("Higher", "Amazed", "Kryptonite", "Breathe", "With Arms Wide Open"))

ggplot(tracks, aes(x = date, y = rank)) +
  geom_line() + 
  facet_wrap(~track, nrow = 1) + 
  theme(axis.text.x = element_text(angle = 90, hjust = 1))

Una columna asociada a más de una variable

La siguiente base de datos proviene de la Organización Mundial de la Salud y contiene el número de casos confirmados de tuberculosis por país y año, la información esta por grupo demográfico de acuerdo a sexo (m, f), y edad (0-4, 5-14, etc). Los datos están disponibles en http://www.who.int/tb/country/data/download/en/.

tb <- read.csv("data/tb.csv") %>% tbl_df()
tb
#> # A tibble: 5,769 x 22
#>    iso2   year new_sp_m04 new_sp_m514 new_sp_m014 new_sp_m1524 new_sp_m2534
#>  * <fct> <int>      <int>       <int>       <int>        <int>        <int>
#>  1 AD     1989         NA          NA          NA           NA           NA
#>  2 AD     1990         NA          NA          NA           NA           NA
#>  3 AD     1991         NA          NA          NA           NA           NA
#>  4 AD     1992         NA          NA          NA           NA           NA
#>  5 AD     1993         NA          NA          NA           NA           NA
#>  6 AD     1994         NA          NA          NA           NA           NA
#>  7 AD     1996         NA          NA           0            0            0
#>  8 AD     1997         NA          NA           0            0            1
#>  9 AD     1998         NA          NA           0            0            0
#> 10 AD     1999         NA          NA           0            0            0
#> # ... with 5,759 more rows, and 15 more variables: new_sp_m3544 <int>,
#> #   new_sp_m4554 <int>, new_sp_m5564 <int>, new_sp_m65 <int>,
#> #   new_sp_mu <int>, new_sp_f04 <int>, new_sp_f514 <int>,
#> #   new_sp_f014 <int>, new_sp_f1524 <int>, new_sp_f2534 <int>,
#> #   new_sp_f3544 <int>, new_sp_f4554 <int>, new_sp_f5564 <int>,
#> #   new_sp_f65 <int>, new_sp_fu <int>

De manera similar a los ejemplos anteriores, utiliza la función gather para apilar las columnas correspondientes a sexo-edad.

            Piensa en como podemos separar la “variable” sexo-edad en dos columnas.

Ahora separaremos las variables sexo y edad de la columna demo, 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,…).

tb_tidy <- separate(tb_long, demo, c("sex", "age"), 8)
tb_tidy
#> # A tibble: 35,750 x 5
#>    iso2   year sex      age       n
#>  * <fct> <int> <chr>    <chr> <int>
#>  1 AD     2005 new_sp_m 04        0
#>  2 AD     2006 new_sp_m 04        0
#>  3 AD     2008 new_sp_m 04        0
#>  4 AE     2006 new_sp_m 04        0
#>  5 AE     2007 new_sp_m 04        0
#>  6 AE     2008 new_sp_m 04        0
#>  7 AG     2007 new_sp_m 04        0
#>  8 AL     2005 new_sp_m 04        0
#>  9 AL     2006 new_sp_m 04        1
#> 10 AL     2007 new_sp_m 04        0
#> # ... with 35,740 more rows
table(tb_tidy$sex)
#> 
#> new_sp_f new_sp_m 
#>    17830    17920

# creamos un mejor código de genero
tb_tidy <- mutate(tb_tidy, sex = substr(sex, 8, 8))
table(tb_tidy$sex)
#> 
#>     f     m 
#> 17830 17920

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 clima en Cuernavaca. ¿Cuáles son las variables en estos datos?

clima <- read_delim("data/clima.txt", "\t", escape_double = FALSE, 
    trim_ws = TRUE)
#> Parsed with column specification:
#> cols(
#>   .default = col_double(),
#>   id = col_character(),
#>   element = col_character(),
#>   d9 = col_logical(),
#>   d12 = col_logical(),
#>   d18 = col_logical(),
#>   d19 = col_logical(),
#>   d20 = col_logical(),
#>   d21 = col_logical(),
#>   d22 = col_logical(),
#>   d24 = col_logical()
#> )
#> See spec(...) for full column specifications.

Estos datos tienen variables en columnas individuales (id, año, mes), en múltiples columnas (día, d1-d31) y en filas (tmin, tmax). Comencemos por apilar las columnas.

clima_long <- gather(clima, day, value, d1:d31, na.rm = TRUE)
clima_long
#> # A tibble: 66 x 6
#>    id           year month element day   value
#>  * <chr>       <dbl> <dbl> <chr>   <chr> <dbl>
#>  1 MX000017004  2010    12 TMAX    d1      299
#>  2 MX000017004  2010    12 TMIN    d1      138
#>  3 MX000017004  2010     2 TMAX    d2      273
#>  4 MX000017004  2010     2 TMIN    d2      144
#>  5 MX000017004  2010    11 TMAX    d2      313
#>  6 MX000017004  2010    11 TMIN    d2      163
#>  7 MX000017004  2010     2 TMAX    d3      241
#>  8 MX000017004  2010     2 TMIN    d3      144
#>  9 MX000017004  2010     7 TMAX    d3      286
#> 10 MX000017004  2010     7 TMIN    d3      175
#> # ... with 56 more rows

Podemos crear algunas variables adicionales.

clima_vars <- clima_long %>% 
  mutate(day = parse_number(day), 
    value = as.numeric(value) / 10) %>%
  select(id, year, month, day, element, value) %>%
  arrange(id, year, month, day)
clima_vars
#> # A tibble: 66 x 6
#>    id           year month   day element value
#>    <chr>       <dbl> <dbl> <dbl> <chr>   <dbl>
#>  1 MX000017004  2010     1    30 TMAX     27.8
#>  2 MX000017004  2010     1    30 TMIN     14.5
#>  3 MX000017004  2010     2     2 TMAX     27.3
#>  4 MX000017004  2010     2     2 TMIN     14.4
#>  5 MX000017004  2010     2     3 TMAX     24.1
#>  6 MX000017004  2010     2     3 TMIN     14.4
#>  7 MX000017004  2010     2    11 TMAX     29.7
#>  8 MX000017004  2010     2    11 TMIN     13.4
#>  9 MX000017004  2010     2    23 TMAX     29.9
#> 10 MX000017004  2010     2    23 TMIN     10.7
#> # ... with 56 more rows

Finalmente, la columna element no es una variable, sino que almacena el nombre de dos variables, la operación que debemos aplicar (spread) es el inverso de apilar (gather):

clima_tidy <- spread(clima_vars, element, value)
clima_tidy
#> # A tibble: 33 x 6
#>    id           year month   day  TMAX  TMIN
#>    <chr>       <dbl> <dbl> <dbl> <dbl> <dbl>
#>  1 MX000017004  2010     1    30  27.8  14.5
#>  2 MX000017004  2010     2     2  27.3  14.4
#>  3 MX000017004  2010     2     3  24.1  14.4
#>  4 MX000017004  2010     2    11  29.7  13.4
#>  5 MX000017004  2010     2    23  29.9  10.7
#>  6 MX000017004  2010     3     5  32.1  14.2
#>  7 MX000017004  2010     3    10  34.5  16.8
#>  8 MX000017004  2010     3    16  31.1  17.6
#>  9 MX000017004  2010     4    27  36.3  16.7
#> 10 MX000017004  2010     5    27  33.2  18.2
#> # ... with 23 more rows

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
clima_lm <- lm(TMAX ~ factor(month), data = clima_tidy)
summary(clima_lm)
#> 
#> Call:
#> lm(formula = TMAX ~ factor(month), data = clima_tidy)
#> 
#> Residuals:
#>    Min     1Q Median     3Q    Max 
#>  -3.65  -0.92  -0.02   1.05   3.18 
#> 
#> Coefficients:
#>                 Estimate Std. Error t value Pr(>|t|)    
#> (Intercept)      27.8000     1.8610  14.938 5.34e-13 ***
#> factor(month)2   -0.0500     2.0807  -0.024  0.98104    
#> factor(month)3    4.7667     2.1489   2.218  0.03717 *  
#> factor(month)4    8.5000     2.6319   3.230  0.00385 ** 
#> factor(month)5    5.4000     2.6319   2.052  0.05228 .  
#> factor(month)6    1.2500     2.2793   0.548  0.58892    
#> factor(month)7    1.4500     2.2793   0.636  0.53123    
#> factor(month)8    0.4714     1.9895   0.237  0.81488    
#> factor(month)10   1.1000     2.0386   0.540  0.59491    
#> factor(month)11   0.3200     2.0386   0.157  0.87670    
#> factor(month)12   1.0500     2.2793   0.461  0.64955    
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 1.861 on 22 degrees of freedom
#> Multiple R-squared:  0.6182, Adjusted R-squared:  0.4447 
#> F-statistic: 3.563 on 10 and 22 DF,  p-value: 0.006196

Mas de un tipo de observación en una misma tabla

En ocasiones las bases de datos involucran valores en diferentes niveles, en diferentes tipos de unidad observacional. En la limpieza de datos, cada unidad observacional debe estar almacenada en su propia tabla (esto esta ligado a normalización de una base de datos), es importante para evitar inconsistencias en los datos.

¿Cuáles son las unidades observacionales de los datos de billboard?

billboard_tidy
#> # A tibble: 5,307 x 7
#>     year artist         track                 time   week  rank date      
#>    <dbl> <chr>          <chr>                 <tim> <dbl> <dbl> <date>    
#>  1  2000 2 Pac          Baby Don't Cry (Keep… 04:22     1    87 2000-02-26
#>  2  2000 2Ge+her        The Hardest Part Of … 03:15     1    91 2000-09-02
#>  3  2000 3 Doors Down   Kryptonite            03:53     1    81 2000-04-08
#>  4  2000 3 Doors Down   Loser                 04:24     1    76 2000-10-21
#>  5  2000 504 Boyz       Wobble Wobble         03:35     1    57 2000-04-15
#>  6  2000 98^0           Give Me Just One Nig… 03:24     1    51 2000-08-19
#>  7  2000 A*Teens        Dancing Queen         03:44     1    97 2000-07-08
#>  8  2000 Aaliyah        I Don't Wanna         04:15     1    84 2000-01-29
#>  9  2000 Aaliyah        Try Again             04:03     1    59 2000-03-18
#> 10  2000 Adams, Yolanda Open My Heart         05:30     1    76 2000-08-26
#> # ... with 5,297 more rows

Separemos esta base de datos en dos: la tabla canción que almacena artista, nombre de la canción y duración; la tabla rank que almacena el ranking de la canción en cada semana.

song <- billboard_tidy %>% 
  select(artist, track, year, time) %>%
  unique() %>%
  arrange(artist) %>%
  mutate(song_id = row_number(artist))
song
#> # A tibble: 317 x 5
#>    artist         track                    year time   song_id
#>    <chr>          <chr>                   <dbl> <time>   <int>
#>  1 2 Pac          Baby Don't Cry (Keep...  2000 04:22        1
#>  2 2Ge+her        The Hardest Part Of ...  2000 03:15        2
#>  3 3 Doors Down   Kryptonite               2000 03:53        3
#>  4 3 Doors Down   Loser                    2000 04:24        4
#>  5 504 Boyz       Wobble Wobble            2000 03:35        5
#>  6 98^0           Give Me Just One Nig...  2000 03:24        6
#>  7 A*Teens        Dancing Queen            2000 03:44        7
#>  8 Aaliyah        I Don't Wanna            2000 04:15        8
#>  9 Aaliyah        Try Again                2000 04:03        9
#> 10 Adams, Yolanda Open My Heart            2000 05:30       10
#> # ... with 307 more rows

rank <- billboard_tidy %>%
  left_join(song, c("artist", "track", "year", "time")) %>%
  select(song_id, date, week, rank) %>%
  arrange(song_id, date) %>%
  tbl_df
rank
#> # A tibble: 5,307 x 4
#>    song_id date        week  rank
#>      <int> <date>     <dbl> <dbl>
#>  1       1 2000-02-26     1    87
#>  2       1 2000-03-04     2    82
#>  3       1 2000-03-11     3    72
#>  4       1 2000-03-18     4    77
#>  5       1 2000-03-25     5    87
#>  6       1 2000-04-01     6    94
#>  7       1 2000-04-08     7    99
#>  8       2 2000-09-02     1    91
#>  9       2 2000-09-09     2    87
#> 10       2 2000-09-16     3    92
#> # ... with 5,297 more rows

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. Leemos los archivos en una lista de tablas.
  2. Para cada tabla agregamos una columna que registra el nombre del archivo original.
  3. Combinamos las tablas en un solo data frame.

Veamos un ejemplo, descarga la carpeta specdata, ésta contiene 332 archivos csv que almacenan información de monitoreo de contaminación en 332 ubicaciones de EUA. Cada archivo contiene información de una unidad de monitoreo y el número de identificación del monitor es el nombre del archivo.

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”.

paths <- dir("data/specdata", pattern = "\\.csv$", full.names = TRUE) 

Después le asignamos el nombre del csv 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.

paths <- set_names(paths, basename(paths))

La función map_df itera sobre cada dirección, lee el csv en dicha dirección y los combina en un data frame.

specdata_us <- map_df(paths, ~read_csv(., col_types = "Tddi"), .id = "filename")

# eliminamos la basura del id
specdata <- specdata_us %>%
  mutate(monitor = parse_number(filename)) %>%
  select(id = ID, monitor, date = Date, sulfate, nitrate)
specdata
#> # A tibble: 772,087 x 5
#>       id monitor date                sulfate nitrate
#>    <int>   <dbl> <dttm>                <dbl>   <dbl>
#>  1     1       1 2003-01-01 00:00:00      NA      NA
#>  2     1       1 2003-01-02 00:00:00      NA      NA
#>  3     1       1 2003-01-03 00:00:00      NA      NA
#>  4     1       1 2003-01-04 00:00:00      NA      NA
#>  5     1       1 2003-01-05 00:00:00      NA      NA
#>  6     1       1 2003-01-06 00:00:00      NA      NA
#>  7     1       1 2003-01-07 00:00:00      NA      NA
#>  8     1       1 2003-01-08 00:00:00      NA      NA
#>  9     1       1 2003-01-09 00:00:00      NA      NA
#> 10     1       1 2003-01-10 00:00:00      NA      NA
#> # ... with 772,077 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

Referencias

Wickham, Hadley. 2014. “Tidy Data.” Journal of Statistical Software, Articles 59 (10):1–23. https://doi.org/10.18637/jss.v059.i10.