5 Manipulación de datos usando tidyverse

Las funciones que hemos estado usando hasta ahora, como read.table y str(), vienen incorporadas en R. Pero existen incontables funciones en R a las que puedes acceder instalando lo que se conoce como paquetes (packages). Antes de usar un paquete por primera vez, primero debes instalarlo y luego debes importarlo en cada sesión de R en la que quieras usarlo.

Hay dos formas de instalar paquetes en R, a través de la consola como ya vimos en la sección ??, y a través de la opción Install en la pestaña Packages de RStudio (opción disponibles para paquetes CRAN solamente):

  • Para instalar y cargar el paquete tidyverse usando RStudio, dirígete a la pestaña Packages -> Install -> escribe el nombre del paquete y haz clic en “Install”. Una vez instalado, para importarlo sólo debes identificarlo y seleccionarlo en la lista de paquetes.

install_packages Instalación de paquetes en RStudio.

tidyverse es un paquete que incluye la instalación varios paquetes útiles para la manipulación y análisis de datos, tales como dplyr, tidyr, ggplot2, etc. Anteriormente practicamos como extraer información de tablas usando [ ], a continuación vamos a utilizar los paquetes dplyr y tidyr para manipular nuestra tabla de datos más fácil, rápido y con más funcionalidades.

  • Vamos a leer nuevamente nuestra tabla de datos, ésta vez usando la función read_csv() del paquete tidyverse:
data <- read_csv("data/IR_table1.csv")
## Parsed with column specification:
## cols(
##   .default = col_double(),
##   sample_ID = col_character(),
##   geo_loc_name = col_character(),
##   species = col_character()
## )
## See spec(...) for full column specifications.
# Usa la función str() para inspeccionar los datos
str(data)
## tibble [87 × 30] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ sample_ID                  : chr [1:87] "SRR6442697" "SRR6442698" "SRR6442699" "SRR6442700" ...
##  $ geo_loc_name               : chr [1:87] "Estrecho de Magallanes" "Estrecho de Magallanes" "Estrecho de Magallanes" "Estrecho de Magallanes" ...
##  $ species                    : chr [1:87] "Megaptera novaeangliae" "Megaptera novaeangliae" "Megaptera novaeangliae" "Megaptera novaeangliae" ...
##  $ observed                   : num [1:87] 31 33 43 29 26 21 37 32 28 30 ...
##  $ shannon                    : num [1:87] 2.12 1.5 2.25 1.31 1.08 ...
##  $ richness_0                 : num [1:87] 31 33 43 29 26 21 37 32 28 30 ...
##  $ richness_20                : num [1:87] 31 33 43 29 26 21 37 32 28 30 ...
##  $ richness_50                : num [1:87] 31 33 43 29 26 21 37 32 28 30 ...
##  $ richness_80                : num [1:87] 31 33 43 29 26 21 37 32 28 30 ...
##  $ diversities_inverse_simpson: num [1:87] 6.11 2.51 7.39 2.64 1.87 ...
##  $ diversities_gini_simpson   : num [1:87] 0.836 0.601 0.865 0.622 0.466 ...
##  $ diversities_shannon        : num [1:87] 2.12 1.5 2.25 1.31 1.08 ...
##  $ diversities_fisher         : num [1:87] 3.91 4.84 5.37 3.09 2.96 ...
##  $ diversities_coverage       : num [1:87] 3 1 3 1 1 2 2 1 2 1 ...
##  $ evenness_camargo           : num [1:87] 0.0293 0.0182 0.0339 0.0139 0.0114 ...
##  $ evenness_pielou            : num [1:87] 0.618 0.429 0.598 0.388 0.331 ...
##  $ evenness_simpson           : num [1:87] 0.0283 0.0116 0.03421 0.01224 0.00867 ...
##  $ evenness_evar              : num [1:87] 0.0855 0.1157 0.074 0.0594 0.083 ...
##  $ evenness_bulla             : num [1:87] 0.0604 0.0558 0.0599 0.0401 0.0437 ...
##  $ dominance_dbp              : num [1:87] 0.302 0.614 0.242 0.525 0.714 ...
##  $ dominance_dmn              : num [1:87] 0.495 0.706 0.439 0.838 0.853 ...
##  $ dominance_absolute         : num [1:87] 3293 2699 3919 19071 13936 ...
##  $ dominance_relative         : num [1:87] 0.302 0.614 0.242 0.525 0.714 ...
##  $ dominance_simpson          : num [1:87] 0.164 0.399 0.135 0.378 0.534 ...
##  $ dominance_core_abundance   : num [1:87] 0.56 0.875 0.503 0.993 0.982 ...
##  $ dominance_gini             : num [1:87] 0.971 0.982 0.966 0.986 0.989 ...
##  $ rarity_log_modulo_skewness : num [1:87] 2.06 2.06 2.06 2.06 2.06 ...
##  $ rarity_low_abundance       : num [1:87] 0.00918 0.00751 0.00569 0.00289 0.00446 ...
##  $ rarity_noncore_abundance   : num [1:87] 0.07617 0.02479 0.28297 0.00022 0.00113 ...
##  $ rarity_rare_abundance      : num [1:87] 0.07617 0.02479 0.28297 0.00022 0.00113 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   sample_ID = col_character(),
##   ..   geo_loc_name = col_character(),
##   ..   species = col_character(),
##   ..   observed = col_double(),
##   ..   shannon = col_double(),
##   ..   richness_0 = col_double(),
##   ..   richness_20 = col_double(),
##   ..   richness_50 = col_double(),
##   ..   richness_80 = col_double(),
##   ..   diversities_inverse_simpson = col_double(),
##   ..   diversities_gini_simpson = col_double(),
##   ..   diversities_shannon = col_double(),
##   ..   diversities_fisher = col_double(),
##   ..   diversities_coverage = col_double(),
##   ..   evenness_camargo = col_double(),
##   ..   evenness_pielou = col_double(),
##   ..   evenness_simpson = col_double(),
##   ..   evenness_evar = col_double(),
##   ..   evenness_bulla = col_double(),
##   ..   dominance_dbp = col_double(),
##   ..   dominance_dmn = col_double(),
##   ..   dominance_absolute = col_double(),
##   ..   dominance_relative = col_double(),
##   ..   dominance_simpson = col_double(),
##   ..   dominance_core_abundance = col_double(),
##   ..   dominance_gini = col_double(),
##   ..   rarity_log_modulo_skewness = col_double(),
##   ..   rarity_low_abundance = col_double(),
##   ..   rarity_noncore_abundance = col_double(),
##   ..   rarity_rare_abundance = col_double()
##   .. )

Como podrás notar, la función read_csv() hace algunos cambios al cargar la tabla con respecto a lo que revisamos anteriormente usando read.table(). Las diferencias son:

  1. Al leer la tabla, muestra un resumen del tipo de dato de cada columna, y sólo muestra las primeras filas y tantas columnas como se puedan visualizar en la pantalla.
  2. Las columnas de clase character (caracteres) no son convertidas en factores.

A continuación vamos a aprender algunas de las funciones más comunes de dplyr:

  • select(): extraer columnas.
  • filter(): extraer filas según condiciones.
  • mutate(): crear nuevas columnas usando la información de otras columnas.
  • group_by() y summarize(): cálcula estadísticas en datos agrupados.
  • arrange(): ordena resultados.
  • count(): conteo de datos.

5.1 Seleccionar columnas y filtrar filas

  • Para seleccionar columnas de una tabla de datos o data frame, vamos a usar la función select(). Luego, para seleccionar filas de acuerdo a cierto criterio (filtrar), vamos a usar la función filter().
dplyr::select(data, sample_ID, geo_loc_name, species, observed) # los argumentos son, primero el objeto que contiene el data frame, y luego los títulos de las columnas a extraer
## # A tibble: 87 x 4
##    sample_ID  geo_loc_name           species                observed
##    <chr>      <chr>                  <chr>                     <dbl>
##  1 SRR6442697 Estrecho de Magallanes Megaptera novaeangliae       31
##  2 SRR6442698 Estrecho de Magallanes Megaptera novaeangliae       33
##  3 SRR6442699 Estrecho de Magallanes Megaptera novaeangliae       43
##  4 SRR6442700 Estrecho de Magallanes Megaptera novaeangliae       29
##  5 SRR6442701 Estrecho de Magallanes Megaptera novaeangliae       26
##  6 SRR6442702 Estrecho de Magallanes Megaptera novaeangliae       21
##  7 SRR6442703 Estrecho de Magallanes Megaptera novaeangliae       37
##  8 SRR6442704 Estrecho de Magallanes Megaptera novaeangliae       32
##  9 SRR6442705 Estrecho de Magallanes Megaptera novaeangliae       28
## 10 SRR6442706 Estrecho de Magallanes Megaptera novaeangliae       30
## # … with 77 more rows
dplyr::filter(data, geo_loc_name == "Chiloe") # los argumentos son, primero el objeto que contiene el data frame, y luego la columna con el criterio de filtro
## # A tibble: 28 x 30
##    sample_ID geo_loc_name species observed shannon richness_0 richness_20
##    <chr>     <chr>        <chr>      <dbl>   <dbl>      <dbl>       <dbl>
##  1 SRR64427… Chiloe       Megapt…       63   1.28          63          63
##  2 SRR64427… Chiloe       Megapt…       14   0.732         14          14
##  3 SRR64427… Chiloe       Balaen…       18   1.41          18          18
##  4 SRR64427… Chiloe       Balaen…       19   1.68          19          19
##  5 SRR64427… Chiloe       Balaen…       36   2.31          36          36
##  6 SRR64427… Chiloe       Balaen…       63   1.31          63          63
##  7 SRR64427… Chiloe       Balaen…       29   1.73          29          29
##  8 SRR64427… Chiloe       Balaen…       38   1.65          38          38
##  9 SRR64427… Chiloe       Balaen…       81   2.05          81          81
## 10 SRR64427… Chiloe       Balaen…       78   0.725         78          78
## # … with 18 more rows, and 23 more variables: richness_50 <dbl>,
## #   richness_80 <dbl>, diversities_inverse_simpson <dbl>,
## #   diversities_gini_simpson <dbl>, diversities_shannon <dbl>,
## #   diversities_fisher <dbl>, diversities_coverage <dbl>,
## #   evenness_camargo <dbl>, evenness_pielou <dbl>, evenness_simpson <dbl>,
## #   evenness_evar <dbl>, evenness_bulla <dbl>, dominance_dbp <dbl>,
## #   dominance_dmn <dbl>, dominance_absolute <dbl>, dominance_relative <dbl>,
## #   dominance_simpson <dbl>, dominance_core_abundance <dbl>,
## #   dominance_gini <dbl>, rarity_log_modulo_skewness <dbl>,
## #   rarity_low_abundance <dbl>, rarity_noncore_abundance <dbl>,
## #   rarity_rare_abundance <dbl>

¿Quieres seleccionar y filtrar al mismo tiempo? Claro! Hay formas de hacer varias operaciones consecutivas en una misma instrucción, de esta manera evitamos tener que guardar objetos “intermedios” innecesariamente.

5.2 Funciones anidadas y pipes

  • Vamos a anidar funciones (i.e. una función dentro de otra):
nrow(data)
## [1] 87
ncol(data)
## [1] 30
data_div <- dplyr::select(dplyr::filter(data, shannon > 1.5), sample_ID, geo_loc_name, species, observed, shannon)
head(data_div)
## # A tibble: 6 x 5
##   sample_ID  geo_loc_name           species                observed shannon
##   <chr>      <chr>                  <chr>                     <dbl>   <dbl>
## 1 SRR6442697 Estrecho de Magallanes Megaptera novaeangliae       31    2.12
## 2 SRR6442699 Estrecho de Magallanes Megaptera novaeangliae       43    2.25
## 3 SRR6442703 Estrecho de Magallanes Megaptera novaeangliae       37    1.86
## 4 SRR6442708 Estrecho de Magallanes Megaptera novaeangliae       27    1.91
## 5 SRR6442709 Estrecho de Magallanes Megaptera novaeangliae       36    1.88
## 6 SRR6442712 Estrecho de Magallanes Megaptera novaeangliae       36    1.94
nrow(data_div) # 39 muestras tienen un índice de shannon mayor a 1.5
## [1] 39
ncol(data_div)
## [1] 5

Es importante recordar que R lee la línea de comando desde dentro hacia fuera. En éste caso, primero se hizo el filtro y luego la selección.

  • Usar funciones anidadas puede ser engorroso cuando quieres hacer muchas operaciones consecutivas, en cuyo caso es convenientes usar pipes. Pipes te permiten usar varias funciones consecutivas, de forma que el output de una función será en input de la siguiente. Los pipes en R lucen así: %>%.
data %>%
  dplyr::filter(shannon > 1.5) %>% 
  dplyr::select(sample_ID, geo_loc_name, species, observed, shannon)
## # A tibble: 39 x 5
##    sample_ID  geo_loc_name           species                observed shannon
##    <chr>      <chr>                  <chr>                     <dbl>   <dbl>
##  1 SRR6442697 Estrecho de Magallanes Megaptera novaeangliae       31    2.12
##  2 SRR6442699 Estrecho de Magallanes Megaptera novaeangliae       43    2.25
##  3 SRR6442703 Estrecho de Magallanes Megaptera novaeangliae       37    1.86
##  4 SRR6442708 Estrecho de Magallanes Megaptera novaeangliae       27    1.91
##  5 SRR6442709 Estrecho de Magallanes Megaptera novaeangliae       36    1.88
##  6 SRR6442712 Estrecho de Magallanes Megaptera novaeangliae       36    1.94
##  7 SRR6442714 Estrecho de Magallanes Megaptera novaeangliae       26    1.81
##  8 SRR6442715 Estrecho de Magallanes Megaptera novaeangliae       35    1.61
##  9 SRR6442722 Chiloe                 Balaenoptera musculus        19    1.68
## 10 SRR6442723 Chiloe                 Balaenoptera musculus        36    2.31
## # … with 29 more rows

Como %>% pasa el objeto de su izquierda como el primer argumento de la función a su derecha, no necesitamos especificar el data frame como el primer argumento de las funciones filter() y select().

5.3 Mutate: crear nuevas columnas desde información existente en otras columnas

En algunas ocasiones necesitamos generar nueva información a partir de la existente, por ejemplo, para hacer conversiones o cálculos matemáticos.

data <- data %>% 
  mutate(log10_dom_abs = log10(dominance_absolute))
# Dale un vistazo a la tabla (View(data)), la última columna es la nueva "log10_dom_abs", también notarás que, en la pestaña "Environment" de RStudio, ahora el objeto "data" tiene "31 variables".
# Si deseas tener una vista previa de como quedará tu "data" antes de guardar cualquier cambio, puedes usar pipe para agregar la función head() al final
data %>% 
  mutate(log10_dom_abs = log10(dominance_absolute)) %>% 
  head()
## # A tibble: 6 x 31
##   sample_ID geo_loc_name species observed shannon richness_0 richness_20
##   <chr>     <chr>        <chr>      <dbl>   <dbl>      <dbl>       <dbl>
## 1 SRR64426… Estrecho de… Megapt…       31    2.12         31          31
## 2 SRR64426… Estrecho de… Megapt…       33    1.50         33          33
## 3 SRR64426… Estrecho de… Megapt…       43    2.25         43          43
## 4 SRR64427… Estrecho de… Megapt…       29    1.31         29          29
## 5 SRR64427… Estrecho de… Megapt…       26    1.08         26          26
## 6 SRR64427… Estrecho de… Megapt…       21    1.13         21          21
## # … with 24 more variables: richness_50 <dbl>, richness_80 <dbl>,
## #   diversities_inverse_simpson <dbl>, diversities_gini_simpson <dbl>,
## #   diversities_shannon <dbl>, diversities_fisher <dbl>,
## #   diversities_coverage <dbl>, evenness_camargo <dbl>, evenness_pielou <dbl>,
## #   evenness_simpson <dbl>, evenness_evar <dbl>, evenness_bulla <dbl>,
## #   dominance_dbp <dbl>, dominance_dmn <dbl>, dominance_absolute <dbl>,
## #   dominance_relative <dbl>, dominance_simpson <dbl>,
## #   dominance_core_abundance <dbl>, dominance_gini <dbl>,
## #   rarity_log_modulo_skewness <dbl>, rarity_low_abundance <dbl>,
## #   rarity_noncore_abundance <dbl>, rarity_rare_abundance <dbl>,
## #   log10_dom_abs <dbl>

5.4 Dividir -> aplicar -> combinar

Varias operaciones de análisis de datos se pueden realizar, primero dividiendo los datos en grupos, segundo aplicando análisis a cada grupo, y tercero combinando los resultados. Hacemos esto usando las funciones group_by() y summarize() juntas. group_by() toma como argumento los nombres de la columna que contiene valores categóricos, a partir de las cuales queremos hacer algún cálculo. summarize() colapsa cada grupo en una única fila.

  • Vamos a calcular el promedio y desviación estándar (mean(); sd()) del número de taxas observadas (columna “observed”) por zona (columna “geo_loc_name”) y especie (columna “species”) de ballena:
data %>% 
  group_by(geo_loc_name, species) %>% # puedes agrupar por una o múltiples columnas
  summarize(mean_observed = mean(observed), # una vez que los datos están agrupados, también puedes aplicar múltiples análisis al mismo tiempo y en múltiples variables
            sd_observed = sd(observed), 
            mean_shannon = mean(shannon), 
            sd_shannon = sd(shannon))
## `summarise()` regrouping output by 'geo_loc_name' (override with `.groups` argument)
## # A tibble: 5 x 6
## # Groups:   geo_loc_name [3]
##   geo_loc_name      species    mean_observed sd_observed mean_shannon sd_shannon
##   <chr>             <chr>              <dbl>       <dbl>        <dbl>      <dbl>
## 1 Chiloe            Balaenopt…          55.0       19.9          1.57      0.568
## 2 Chiloe            Megaptera…          38.5       34.6          1.00      0.385
## 3 Estrecho de Maga… Megaptera…          33.5        7.60         1.42      0.457
## 4 Reserva Nacional… Balaenopt…          33.9       22.3          1.34      0.717
## 5 Reserva Nacional… Megaptera…          43.3       23.6          1.52      0.564
  • Si lo necesitas, también puedes agregar un filtro antes de agrupar los datos y hacer estadística de los grupos. Por ejemplo, supongamos que decidimos no considerar aquellas muestras con menos de 20 taxas (observed < 20):
data %>% 
  dplyr::filter(observed > 20) %>% 
  group_by(geo_loc_name, species) %>% 
  summarize(mean_observed_min20 = mean(observed)) %>% 
  print() # para ver el resultado (output) en la consola
## `summarise()` regrouping output by 'geo_loc_name' (override with `.groups` argument)
## # A tibble: 5 x 3
## # Groups:   geo_loc_name [3]
##   geo_loc_name                         species                mean_observed_min…
##   <chr>                                <chr>                               <dbl>
## 1 Chiloe                               Balaenoptera musculus                59.7
## 2 Chiloe                               Megaptera novaeangliae               63  
## 3 Estrecho de Magallanes               Megaptera novaeangliae               33.5
## 4 Reserva Nacional Pinguino de Humbol… Balaenoptera physalus                46.8
## 5 Reserva Nacional Pinguino de Humbol… Megaptera novaeangliae               49.2
  • Muchas veces es útil re-organizar los datos para una más eficiente interpretación de los resultados. Por ejemplo, si queremos ordenar los resultados por número promedio de taxa observado por grupo (“mean_observed”) en orden decreciente:
data %>% 
  group_by(geo_loc_name, species) %>% 
  summarize(mean_observed = mean(observed), 
            sd_observed = sd(observed), 
            mean_shannon = mean(shannon), 
            sd_shannon = sd(shannon)) %>% 
  arrange(desc(mean_observed)) # la función arrange(), por defecto, ordena los datos en orden creciente, usamos la función desc() para ordenar en orden decreciente
## `summarise()` regrouping output by 'geo_loc_name' (override with `.groups` argument)
## # A tibble: 5 x 6
## # Groups:   geo_loc_name [3]
##   geo_loc_name      species    mean_observed sd_observed mean_shannon sd_shannon
##   <chr>             <chr>              <dbl>       <dbl>        <dbl>      <dbl>
## 1 Chiloe            Balaenopt…          55.0       19.9          1.57      0.568
## 2 Reserva Nacional… Megaptera…          43.3       23.6          1.52      0.564
## 3 Chiloe            Megaptera…          38.5       34.6          1.00      0.385
## 4 Reserva Nacional… Balaenopt…          33.9       22.3          1.34      0.717
## 5 Estrecho de Maga… Megaptera…          33.5        7.60         1.42      0.457

5.5 Contar

La función count() nos permite conocer el número de observaciones por cada variable, o combinación de ellas, en tus datos.

data %>% 
  dplyr::count(species) # ¿cuantas muestras por especie de ballena tenemos?
data %>% 
  dplyr::count(geo_loc_name, sort = TRUE) # argumento "sort = TRUE" para ordenar (decreciente)
data %>% 
  dplyr::count(geo_loc_name, species) # ¿cuantas muestras de cada especie tenemos por zona?

5.6 Remodelar tablas usando spread() y gather()

Con el objetivo de explorar las relaciones entre ciertas variables de interés en nuestros datos, podemos remodelar la tabla de datos de acuerdo a éstas variables.

Supongamos que nos interesa explorar la relación entre la diversidad del microbioma de la piel (“shannon”) de las especies de ballena (“species”) y su locación geográfica.

  • Primero, necesitamos usar group_by() y summarize(), para agrupar nuestras variables de interés y crear una nueva columna con los valores de índice de Shannon promedio para cada grupo. Después, usamos la función spread() para transformar data de modo que: cada especie ahora sea una columna, cada locación geográfica ahora sea una fila, y los valores de Shannon estén en cada celda según corresponda.
data_spread <- data %>% 
  group_by(geo_loc_name, species) %>% 
  summarize(mean_shannon = mean(shannon)) %>% 
  spread(key = species, value = mean_shannon, fill = NA)
## `summarise()` regrouping output by 'geo_loc_name' (override with `.groups` argument)
head(data_spread)
## # A tibble: 3 x 4
## # Groups:   geo_loc_name [3]
##   geo_loc_name         `Balaenoptera musc… `Balaenoptera phy… `Megaptera novaea…
##   <chr>                              <dbl>              <dbl>              <dbl>
## 1 Chiloe                              1.57              NA                  1.00
## 2 Estrecho de Magalla…               NA                 NA                  1.42
## 3 Reserva Nacional Pi…               NA                  1.34               1.52
# Como el número de taxa observada varía por cada muestra, tenemos como resultado varias celdas "NA" ("missing data"). Para éstos casos, la función spread() viene con el argumento "fill".

Ahora vamos a suponer la situación contraria. Inicialmente, tenemos una tabla de datos como data_spread, en la que los nombres de las especies (“species”) son columnas, pero en vez de ello, queremos que las especies sean valores de la variable “species” (columna: “species”).

  • Para lograrlo, necesitamos reunir los nombres de las columnas (especies) y convertirlos en un set de variables:
data_gather <- data_spread %>% 
  gather(key = species, value = mean_shannon, -geo_loc_name) %>% 
  dplyr::filter(!is.na(mean_shannon))
head(data_gather)
## # A tibble: 5 x 3
## # Groups:   geo_loc_name [3]
##   geo_loc_name                          species                mean_shannon
##   <chr>                                 <chr>                         <dbl>
## 1 Chiloe                                Balaenoptera musculus          1.57
## 2 Reserva Nacional Pinguino de Humboldt Balaenoptera physalus          1.34
## 3 Chiloe                                Megaptera novaeangliae         1.00
## 4 Estrecho de Magallanes                Megaptera novaeangliae         1.42
## 5 Reserva Nacional Pinguino de Humboldt Megaptera novaeangliae         1.52

La función is.na() determina si un dato es NA (Not Available). El símbolo ! niega el el resultado. Por lo tanto, al usar !is.na(mean_shannon) estamos pidiendo por valor que no es NA en la columna mean_shannon.

5.7 Exportar datos

El trabajo hecho en una sesión de R (e.g., análisis, nuevas tablas, etc.) sólo existe en la memoria de R mientras la sesión esté abierta. Necesitas exportar los nuevos datos creados para archivarlos en tu computadora. Entonces, así como existe la función read_csv() para leer archivos CSV (comma-separated values) en R, hay una función para generar archivos CSV a partir de tablas de datos en contenidas como objetos en la memoria de R.

  • Por ejemplo, para exportar el data frame que creamos recién usando la función gather() y filter():
# Los argumentos básicos de write_csv() son primero indicar el objeto que se quiere exportar y luego la ruta y nombre del archivo de salida, incluyendo la extensión .csv
write_csv(data_gather, path = "data_output/data_gather.csv")