#¿Qué es el “tidyverse”?

Por varias razones debemos detenernos en un concepto fundamental en el uso moderno de R, que es el concepto de los datos tidy (ordenados) y los paquetes compilados en la llamada suite tidyverse.: En todos los foros y contenidos de aprendizaje modernos están sus funciones y comandos. Para desenvolverosen cualquier recurso sobre R que encontréis, es necesario que estéis familiarizados con su existencia. Tanto, que el módulo de gráficos más potente de este curso (el 6) está dedicado a esta filosofía de trabajo y sus herramientas asociadas.

Se trata de una filosofía particular acerca del tratamiento de los datos hecha de una manera fácil y eficiente en cuanto al código y lo intuitivo de algunas transformaciones que pueden ser conceptualmente más difícil de entender cuando escribimos el código. Podríamos definir las siguientes particularidades:

Esto es especialmente importante en las ciencias de la vida y las ciencias experimentales, donde muchas veces tendemos a almacenar los datos en columnas, por ejemplo poniendo en cada columna las observaciones de un año determinado, por ejemplo.

install.packages("tidyverse")

Al cargarlos, nos indica cuáles están cargados, cuáles no y si hay conflictos.

library(tidyverse)
## Warning: package 'tidyverse' was built under R version 3.6.3
## -- Attaching packages ------------------------------------------------------------------- tidyverse 1.3.0 --
## v ggplot2 3.2.1     v purrr   0.3.4
## v tibble  2.1.3     v dplyr   0.8.3
## v tidyr   1.0.0     v stringr 1.4.0
## v readr   1.3.1     v forcats 0.4.0
## Warning: package 'purrr' was built under R version 3.6.3
## -- Conflicts ---------------------------------------------------------------------- tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()

Uso de tidyverse para la lectura de archivos (readr)

Mediante el paquete readr se implementan nuevas funciones para la lectura de los archivos de una manera más eficaz e intenando evitar algunos de los problemas más frecuentes, como no reconocer correctamente algunos formatos, como las fechas. En cualquier caso, este procedimiento está concebido para poder leer archivos de datos muy masivos, por lo que solo notaréis la ventaja de adaptaros a este paquete si tenéis archivos de datos muy grandes.

A nivel de lenguaje, lo que hacen los programadores es crear funciones análogas a las básicas con nombres parecidos pero distinguibles: así, habiendo una función básica llamada read.table(), tenemos una función análoga llamada read_table() y otra llamada read_table2(). Igualmente para leer archivos/valores separados por comas (csv, comma separated values), se han creado, imitando a read.csv(), las funciones read_csv()y read_csv2().

Vamos a trabajar con un archivo de datos, biocrust, con cierto tamaño. Una apertura del archivo normal lo podemos hacer con read.table(), siempre y cuando nos aseguremos con el argumento header=TRUE que avisa que tenemos un encabezado cabecero. biocrust es un archivo de datos disponible del catedrático Fernando Maestre y corresponden a observaciones de la cobertura de costras biológicas y muchas variables climáticas, el sensor, y la fecha.

setwd("C:/Users/Alejandro/Documents/ISM/Datasets")
biocrust<-read.csv("C:/Users/Alejandro/Documents/ISM/Datasets/biocrust.csv",header=TRUE)
biocrust<-read.table("biocrust.txt",header=TRUE)

Y podemos ver qué tipo de datos tenemos dentro:

head(biocrust)
##   Event Sensor OTC RS    Cover      Date          t0   duration     H0
## 1     1      1   1  1 3.913785  3/1/2009  4:42:34 AM 7:30:00 AM 13.270
## 2     2      1   1  1 3.913785  3/2/2009 10:32:34 PM 5:50:00 AM 13.865
## 3     3      1   1  1 3.913785  3/4/2009 10:52:34 AM 2:00:00 AM 14.630
## 4     4      1   1  1 3.913785  3/5/2009  2:32:34 AM 2:30:00 AM 14.460
## 5     5      1   1  1 3.913785 3/28/2009  5:34:46 PM 2:29:59 AM 11.060
## 6     6      1   1  1 3.913785 3/30/2009  6:54:46 AM 6:20:00 AM 12.165
##     Hmax Ppt I10    Tmean
## 1 14.290 5.2 0.8 7.544286
## 2 15.480 5.2 0.4 6.861818
## 3 14.800 0.4 0.2 7.625000
## 4 14.800 2.6 0.4 4.530909
## 5 12.080 2.4 0.4 4.870000
## 6 12.845 5.2 0.6 3.003333
str(biocrust)
## 'data.frame':    9943 obs. of  13 variables:
##  $ Event   : int  1 2 3 4 5 6 7 8 9 10 ...
##  $ Sensor  : int  1 1 1 1 1 1 1 1 1 1 ...
##  $ OTC     : int  1 1 1 1 1 1 1 1 1 1 ...
##  $ RS      : int  1 1 1 1 1 1 1 1 1 1 ...
##  $ Cover   : num  3.91 3.91 3.91 3.91 3.91 ...
##  $ Date    : Factor w/ 440 levels "1/1/2014","1/1/2016",..: 261 270 295 298 287 292 307 313 313 317 ...
##  $ t0      : Factor w/ 392 levels "1:09:01 AM","1:09:01 PM",..: 212 43 48 134 245 289 280 381 323 145 ...
##  $ duration: Factor w/ 129 levels "1:00:00 AM","1:10:00 AM",..: 106 90 44 53 51 96 56 37 62 7 ...
##  $ H0      : num  13.3 13.9 14.6 14.5 11.1 ...
##  $ Hmax    : num  14.3 15.5 14.8 14.8 12.1 ...
##  $ Ppt     : num  5.2 5.2 0.4 2.6 2.4 5.2 3.8 0.6 3.8 1.4 ...
##  $ I10     : num  0.8 0.4 0.2 0.4 0.4 0.6 1 0.4 0.6 0.4 ...
##  $ Tmean   : num  7.54 6.86 7.62 4.53 4.87 ...

Las variables Date,t0 y duración, que expresan fecha y un dato horario están mal interpretadas porque me las identifica como factores con cientos de niveles y no como lo que son. Son este tipo de errores los que se intentan evitar con estas otras funciones:

Vamos a ver qué pasa si cargamos el archivo de nuevo, sobreescribiendo el objeto, con esta variable:

biocrust<-read_table("C:/Users/Alejandro/Documents/ISM/Datasets/biocrust.csv")
str(biocrust)
## Classes 'spec_tbl_df', 'tbl_df', 'tbl' and 'data.frame': 9943 obs. of  1 variable:
##  $ Event,Sensor,OTC,RS,Cover,Date,t0,duration,H0,Hmax,Ppt,I10,Tmean: chr  "1,1,1,1,3.913785499,3/1/2009,4:42:34 AM,7:30:00 AM,13.27,14.29,5.2,0.8,7.544285714" "2,1,1,1,3.913785499,3/2/2009,10:32:34 PM,5:50:00 AM,13.865,15.48,5.2,0.4,6.861818182" "3,1,1,1,3.913785499,3/4/2009,10:52:34 AM,2:00:00 AM,14.63,14.8,0.4,0.2,7.625" "4,1,1,1,3.913785499,3/5/2009,2:32:34 AM,2:30:00 AM,14.46,14.8,2.6,0.4,4.530909091" ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   `Event,Sensor,OTC,RS,Cover,Date,t0,duration,H0,Hmax,Ppt,I10,Tmean` = col_character()
##   .. )

Nos encontraremos con que no se ha identificado el separador de las columnas nos ha incluido todas juntas en una sola.

Una alternativa recomendada es acudir a la función read_delim() que puede tomar el argumento delim= para definir cuál es el separador de nuestros datos:

biocrust<-read_delim("C:/Users/Alejandro/Documents/ISM/Datasets/biocrust.csv",delim=",")
## Parsed with column specification:
## cols(
##   Event = col_double(),
##   Sensor = col_double(),
##   OTC = col_double(),
##   RS = col_double(),
##   Cover = col_double(),
##   Date = col_character(),
##   t0 = col_time(format = ""),
##   duration = col_time(format = ""),
##   H0 = col_double(),
##   Hmax = col_double(),
##   Ppt = col_double(),
##   I10 = col_double(),
##   Tmean = col_double()
## )

En este caso, automáticamente veremos el resultado de cómo se han interpretado las columnas: date y t0 sigue interpretándose mal, y duración y H0, que también es tiempo, están correctamente interpretadas.

Además vamos a ver una particularidad muy importante del tidyverse: la clase de datos es un marco de datos muy particular llamado tibble. Es esencialmente lo mismo, pero hay atributos adjudicados a cada columna. Así, se visualizarán de manera diferente por pantalla. No solo porque nos dará información sobre las columnas, sino porque automáticamente solo nos saldrán 6 datos por pantalla, ahorrándonos tener que usar head() para visualizar la cabecera. Con el tibble, esto se hace automáticamente por nosotros, porque nadie quiere ver todos los datos por la consola, que es lo que pasa si llamamos a un dataframe directamente.

class(biocrust)
## [1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame"
biocrust
## # A tibble: 9,943 x 13
##    Event Sensor   OTC    RS Cover Date  t0       duration    H0  Hmax   Ppt
##    <dbl>  <dbl> <dbl> <dbl> <dbl> <chr> <time>   <time>   <dbl> <dbl> <dbl>
##  1     1      1     1     1  3.91 3/1/~ 04:42:34 07:30:00 13.3  14.3    5.2
##  2     2      1     1     1  3.91 3/2/~ 22:32:34 05:50:00 13.9  15.5    5.2
##  3     3      1     1     1  3.91 3/4/~ 10:52:34 02:00:00 14.6  14.8    0.4
##  4     4      1     1     1  3.91 3/5/~ 02:32:34 02:30:00 14.5  14.8    2.6
##  5     5      1     1     1  3.91 3/28~ 17:34:46 02:29:59 11.1  12.1    2.4
##  6     6      1     1     1  3.91 3/30~ 06:54:46 06:20:00 12.2  12.8    5.2
##  7     7      1     1     1  3.91 4/10~ 06:34:46 02:50:00  9.10  9.78   3.8
##  8     8      1     1     1  3.91 4/14~ 09:34:46 00:40:00  9.36  9.53   0.6
##  9     9      1     1     1  3.91 4/14~ 19:54:46 03:20:00  9.44  9.96   3.8
## 10    10      1     1     1  3.91 4/16~ 02:44:46 01:30:00 11.1  11.1    1.4
## # ... with 9,933 more rows, and 2 more variables: I10 <dbl>, Tmean <dbl>

Si tenemos un archivo .csv, que típicamente separa las columnas por comas, podremos recurrir a la función read_csv() ya que es la función que sustituye a `read.csv(). Como es una función específica para datos con las columnadas separadas por comas, en general no debería ser necesario especificar ese argumento.

Lo vamos a probar con el archivo “Abies.csv” que contiene datos reproductivos de abetos, cada año puesto en una columna. V

abies<-read_csv("C:/Users/Alejandro/Documents/ISM/Datasets/abies.csv")
## Parsed with column specification:
## cols(
##   Var = col_character(),
##   Wave = col_character(),
##   Tree_name = col_character(),
##   `1999` = col_double(),
##   `1998` = col_double(),
##   `1997` = col_double(),
##   `1996` = col_double(),
##   `1995` = col_double(),
##   `1994` = col_double(),
##   `1993` = col_double(),
##   `1992` = col_double(),
##   `1991` = col_double(),
##   DBH = col_double()
## )

Veremos que los datos identificados en las columnas cuyo nombre son los años se van a identificar como dos tipos de datos numéricos diferentes. Esto es así porque estas funciones van a evaluar X filas para intentar averiguar qué datos son. Algunas podemos considerar que tampoco están bien identificadas, porque la primera, Var, podría ser un factor, más que un texto.

Manipulación de datos con tidyverse (dplyr)

La función select() es uno de los ejemplos típicos del tidyverse, y permite seleccionar columnas determinadas. Unas veces no será más útil que la notación normal de corchetes, pero a veces sí, porque incorpora argumentos muy útiles a veces. Por ejemplo, starts_with nos permite seleccionar columnas que empiezan de una determinada manera. Como esta, hay muchas.

El argumento obligatorio es el dataframe o el tibble (funcionará con ambos tipos):

select(abies,starts_with("199"))
## # A tibble: 371 x 9
##    `1999` `1998` `1997` `1996` `1995` `1994` `1993` `1992` `1991`
##     <dbl>  <dbl>  <dbl>  <dbl>  <dbl>  <dbl>  <dbl>  <dbl>  <dbl>
##  1      0      0      0      0      0     19      0      0      0
##  2      0     22      0      0      0     20      0      0      0
##  3      0     22      0      0      0     18      0      0      0
##  4      0     17      0      0      0     51      0      0      0
##  5      0      2      0      0      0      3      0      0      0
##  6      0      0      0      0      0      0      0      0      0
##  7      0      0      0      0      0     21      0      0      0
##  8      0     54      0      0      0     60      0      0      0
##  9      0      0      0      0      0     37      0      0      0
## 10      0     44      0      0      0     48      0      0      0
## # ... with 361 more rows

Otra manera de utilizar la función es expresar los nombres de las columnas sin comillas. Vamos a quedarnos solo con la columna DBH, diameter at the breast height.

select(abies,DBH)
## # A tibble: 371 x 1
##      DBH
##    <dbl>
##  1   9.4
##  2  10.6
##  3   7.7
##  4  10.6
##  5   8.7
##  6  10.1
##  7   8.1
##  8  11.6
##  9  10.1
## 10  13.3
## # ... with 361 more rows

Concepto de tidy data o “datos limpios” o largos

Como comentábamos, una manera en la que corrientemente almacenamos nuestros datos recogidos en campo, y esto ocurre especialmente en las ciencias experimentales, es ir almacenando columna a columna los datos recogidos para cada sujeto observado. Sin embargo se considera que no es el formato más adecuado. La filosofía tidyverse requiere como decíamos, alojar cada observación del mismo tipo de dato en filas diferentes. Las 9 columnas 1999-1991 tienen todas el mismo tipo de datos u observaciones (número de conos). Por tanto hay que pasar cada dato a una fila diferente. Entonces tendremos, por cada árbol, ya no una sola fila sino 9, conteniendo en cada fila un solo dato del número de conos, acompañado por una nueva columna que especifique a qué año corresponde el dato. Y el contenido de las variables Var, Wave, Tree_name y DBH repetirán los datos fila a fila para ese árbol en concreto. Con el resultado lo veremos más claro. En otra sección del curso veremos otro ejemplo con los datos de emisiones de gases de efecto invernadero inventariados por el Banco Mundial y que tenéis en vuestro directorio de trabajo, como GHG_wide.csv. (Greenhouse Gases en formato wide o ancho)

emisiones<-read_delim("C:/Users/Alejandro/Documents/ISM/Datasets/GHG_wide.csv",delim=",")
## Parsed with column specification:
## cols(
##   .default = col_double(),
##   Country = col_character(),
##   `2015` = col_logical(),
##   `2016` = col_logical(),
##   `2017` = col_logical(),
##   `2018` = col_logical(),
##   `2019` = col_logical()
## )
## See spec(...) for full column specifications.

La función del tidyverse para realizarlo es pivot_longer() y hay que definirle varios parámetros o argumentos:

Para decirle qué columnas son las que tienen que agruparse en una sola, le decimos que van a ser desde la columna número 2 hasta la última. El número de la última columna, coincide, evidentemente, con el número de columnas de nuestros datos y lo podemos averiguar de manera sencilla con la funcion ncol().

emisiones_long<-pivot_longer(data=emisiones,cols=2:ncol(emisiones),names_to="year",values_to="CO2e")

Otras manipulaciones de datos

A continuación vamos a ver la función filter(), para quedarnos solo con aquellas filas que tengan datos que satisfagan una condición. Esta función es idéntica a la función básica subset() de R básico. No presenta grandes novedades salvo si los archivos son muy pesados. La sintaxis de la función es muy parecida: se especifican los datos de la función y luego la condición.

En el siguiente ejemplo queremos quedarnos solo con los datos de emisiones que sean de la variedad España. Notad que le estamos indicando la condición del argumento con un doble símbolo =, es decir, ==. Esta es la manera de preguntar si un dato es igual a otro en R. Tenéis todas las operaciones lógicas de este tipo (distinto que, menor, etc.) en el de programación.

filter(emisiones_long,Country=="Spain")
## # A tibble: 60 x 3
##    Country year   CO2e
##    <chr>   <chr> <dbl>
##  1 Spain   1960   1.61
##  2 Spain   1961   1.75
##  3 Spain   1962   1.94
##  4 Spain   1963   1.88
##  5 Spain   1964   2.04
##  6 Spain   1965   2.23
##  7 Spain   1966   2.41
##  8 Spain   1967   2.65
##  9 Spain   1968   2.93
## 10 Spain   1969   2.90
## # ... with 50 more rows

Resúmenes de datos en tidyverse

Finalmente vamos a ver la función aggregate() de R básico y su análogo en tidyverse, group_by(). Estas funciones se usan para recalcular los datos para agruparlos en formatos más resumidos. Por ejemplo, si tenemos los datos diarios de temperatura de una estación meteorológica de todo el año, de manera que tenemos un registro diario por fial, pero queremos solamente la media de cada mes.

El objetivo no es que conozcáis el procedimiento del tidyverse sino que existe este procedimiento en general. De hecho, recomendamos la función básica aggregate() por ser más sencilla en su sintaxis.

Vamos a ver un ejemplo práctico con los datos de gases de efecto invernadero en formato largo. Los tenemos desagregados por países, queremos ahora los datos año a año pero del total mundial.

La función aggregate() funcionará con una fórmula, que vamos ver varias veces en otros módulos. La manera de especificar la fórmula es la siguiente:

GHG_total<-aggregate(CO2e~year,data=emisiones_long,FUN=sum)
head(GHG_total)
##   year     CO2e
## 1 1960 394.0419
## 2 1961 418.0022
## 3 1962 440.2820
## 4 1963 543.4637
## 5 1964 592.9685
## 6 1965 617.2565
tail(GHG_total)
##    year     CO2e
## 50 2009 1176.756
## 51 2010 1208.117
## 52 2011 1198.970
## 53 2012 1244.811
## 54 2013 1224.046
## 55 2014 1222.509

Nos ha sumado todos los datos correspondientes a cada año que ha encontrado.

La función más actual que tiene el tidyverse en su paquete dplyr es summarise(). Pero si lo que se quiere es obtener resúmenes de grupos, como en el caso de arriba, años, hay que usar una función muy importante: group_by(). Esta función, como su nombre indica, nos hace agrupaciones dentro de nuestro marco de datos. Recordad que nuestro marco de datos de tidyverse es un tibble, y que es especial, porque tiene más funcionalidades. Una de ellas es recordar si se le ha hecho una agrupación de los datos. Si la hacemos, va a marcar todas las manipulaciones que hagamos de los datos. Si por ejemplo volvemos a pedir una suma, nos sacará una suma por grupo.

Importante: hay que eliminar antes los valores

emisiones_long<-na.omit(emisiones_long)
GHG_total<-as.tibble(emisiones_long)
## Warning: `as.tibble()` is deprecated, use `as_tibble()` (but mind the new semantics).
## This warning is displayed once per session.
GHG_total<-group_by(emisiones_long,year)

GHG_total<-summarise(GHG_total,total=sum(CO2e))