8 Limpieza y manipulación de datos

8.1 Objetivos

En este capítulo veremos:

  • Que es la estrategia Divida-Aplique-Combine y como se utiliza
  • La diferencia entre los formatos de tablas anchas vs. largas y como convertir de un formato al otro
  • Como usar dplyr y tidyr para limpiar y manipular datos para análisis
  • Como unir múltiples data.frame usando dplyr

8.2 Introducción

Es muy raro que recibamos datos en un formato que nos permita trabajar o hacer los análisis, además es común que algunos paquetes de R requieran los datos en un formato, mientras otros paquetes los requieren en otro. Es por esto, que para hacer nuestro trabajo en forma eficiente, es necesario que podamos re-formatear los datos de acuerdo a las necesidades del trabajo a realizar. Los paquetes de R dplyr y tidyr entregan funciones muy completas, útiles y potentes para realizar estos cambios de formato.

Los análisis toman muchas formas, pero generalmente se pueden clasificar en lo que se conoce como la estrategia Divida-Aplique-Combine (Split-Apply-Combine). Esta estrategia sigue un set de pasos típicos:

  • Divida: Dividir los datos en grupos lógicos (ej. área, stock, año).
  • Aplique: Cálcular las estadísticas resúmenes para cada grupo (ej. promedio del largo total por año).
  • Combine: Combinar los grupos en una tabla única.
Diagrama de la estragegia divida, aplique y combine.

Figure 8.1: Diagrama de la estragegia divida, aplique y combine.

Como se muestra en la figura 8.1, la tabla original se dividió en grupos anuales (year) para calcular el largo promedio de cada grupo y posteriormente combinar en una tabla única que incluye los promedios anuales.

dplyr permite hacer eso en forma muy simple. Asumiendo que nuestros datos de largo están cargados en un data.frame llamado length_data, esto se puede hacer de la siguiente forma:

year length_cm
1991 5.673318
1991 3.081224
1991 4.592696
1992 4.381523
1992 5.597777
1992 4.900052
1992 4.139282
1992 5.422823
1992 5.905247
1992 5.098922

Es muy común también que se deba cambiar el formato de las tablas de datos (“reshape”). Veamos un ejemplo de una tabla en el formato, que de ahora en adelante llamaremos, “ancho”:

site 1990 1991 1993
gold 100 118 112
lake 100 118 112
dredge 100 118 112

Es muy probable que usted esté familiarizado con datos en este formato, donde los valores de las variables observadas están repartidos en más de una columna (en este caso, columnas para cada año). Otra forma de describir esto es que existe más de una medición por cada fila. Este formato funciona muy bien para la entrada de datos (en excel) y a veces para algunos análisis, pero se hace rápidamente inútil para usarlos en R. Por ejemplo, ¿cómo puede ajustar un modelo con el año como una variable predictora? en un mundo ideal deberíamos poder ejecutar la siguiente línea de código:

Esto no va a funcionar en una tabla con formado ancho ya que la función lm necesita length y year como columnas de la tabla para funcionar.

Otra situación son los gráficos, por ejmeplo ¿Cómo podemos hacer un gráfico para cada año? Para esto es posible llamar la función plot una vez para cada año, sin embargo esto esto resulta tedioso, especialmente si hay muchos años en los datos, es difícil mantener el código y agregar más años al set de datos.

El paquete tidyr tiene funciones que nos permiten cambiar entre el formato ancho y largo utilizando la función gather:

site year length
gold 1990 101
lake 1990 104
dredge 1990 144
dredge 1993 145

En esta clase vamos a aprender a usar las funciones más comunes de los paquetes dplyr y tidyr:

  • dplyr
    • mutate()
    • group_by()
    • summarise()
    • select()
    • filter()
    • arrange()
    • left_join()
    • rename()
  • tidyr
    • gather()
    • spread()
    • extract()
    • separate()

8.3 Configuración (Setup)

Vamos a comenzar con las funciones más comunes del paquete dplyr. Para demostrar como se usan, vamos a trabajar con una versión de un set de datos ya pre-ordenado del Departamento de Pesca y Caza de Alaska (ADF&G) que contiene los datos de capturas comerciales de 1878 a 1997 (Byerly (1999)). El set de datos y su referencia a la fuente original se puede encontrar en el siguiente archivo público: https://knb.ecoinformatics.org/#view/df35b.304.2.

Primero, vamos a cargar los paquetes dplyr y tidyr:

Ahora se leen los datos y les daremos una mirada:

##   Region Year Chinook Sockeye Coho Pink Chum All notesRegCode
## 1    SSE 1886       0       5    0    0    0   5             
## 2    SSE 1887       0     155    0    0    0 155             
## 3    SSE 1888       0     224   16    0    0 240             
## 4    SSE 1889       0     182   11   92    0 285             
## 5    SSE 1890       0     251   42    0    0 292             
## 6    SSE 1891       0     274   24    0    0 298

Nota: para descargar los datos, copié el URL desde el botón de descarga (Download) en https://knb.ecoinformatics.org/#view/df35b.304.2

Este dataset está relativamente limpio y es de fácil interpretación. Si bien estos datos están limpios, el formato de la tabla hace que sea difícil usarlos para alguno de los análisis que debemos realizar. Como primer paso tenemos que arreglar eso.

8.4 Acerca del operador de pipe (tubo; %>%)

Antes de comenzar a aprender sobre tidyr y dplyr es necesarios que se explique de que se trata el %>%.

Tanto el paquete tidyr como dplyr usa el operador %>%. Este operador es una forma muy eficiente para encadenar operaciones. Esta “tubería” (como se le conoce en inglés) toma la salida de una operación anterior y la entrega como una entrada a la siguiente.

Digamos que quiere filtrar usando la función filter unas filas y seleccionar usando select una columna de la misma tabla de datos. Para esto se puede usar

df_filtered <- filter(df, ...)
df_selected <- select(df_filtered, ...)

Sin embargo, su código será más simple y eficiente (más rápido con set de datos grandes!)

df_cleaned <- df %>% 
              filter(...) %>%
              select(...)

Si usted piensa en el operador de asignación (<-) como “recibe”, entonces el operador pipe se puede leer como “entonces”.

De esta forma, el fragmento de código anterior se puede traducir como:

El data.frame limpio recibe los datos originales, entonces realiza un filtro (de los datos originales) y entonces selecciona (sobre los datos filtrados).

El beneficio de usar pipes es que no es necesario seguir el rastro (o de sobre-escribir) de sets de datos intermedios. El costo es que se hace un poco mas difícil de explicar el razonamiento de cada paso, especialmente cuando existen muchas operaciones encadenadas. Lo ideal es tener un balance entre escribir códigos eficientes (operaciones encadenadas) y que sean legibles e interpretables, para entender la lógica de que y por qué se hizo lo que se hizo.

RStudio tiene una combinación de teclas para %>% : Ctrl + Shift + M (Windows), Cmd + Shift + M (Mac).

8.5 Funciones

8.5.1 Seleccionar/remover columnas: select()

El primer problema con los datos con los que vamos a trabajar son las columnas All y notesRegCode. Vamos a seleccionar sólo las columnas que necesitamos y las asignaremos a la variable llamada catch_data.

##   Region Year Chinook Sockeye Coho Pink Chum
## 1    SSE 1886       0       5    0    0    0
## 2    SSE 1887       0     155    0    0    0
## 3    SSE 1888       0     224   16    0    0
## 4    SSE 1889       0     182   11   92    0
## 5    SSE 1890       0     251   42    0    0
## 6    SSE 1891       0     274   24    0    0

Esto está mucho mejor.

select nos permite indicar las columnas que no queremos, para esto sólo tenemos que pasar los nombres de las columnas (sin comillas) con un signo menos (-):

##   Region Year Chinook Sockeye Coho Pink Chum
## 1    SSE 1886       0       5    0    0    0
## 2    SSE 1887       0     155    0    0    0
## 3    SSE 1888       0     224   16    0    0
## 4    SSE 1889       0     182   11   92    0
## 5    SSE 1890       0     251   42    0    0
## 6    SSE 1891       0     274   24    0    0

8.5.2 Cambiar el formato: gather() y spread()

El siguiente problema con los datos es que están en un formato ancho y que para trabajar los necesitamos en uno largo. gather() del paquete tidyr nos ayuda a hacer este cambio en el formato:

##   Region Year species catch
## 1    SSE 1886 Chinook     0
## 2    SSE 1887 Chinook     0
## 3    SSE 1888 Chinook     0
## 4    SSE 1889 Chinook     0
## 5    SSE 1890 Chinook     0
## 6    SSE 1891 Chinook     0

La sintaxis que usamos para gather() puede ser un poco confusa, así que la explicaremos con el diagrama rotulado que se presenta en la figura 8.2:

Diagrama rotulado donde se explica cada uno de los inputs a la función gather.

Figure 8.2: Diagrama rotulado donde se explica cada uno de los inputs a la función gather.

Los primeros dos argumentos de la función gather() son los nombres de las columnas nuevas que serán creadas y los otros argumentos con el símbolo - son las columnas que se deben mantener en el proceso. La función opuesta a gather() es spread(), funciona la misma forma declarativa:

##   Region Year Chinook Chum Coho Pink Sockeye
## 1    ALU 1911       0    0    0    0       9
## 2    ALU 1912       0    0    0    0       0
## 3    ALU 1913       0    0    0    0       0
## 4    ALU 1914       0    0    0    0       0
## 5    ALU 1915       0    0    0    0       0
## 6    ALU 1916       0    0    1  180      76

8.5.3 Renombrando columnas con rename()

Si le da una mirada a los datos, va a notar que existen valores en la columna catch que son muy pequeños (se supone que son valores de capturas anuales). Si analizamos los metadata veremos que la columna catch está en miles de pescados, así que la vamos a convertir antes de seguir con los análisis.

Primero cambiemos el nombre de catch a catch_thousands:

##   Region Year species catch_thousands
## 1    SSE 1886 Chinook               0
## 2    SSE 1887 Chinook               0
## 3    SSE 1888 Chinook               0
## 4    SSE 1889 Chinook               0
## 5    SSE 1890 Chinook               0
## 6    SSE 1891 Chinook               0

8.5.4 Agregando columnas: mutate()

Ahora vamos a crear una columna llamada catch con individuos como unidad (en vez de miles de pescados). Note que hemos agregado, a la expresión que creamos mas arriba, la llamada a la función mutate. Esto aprovecha el operador pipe agrupando un set de comandos, todos relacionados con el objetivo de limpiar el data.frame catch_long.

Aquí va a ver el siguiente mensaje:

Error in mutate_impl(.data, dots) : Evaluation error: non-numeric argument to binary operator.

Este mensaje de error es críptico – ¿Qué es lo que está diciendo? Este tipo de errores pueden ser difíciles de diagnosticar, lo que está sugiriendo es que la columna catch no es exactamente lo que estábamos esperando. ¿Cómo podemos ver esto? R le entrega una variedad de funciones que le permiten hacer resúmenes rápidos de una tabla de datos relativamente grande:

##     Region               Year        species          catch_thousands   
##  Length:8540        Min.   :1878   Length:8540        Length:8540       
##  Class :character   1st Qu.:1922   Class :character   Class :character  
##  Mode  :character   Median :1947   Mode  :character   Mode  :character  
##                     Mean   :1946                                        
##                     3rd Qu.:1972                                        
##                     Max.   :1997
  • Actividad: ¿Qué otras formas (funciones) se podrían haber usado para identificar el problema?

Vea que en la salida de más arriba la columna catch_thousands se ve como Class :character. Esto quiere decir que contiene caracteres de texto y no números, esto parece incorrecto ya que las capturas deberían ser números enteros (R muestra esto como enteros o integers en inglés).

Vamos a convertir esta columna a enteros y veremos que sucede:

## Warning: NAs introduced by coercion

Aquí se muestra el mensaje de error “NAs introduced by coercion” donde R dice que no fue posible convertir cada uno de los valores a enteros (integers) y que, para esos valores que no pudo convertir, pusa NA en su lugar. Este es común y lo podemos esperar cuando realicemos limpieza de datos, sin embargo es importante que tengamos las capacidades para manejar estas situaciones.

Podemos encontrar cuales son los valores con NAs combinando las funciones is.na() y which(), y guardando el resultado en una variable, por ejemplo i.

## [1] 401

Se ve que existe sólo una fila con problemas, veamosla:

##     Region Year species catch_thousands
## 401    GSE 1955 Chinook               I

Bueno, esto es raro: El valor en catch_thousands es I lo que claramente no es un número. Resulta que este set de datos viene de un archivo PDF que fue convertido automáticamente a CSV (formato de texto separado por comas) y este valor I corresponde a un 1.

Ahora procedemos a arreglarlo:

##   Region Year species catch_thousands
## 1    SSE 1886 Chinook               0
## 2    SSE 1887 Chinook               0
## 3    SSE 1888 Chinook               0
## 4    SSE 1889 Chinook               0
## 5    SSE 1890 Chinook               0
## 6    SSE 1891 Chinook               0

Vea que en esta serie de comandos se hace una doble llamada a la función mutate(). Esto ocurre porque el proceso de mutate() es secuencial, los resultados de un mutate() estarán disponibles para el siguiente.

Ahora realicemos la conversión nuevamente, agregando un nuevo llamado a la misma función.

##   Region Year species catch_thousands catch
## 1    SSE 1886 Chinook               0     0
## 2    SSE 1887 Chinook               0     0
## 3    SSE 1888 Chinook               0     0
## 4    SSE 1889 Chinook               0     0
## 5    SSE 1890 Chinook               0     0
## 6    SSE 1891 Chinook               0     0

Ahora no se ve ningún mensaje de error y podemos eliminar la columna catch_thousands que ya no necesitamos:

##   Region Year species catch
## 1    SSE 1886 Chinook     0
## 2    SSE 1887 Chinook     0
## 3    SSE 1888 Chinook     0
## 4    SSE 1889 Chinook     0
## 5    SSE 1890 Chinook     0
## 6    SSE 1891 Chinook     0

En este punto los datos estan limpios y ordenados para inicial el análisis.

8.5.5 group_by y summarise

Como se dijo en la introducción del presente capítulo, dplyr nos deja utilizar la estrategia Divida-Aplique-Combine, que se ejemplifica con el uso de las funciones group_by() y summarise():

## # A tibble: 6 x 2
##   Region `mean(catch)`
##   <chr>          <dbl>
## 1 ALU           40384.
## 2 BER           16373.
## 3 BRB         2709796.
## 4 CHG          315487.
## 5 CKI          683571.
## 6 COP          179223.
  • Actividad: Encuentre otra función para agrupar y calcular estadísticas para cada grupo.
  • Ejercicio: Encuentre si es posible agrupar múltiples variables.

Otro uso común de la función group_by() seguida de summarize() es para contar el número de filas en cada grupo. Para esto usamos la función n() del paquete dplyr.

## # A tibble: 6 x 2
##   Region     n
##   <chr>  <int>
## 1 ALU      435
## 2 BER      510
## 3 BRB      570
## 4 CHG      550
## 5 CKI      525
## 6 COP      470

8.5.6 Filtrando columnas: filter()

filter() es una función que se usa para filtrar las filas de un data.frame que cumplen con alguna condición. Es similar a la función subset() de R base.

Vamos a nuestro data.frame original y filtremos con la función filter() algunos datos:

##   Region Year species catch
## 1    SSE 1886 Chinook     0
## 2    SSE 1887 Chinook     0
## 3    SSE 1888 Chinook     0
## 4    SSE 1889 Chinook     0
## 5    SSE 1890 Chinook     0
## 6    SSE 1891 Chinook     0
  • Actividad: Filtre las capturas que sean mayores a 1 millón de salmones.
  • Actividad: Filtre sólo los salmones Chinook en la Región SSE.

8.5.7 Ordenando sus datos: arrange()

arrange() es la función que se usar para ordenar filas de un data.frame. En mi experiencia, arrange() se usa comúnmente en dos casos:

  • Cuando se necesita calcular una suma acumulada (con cumsum()).
  • Cuando se necesita mostrar una tabla (por ejemplo en un documento .Rmd) ordenada.

Re-calculemos el promedio de captura por región y luego ordenemos la salida, usando arrange(), por captura promedio:

## # A tibble: 6 x 2
##   Region mean_catch
##   <chr>       <dbl>
## 1 BER        16373.
## 2 KTZ        18836.
## 3 ALU        40384.
## 4 NRS        51503.
## 5 KSK        67642.
## 6 YUK        68646.

La función arrange() ordena por defecto en orden creciente. Para invertir este orden se usa la función desc() en la columna que se quiera ordenar:

## # A tibble: 6 x 2
##   Region mean_catch
##   <chr>       <dbl>
## 1 SSE      3184661.
## 2 BRB      2709796.
## 3 NSE      1825021.
## 4 KOD      1528350 
## 5 PWS      1419237.
## 6 SOP      1110942.

8.5.8 Joins en dplyr

Ahora que ya somos expertos en manipular un data.frame único ¿Qué podemos hacer?

La respuesta es obvia, manipular más de un data.frame.

Si usted a utilizado bases de datos anteriormente, es probable que sea familiar con las funciones “join”. Estas funciones nos permiten unir, en forma inteligente, dos tablas para formar una sola, esto se hace utilizando alguna columna que tengan en común. Ya hemos hablado sobre joins en la sección sobre uniones de tablas [7.6] del capítulo [7]. Aquí vamos a ver como se hace dentro del entorno dplyr.

El set de datos con el que hemos estado trabajando, https://knb.ecoinformatics.org/#view/df35b.304.2, contiene un segundo documento con datos en formato CSV que contiene las definiciones de los códigos para cada Region de Alaska. Esto es una forma muy común de almacenar información auxiliar sobre nuestro set de datos de interés (capturas) pero que, para fines analíticos, a menudo las queremos en el mismo data.frame. Join (unión), nos permite hacer esto fácilmente.

Veamos que es lo que hará nuestra unión utilizando una versión simplificada de los datos:

Uso de _leftjoin_ para combinar dos tablas utilizando la columna **Region** como clave.

Figure 8.3: Uso de leftjoin para combinar dos tablas utilizando la columna Region como clave.

Primero, leamos la tabla con la definición de los nombres de la regiones y seleccionemos sólo las columnas de interés. Note que he usado el operador %>% para pasar los resultados de la función read.csv, creando de esta forma un fragmento de código que lee y selecciona los datos de interés.

##      code                                  mgmtArea
## 1     GSE              Unallocated Southeast Alaska
## 2     NSE                 Northern Southeast Alaska
## 3     SSE                 Southern Southeast Alaska
## 4     YAK                                   Yakutat
## 5 PWSmgmt      Prince William Sound Management Area
## 6     BER Bering River Subarea Copper River Subarea

Si examina la columna region_defs del data.frame, puede ver que los nombres de columnas no son iguales a las presentadas en la figura 8.3. Si los nombres de las columnas clave no son iguales, es posible definir implícitamente cuales son las columnas de la tabla a la derecha y a la izquierda que se deben usar, tal como se muestra en el código siguiente:

##   Region Year species catch                  mgmtArea
## 1    SSE 1886 Chinook     0 Southern Southeast Alaska
## 2    SSE 1887 Chinook     0 Southern Southeast Alaska
## 3    SSE 1888 Chinook     0 Southern Southeast Alaska
## 4    SSE 1889 Chinook     0 Southern Southeast Alaska
## 5    SSE 1890 Chinook     0 Southern Southeast Alaska
## 6    SSE 1891 Chinook     0 Southern Southeast Alaska

Note que me he desviado de la sintaxis que usa los pipes (a pesar de que igualmente funciona aquí) porque prefiero ver los data.frames que estoy uniendo uno al lado del otro.

Otra forma de hacer esta unión es cambiando los nombres de las columnas usando la función rename, de esta forma se puede cambiar el nombre de la columna code a Region el data.frame region_defs, y ejecutar el left_join de la siguiente forma:

Ahora las capturas tienen la información auxiliar con la definición de las regiones a su lado. Nota: dplyr entrega un set completo de uniones: inner, left, right, full, semi, anti y no solo left_join.

8.5.9 separate() y unite()

separate() y su complemento unite(), nos permiten separar fácilmente una columna en varias (o varias en una sola). Esto puede ser realmente útil cuando tenemos una columna con fechas y necesitamos agrupar los datos por año o mes. Creemos un nuevo data.frame con datos ficticios para ilustrar esto:

##   month day year
## 1     5  24 1930
## 2     5  25 1930
## 3     5  26 1930
## 4     5  27 1930
## 5     5  28 1930
  • Actividad: Separe la columna city del siguiente data.frame en las columnas city y state_code:

unite() hace exactamente lo inverso a la función separate():

##        date
## 1 5/24/1930
## 2 5/25/1930
## 3 5/26/1930
## 4 5/27/1930
## 5 5/28/1930
  • Actividad: Use unite() en su solución a la actividad anterior para combinar las columnas que acaba de separar y déjelas en una única columna que incluya la cuidad y el estado, city:

8.6 Resumen

Acabamos de aprender varias cosas que se pueden hacer con los paquetes dplyr y tidyr, pero si se pregunta como se vería esto en un análisis real…

Veamos eso a continuación:

## # A tibble: 6 x 3
##   Region mean_catch mgmtArea                                 
##   <chr>       <dbl> <chr>                                    
## 1 ALU        40384. Aleutian Islands Subarea                 
## 2 BER        16373. Bering River Subarea Copper River Subarea
## 3 BRB      2709796. Bristol Bay Management Area              
## 4 CHG       315487. Chignik Management Area                  
## 5 CKI       683571. Cook Inlet Management Area               
## 6 COP       179223. Copper River Subarea

Referencias

Byerly, Mike. 1999. “Alaska Commercial Salmon Catches by Management Region (1886- 1997).” KNB. https://knb.ecoinformatics.org/view/df35b.304.2.