#¿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:
A pesar de todas las ventajas que se le reconocen y que es un lenguaje líder, tiene ya muchos años y hay algunos procedimientos que pueden ser hechos de manera más liviana y fácil. Especialmente cuando tenemos acceso a más datos y de mayor tamaño.
Esta filosfía es lanzada precisamente por los creadores de Rstudio (muchos de los desarrolladores son los mismos), y que iréis comprobando que está diseñado para facilitar todo lo posible escrbiir el código (identifica errores, pone paréntesis automáticamente, anticipa qué es lo que se va a escribir, tiene atajos realmente útiles…).
Tidyverse
se trata de un conjunto de paquetes, una suite diseñados para trabajar juntos correctamente sobre el tratamiento de los datos de todo tipo: así, hay un modo particular de visualización de gráficos en el paquete ggplot, fechas y otros datos temporales en lubridate, cambios en la estructura de los datos con tidyr, etc. pero también paquetes para programar mejor, leeer mejor y más rápido archivos muy grandes, etc intentando reconocer qué tipo de datos está leyendo en cada celda.
El ideador es uno de los analistas de Rstudio, que ha trabajado muy intensamente en las sucesivas versiones de tidyverse, Hadley Wickham: [!http://hadley.nz/]. Es muy recomendable seguirle en redes sociales.
En cuanto a su filosofía tidy que le atribuye el nombre, se trata de una aproximación al tratamiento de los datos que entiende que hay un formato o estructura de los datos que es particularmente útil para poder construir los comandos de R de manera más sencilla. De manera resumida, se trata de intentar tener el menor número de variables posible, y que la información sobre las observaciones esté expandida caso a caso, fila a fila, en archivos de datos muy largos pero con menos columnas, menos anchos. Lo veremos un poco más adelante en un ejercicio, pero sobre todo cuando Julia os enseñe la sintaxis de ggplot
.
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.
También considera que podemos reducir el tiempo que dedicamos a escribir líneas de código mediante opciones de lenguaje más sencillas. Así, se ha desarrollado un elemento nuevo, el pipe
o tubería, que se escribe %>%
y cambia todo el sistema de lenguaje de R. Está tan implementado su uso dada su utilidad, que le hemos dedicado un vídeotutorial entero para que la entendáis. De lo contrario, mucha de la información que disponéis ahí fuera os resultará extraña si recurren a este símbolo.
Tidyverse puede funcionar como un solo paquete: No es necesario instalar y cargar paquetes uno a uno sino todos a la vez, salvo algunos paquetes periféricos como lubridate
(que es excelente para trabajar con fechas). Y por supuesto, todas la extensiones que la comunidad ha desarrrollado más adelante para ampliar el compendio de opciones. Por ejemplo, para generar animaciones gráficas o mejorar la visualización de nuestros análisis estadísticos. Con instalar y cargar tidyverse
estaríamos cargando el conjunto entero.
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()
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.
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
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:
Los datos a transformar (emisiones);
El argumento cols=
nos pide las columnas de donde vienen los datos a juntar en una sola. Es imprescindible porque si no, no sabríamos a qué año corresponde el número toneladas de gases.
El argumento names_to=
nos pide el nombre de la nueva variable que va a alojar los valores del dato que hemos observado, es decir, las toneladas de emisiones de gases.
values_to=
El nombre de la variable/columna donde se van a guardar los datos que antes tenía en cada columna. El nombre elegido es por ejemplo CO2e, de CO2 equivalente, la medida estándar de los gases de efecto invernadero.
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")
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
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:
la variable a agrupar en función de otras variables. Pueden ser una o varias, pero vamos a ver la diferencia de incluirlas todas o solo algunas. La manera de decir “en función de” es con el símbolo ~. Esto es así también para los modelos estadísticos que veremos.
Especificar el dataframe del que vamos a agrupar los datos con el argumento data=
. Esto es común a otros procedimientos como los estadísticos.
FUN= indica el cálculo que queremos hacer. Podemos usar otros como mean o sd para la media o la desviación típica.
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))