Ciencia de datos con R
Author

(Fernández‑Avilés & Montero, 2024, pp. 49-57)

3.6 Organización de datos con el tidyverse

3.6.1 El tidyverse y su flujo de trabajo

El tidyverse es, según se define en su propia página web,12 un conjunto de paquetes de R “opinables” diseñados para ciencia de datos. Las principales ventajas (opinables) de utilizar el tidyverse son tres:

  1. Utiliza una gramática, estructuras de datos y filosofía de diseño común.

  2. El flujo de trabajo es más fluido y, una vez se comprenden las ideas principales, más intuitivo.

  3. Para la mayoría de las operaciones, es computacionalmente más eficiente.

Uno de los paquetes más populares del tidyverse es ggplot2, que proporciona una “gramática de gráficos” (Wickham, 2016) y es una pieza clave del tidyverse actual, junto con los paquetes dplyr (gramática para la manipulación de datos) y tidyr (herramienta para crear datos tidy). El flujo de trabajo propuesto por el tidyverse se describe en el libro R for Data Science (Wickham & Grolemund, 2016) y se sintetiza en la Fig. 3.1.

Figura 3.1: Flujo de trabajo en ciencia de datos propuesto por el t i d y v e r s e . Fuente: Wickham and Grolemund (2016).

Figura 3.1: Flujo de trabajo en ciencia de datos propuesto por el t i d y v e r s e . Fuente: Wickham and Grolemund (2016).

Además del mencionado libro, la web del tidyverse (http://tidyverse.org) contiene toda la documentación de los paquetes, incluidos artículos para tareas concretas, que merece la pena leer alguna vez. En la web están también las conocidas como cheatsheets, algunas de ellas disponibles también en la ayuda de RStudio (menú Help/Cheatsheets).

Dentro del flujo de trabajo de la Fig. 3.1, ya se ha tratado la primera etapa (Import) en la Sec. 3.5.2. Es importante señalar que, al utilizar las funciones del tidyverse, los datos se organizan en objetos de clase tibble, que es una extensión del data.frame de R base. Las principales diferencias son:

  • Permite una representación compacta en la consola al mostrar la tabla de datos.

  • La selección con corchetes simples de una única variable siempre devuelve otro tibble (a diferencia de un data.frame, que devuelve un vector).

Se puede forzar a que una tabla de datos sea de un tipo u otro con las funciones as.data.frame (de tibble a data.frame) y as_tibble (de data.frame a tibble).

Siguiendo con el esquema de la Fig. 3.1, en este apartado se verán algunas tareas de las etapas Tidy (organizar) y Transform (transformar), que serán ampliadas en los Cap. 8 y 9. La visualización (Visualise) se tratará específicamente en el Cap. 11 y transversalmente en muchos otros. La modelización (Model) se trata extensamente en los capítulos de las partes IV a IX, y la comunicación (Communicate) se verá en los capítulos de la Parte X. Una de las características de la forma en que están programados los paquetes del tidyverse es que se puede trabajar13 con pipes.

El pipe es, básicamente, un operador compuesto de dos caracteres, |>, que se puede obtener con el atajo de teclado CTRL+MAYUS+M. El operador se pone en medio de dos expresiones de R, sean lado_izquierdo y lado_derecho las expresiones que se ponen a izquierda y derecha del pipe. Entonces se utiliza de la siguiente manera:

lado_izquierdo |> lado_derecho
NoteNota

El operador nativo de R|>, apareció en la versión R-4.1.0. Hay un operador alternativo que proviene del paquete magrittr%>%, que había que usar antes de esta versión, y mucha literatura y documentación está escrita usándolo. Hay diferencias, pero a los efectos de este capítulo ambos operadores se pueden utilizar indistintamente.

La expresión lado_izquierdo debe producir un valor, que puede ser cualquier objeto de R. La expresión lado_derecho debe ser una función, que tomará como primer argumento el valor producido en la parte izquierda. Si se desea guardar el resultado final, se debe asignar el resultado a algún nombre de objeto para que se almacene en el espacio de trabajo. La siguiente expresión sería un ejemplo de uso.

nombre_objeto <- lado_izquierdo |>
  lado_derecho

La ventaja de usar los pipes es que se pueden encadenar, de forma que el resultado de cada operación pasa a la siguiente expresión del pipeline (secuencia de operaciones con pipe), como en el siguiente ejemplo:

library("dplyr")
contam_mad |> colnames() |> length()
#> [1] 12

3.6.2 Transformación de datos con dplyr

En la gramática del tidyverse, dentro del paquete dplyr se dispone de una serie de “verbos” (funciones) para una sola tabla, que se pueden agrupar en tres categorías: para trabajar con filas, para trabajar con columnas y para resumir datos.

3.6.2.1 Operaciones con filas

Los verbos definidos para estas operaciones son:

  • filter(): elige filas en función de los valores de la columna.

    pm10 <- contam_mad |> 
      filter(nom_abv == "PM10")   # se filtra por PM10
  • arrange(): cambia el orden de las filas con algún criterio.

    zonas<- contam_mad |>
      arrange(desc(zona), daily_mean)
  • slice(): extrae filas por su índice. También hay una serie de funciones “asistentes” (helpers) para obtener los índices que se utilizan con frecuencia. Por ejemplo:

    • slice_head() y slice_tail() obtienen las primeras y últimas filas respectivamente (por defecto, una). Se puede especificar n (número) o prop (proporción) de filas.

    • slice_sample() obtiene una muestra aleatoria de n filas (o proporción prop).

    • slice_min(), slice_max() obtienen las filas que contienen los menores o mayores valores respectivamente de la variable indicada en el argumento order_by. Si no se especifica n o prop, se obtienen solo las filas que contienen el mínimo o el máximo. Nótese que puede haber más de una fila que cumpla la condición.

Véase el resultado de los siguientes ejemplos:

pm10 |> slice(10:15) # extrae filas desde la 10 a la 15
pm10 |> slice_tail(n = 3) # extrae las tres últimas filas
pm10 |> slice_max(order_by = daily_mean) # día con mayor valor medio de PM10
set.seed(1) # Para que la muestra aleatoria sea reproducible
pm10 |> slice_sample(n = 4) # muestra 4 registros

3.6.2.2 Operaciones con columnas

Los verbos definidos para estas operaciones son:

  • select(): indica cuando una columna se incluye o no. Se pueden utilizar helpers para seleccionar columnas que cumplan cierta condición (por ejemplo, ser numéricas) y también para “quitar” columnas de la selección (con el signo menos, [-]).

    pm10 |> select(longitud, latitud, daily_mean, tipo)
    pm10 |> select(where(is.numeric))
    pm10 |> select(-c(id:latitud))

En cuanto a la modificación de datos, existen múltiples posibilidades. Algunas de ellas son:

  • rename(): cambia el nombre de la columna.

  • mutate(): cambia los valores de las columnas y crea nuevas columnas. La función transmute() funciona igual que mutate(), pero la tabla de datos resultante solo contiene las nuevas columnas creadas.

  • relocate(): cambia el orden de las columnas.

pm10 |> rename(zona_calidad_aire = zona)
pm10 |> relocate(fecha, .before = estaciones)
pm10_na <- pm10 |> mutate(isna = is.na(daily_mean))

En este punto, es importante señalar que dentro de la función mutate() se puede usar cualquier función vectorizada para transformar las variables. Por ejemplo, se podría transformar una columna con las funciones as.xxx que se vieron en la Sec. 3.5.1, aplicar formatos a fechas o usar funciones del paquete lubridate para trabajar con este tipo de datos. A medida que se avance en el libro irán apareciendo aplicaciones que ahora, quizás, no sean tan evidentes.

3.6.2.3 Operaciones de resumen y agrupación

La primera operación de resumen que puede surgir es “contar” filas. La función tally() devuelve el número de filas totales de un data.frame. La función count() proporciona también este número; si, además, se pasa como argumento alguna variable, lo que devuelve es el número de filas para cada valor diferente de dicha/s variable/s. Estos recuentos se pueden añadir a la tabla de datos con las funciones add_count() y add_tally(), lo que permite calcular frecuencias absolutas y relativas fácilmente.

pm10 |> tally()
#>       n
#> 1 53794
pm10 |> count(zona)
#>            zona     n
#> 1: Interior M30 20690
#> 2:      Noreste 12414
#> 3:     Noroeste  4138
#> 4:      Sureste  8276
#> 5:     Suroeste  8276

La función summarise() (o, equivalentemente, summarize()) aplica alguna función de resumen a la/s variable/s que se especifiquen (mean(), max(), etc.). El paquete dplyr tiene algunas funciones de resumen adicionales, como n() (número de filas), n_distinct() (número de filas con valores distintos) y first(), last(), nth() (primero, último y n-ésimo valor, en el orden en el que se encuentran, respectivamente).

En muchas ocasiones, las operaciones de análisis se realizan en grupos definidos por alguna variable de agrupación. La función group_by() “prepara” la tabla de datos para realizar operaciones de este tipo. Una vez agrupados los datos, se pueden añadir operaciones de resumen como las vistas anteriormente. A veces hay que “desagrupar” los datos, para lo que se utiliza la función ungroup().

A continuación, se muestra una expresión un poco más compleja que las anteriores. En el conjunto de datos contam_mad del paquete CDR, se filtra por el nombre de contaminante “NOx”. Después se agrupan los datos por zona y se calculan algunos estadísticos de resumen para cada zona.

contam_mad |>  
  filter(nom_abv == "NOx") |> # se filtra por N0x
  group_by(zona) |>
  summarize(
    min = min(daily_mean, na.rm = TRUE),
    q1 = quantile(daily_mean, 0.25, na.rm = TRUE),
    median = median(daily_mean, na.rm = TRUE),
    mean = mean(daily_mean, na.rm = TRUE),
    q3 = quantile(daily_mean, 0.75, na.rm = TRUE),
    max = max(daily_mean, na.rm = TRUE)
  )
#> A tibble: 5 × 7
    zona            min    q1 median  mean    q3   max
    <chr>         <dbl> <dbl>  <dbl> <dbl> <dbl> <dbl>
#> 1 Interior M30 0.0833  32.4   54.1  72.9  90.0  759.
#> 2 Noreste      1       23.8   39.6  56.2  68.9  516.
#> 3 Noroeste     0       12.0   20.3  29.7  34.5  352.
#> 4 Sureste      0       29.1   45.4  64.6  77.2  453 
#> 5 Suroeste     0.667   33.5   59.6  90.5 114.   666.

3.6.3 Combinación de datos

En el apartado anterior se han tratado los “verbos” de una tabla. Es muy común que haya que combinar datos de distintas tablas, para lo cual se utilizan lo que el tidyverse considera two tables verbs. En esencia, para combinar tablas que contienen información relacionada, hay que saber cuáles son las columnas que se refieren a lo mismo, para hacer las uniones (joins) utilizando esas columnas. Hay cuatro tipos de uniones que se pueden realizar, usando las siguientes funciones:

  • inner_join(): se incluyen las filas de ambas tablas para las que coinciden las variables de unión.

  • left_join(): se incluyen todas las filas de la primera tabla y solo las de la segunda donde hay coincidencias.

  • right_join(): se incluyen todas las filas de la segunda tabla y solo las de la primera donde hay coincidencias.

  • full_join(): se incluyen todas las filas de las dos tablas.

Las funciones requieren como argumentos dos tablas de datos y la especificación de las columnas coincidentes. Si no se especifica, hace las uniones por todas las columnas coincidentes en ambas tablas. Para las filas que solo están en una de las tablas, se añaden valores NA donde no haya coincidencias.

A modo de ejemplo, las siguientes expresiones unen dos datasets para combinar datos de municipios con su renta. En el Cap. 8 se verán estas uniones en la práctica.

library("sf")
munis_renta <- municipios |>
  left_join(renta_municipio_data) |> 
  select(name, cpro, cmun, `2019`) 
#> Joining, by = "codigo_ine"

Otra forma de unir tablas es, simplemente, añadiendo columnas (que tengan el mismo número de filas) o filas (que tengan el mismo número de columnas). Para ello se usan las funciones bind_cols() y bind_rows(), respectivamente. Otra forma conveniente de añadir nuevas filas o columnas son las funciones add_row() y add_column(). Se pueden añadir antes o después de una fila/columna especificada con el argumento .before, y pasando los valores como pares “variable = valor” para cada variable en el conjunto de datos.

Como comentario final del paquete dplyr, una característica importante es que se pueden usar las funciones vistas sobre tablas de una base de datos, sin necesidad de utilizar sentencias SQL y con la ventaja de que las operaciones se realizan en el motor de la base de datos. En el Cap. 5 se tratarán las cuestiones relacionadas con los gestores de bases de datos y SQL.

3.6.4 Reorganización de datos

A lo largo del capítulo se ha visto la importancia de disponer los datos de forma rectangular, de forma que se tenga una columna para cada variable y una fila para cada observación. Algunas veces es conveniente reorganizar los datos más “a lo ancho” o más “a lo largo” de lo que se encuentran.

Para estas operaciones se utilizan las funciones pivot_longer() y pivot_wider() del paquete tidyr del tidyverse de la siguiente forma:

  • pivot_longer(): el argumento names_to asigna el nombre de la nueva variable que va a indicar de qué columna vienen los datos; y el argumento values_to asigna el nombre de la nueva variable que va a contener el valor de la tabla original.

  • pivot_wider(): el argumento names_from indica el nombre de la variable que contiene los nombres de las nuevas columnas a crear a lo ancho; y el argumento values_from indica el nombre de la variable que contiene los valores en la tabla original. Las observaciones deben estar identificadas de forma única por varias variables. Si no es el caso, se puede aplicar una función al estilo de las tablas dinámicas de las hojas de cálculo con el argumento values_fn.

NoteNota

Las funciones pivot_longer() y pivot_wider() admiten otros argumentos names_xx y values_xx para personalizar la forma de reestructurar los datos. En la mayoría de las ocasiones será suficiente con las comentadas (xx_from y xx_to). Si fuera necesario, se recomienda consultar la ayuda de las funciones, o la lectura del artículo sobre pivoting.

A modo de ejemplo, el conjunto de datos contam_mad tiene los datos “mezclados” de varias variables medioambientales en la columna daily_mean. La columna nom_abv contiene el parámetro al que se refiere la columna de datos. Entonces, interesa “extender” la tabla para tener cada parámetro en una columna, de forma que se pueda hacer un análisis de datos adecuado, como en el siguiente código:

library("tidyr")
extendida <- contam_mad |>
  pivot_wider(names_from = "nom_abv",
              values_from = "daily_mean",
              values_fn = mean)
colnames(extendida)
#>  [1] "estaciones" "id"         "id_name"    "longitud"  
#>  [5] "latitud"    "nom_mag"    "ud_med"     "fecha"     
#>  [9] "zona"       "tipo"       "BEN"        "SO2"       
#> [13] "NO2"        "EBE"        "CO"         "NO"        
#> [17] "PM10"       "PM2.5"      "TOL"        "NOx"

Se deja como ejercicio volver a obtener la tabla original usando la función pivot_longer() a partir del objeto extendida.

El paquete tidyr también contiene funciones para reorganizar las columnas de la tabla uniendo columnas con la función unite(), o separando una columna en dos o más con la función separate() (véanse los detalles en la ayuda de las funciones).

Para terminar este apartado de reorganización de datos, se da una primera aproximación al tratamiento de valores perdidos, que se abordará en el Cap. 8. En R, un valor perdido se representa por el valor especial NA (not available). Brevemente, las funciones más utilizadas en este campo son:

  • drop_na() del paquete tidyr: permite eliminar las filas que tienen valores perdidos en ciertas variables (o en cualquiera, si no se especifica ninguna).

  • replace_na(): sustituye los valores perdidos en cada variable por el valor especificado.

  • fill(): permite “rellenar” valores perdidos con los últimos encontrados.

Los datos de contaminación a menudo tienen muchos valores perdidos. La siguiente expresión elimina las filas del conjunto de datos contam_mad con valores perdidos y, después, cuenta las filas.

contam_mad |>  
  drop_na() |> # se omiten los NAs para el análisis
  count()
#>         n
#> 1: 505773
ImportantResumen
  • R es software libre y gratuito, mantenido por una enorme comunidad.

  • La forma de interactuar con R es mediante expresiones, que se escriben en scripts, y al ejecutarlas se obtienen los resultados.

  • Los objetos de datos que se vayan a usar deben estar en el espacio de trabajo.

  • RStudio es un “envoltorio” de R, y por tanto R tiene que estar instalado en el sistema para poder usar RStudio.

  • Los paquetes se instalan una sola vez, y deben cargarse con library() para usar sus funciones.

  • La tabla de datos o data.frame es la estructura de datos más adecuada para análisis de datos y cada columna es un vector.

  • El tidyverse es un conjunto de paquetes que facilita las tareas de análisis de datos.

  • El operador pipe, |>, permite “pasar” valores a funciones de forma encadenada.

  • Las operaciones básicas con una tabla son filtrado, selección y resumen.

  • Para crear nuevas columnas en las tablas de datos se usa la función mutate.

  • Para combinar tablas con columnas comunes se usan las funciones xx_join.