TIDYVERSE

Danilo Verdugo (dynageo@gmail.com)

Ciencia de Datos

La ciencia de datos es una disciplina que permite transformar datos sin procesar (o crudos) en comprensión, perspectiva y conocimiento.

El Ciclo Básico

IMPORTAR > ORDENAR > TRANSFORMAR > VISUALIZAR > COMUNICAR

R para ciencia de Datos

Tidyverse es una colección de paquetes diseñados para la ciencia de datos. Todos los paquetes comparten:

  • Filosofía de diseño

  • Gramática

  • Estructuras de datos subyacentes

Para instalar el tidyverse completo:

install.packages("tidyverse")
library(tidyverse)

Datos tidy

Los paquetes de Tidyverse trabajan con datos tidy: ordenados, organizados.

los datos tidy deben cumplir con tres características:

  • Cada variable debe tener su propia columna.
  • Cada observación debe tener su propia fila.
  • Cada valor debe tener su propia celda.
head(mpg)
# A tibble: 6 × 11
  manufacturer model displ  year   cyl trans      drv     cty   hwy fl    class 
  <chr>        <chr> <dbl> <int> <int> <chr>      <chr> <int> <int> <chr> <chr> 
1 audi         a4      1.8  1999     4 auto(l5)   f        18    29 p     compa…
2 audi         a4      1.8  1999     4 manual(m5) f        21    29 p     compa…
3 audi         a4      2    2008     4 manual(m6) f        20    31 p     compa…
4 audi         a4      2    2008     4 auto(av)   f        21    30 p     compa…
5 audi         a4      2.8  1999     6 auto(l5)   f        16    26 p     compa…
6 audi         a4      2.8  1999     6 manual(m5) f        18    26 p     compa…

Pipes

Las funciones de Tidyverse pueden encadenarse a través del operador pipe (tubo), ya sea el del paquete magrittr (%>%) o el del paquete base de R (|>). Los procesos se enlazan con pipes para formar pipelines (tuberías).

mpg |> select(manufacturer)

Data Espacial

La integración de la filosofía “Tidyverse” con los datos espaciales ha dado lugar a lo que a veces se llama “Tidy Spatial Data Science”.

Paquete sf (Simple Features)

Durante años, el paquete estándar en R fue sp.

Potente, pero complejo: trataba los datos espaciales como objetos extraños y difíciles de manipular.

El paquete sf (Edzer Pebesma) cambió las reglas del juego al adoptar la filosofía tidy.

Claves

  • Dataframes como base: En sf, un mapa es simplemente un dataframe (una tabla).

  • La columna geometry: Es una “columna lista” especial (generalmente llamada geometry) que contiene las coordenadas.

  • Compatibilidad total: Como es un dataframe, puedes usar todos los verbos de dplyr (select, filter, mutate, group_by) directamente sobre tus mapas.

Clave: “Los datos espaciales no son especiales; son solo datos con atributos geométricos”.

El Ecosistema Tidy Espacial

La filosofía tidy no viene sola; trae consigo una familia de herramientas diseñadas para trabajar en armonía:

Área Paquete Principal Descripción Tidy
Vectores sf Maneja puntos, líneas y polígonos. Permite usar el operador “pipe” (%>% o |>
Rasters stars / terra stars Cubos de datos espaciotemporales. (terra muy rápido, se integra bien).
Área Paquete Principal Descripción Tidy
Visualización ggplot2 Incluye geom_sf(), detecta automáticamente la geometría y proyección, permitiendo crear mapas complejos con la gramática de gráficos habitual.
Área Paquete Principal Descripción Tidy
Mapas Interactivos tmap / leaflet tmap (Thematic Maps) utiliza una sintaxis por capas muy similar a ggplot2, ideal para la filosofía tidy.

Ventajas de la filosofía “Tidy” en lo espacial

  1. Legibilidad: El código se lee como una oración.

    • Antes: Tenías que extraer coordenadas, unir tablas con IDs complejos y luego plotear.

    • Ahora: leer_datos() |> filtrar(region == "X") |> calcular_area() |> plotear()

  1. Manipulación de atributos: Puedes hacer joins espaciales (unir datos por posición) con la misma facilidad que un left_join normal.
  1. Consistencia: No tienes que aprender una sintaxis para tus datos estadísticos y otra diferente para tus datos geográficos.

Ejemplo

Se necesita filtrar un mapa de ciudades para dejar solo las grandes y luego dibujarlas.

Sin filosofía Tidy (Estilo antiguo/base):

# Requería indexación compleja y gestión de objetos S4 
ciudades_grandes <- ciudades[ciudades$poblacion > 100000, ] 
plot(ciudades_grandes) 

Con filosofía Tidy (sf + dplyr + ggplot2):

ciudades %>%   
filter(poblacion > 100000) %>%   
ggplot()

El futuro: La evolución continúa

Actualmente, el ecosistema está madurando hacia el manejo de Big Data espacial (usando paquetes como arrow junto con sf) y la integración fluida con bases de datos espaciales (PostGIS) sin salir del entorno de RStudio.

IMPORTAR

Carga de datos.

CSV

#archivo local
datos <- read_csv("data/students.txt")
#desde una URL
datos <- read_csv("https://pos.it/r4ds-students-csv")
datos <- read_csv("data/students.txt", na = c("N/A", ""))

Escritura

write_csv(datos, "/data/datos-1.csv")

VECTORIAL: Geodata

El paquete geodata es el sucesor moderno para descargar datos (reemplazando al antiguo getData de raster). Las fuentes de datos para el paquete se encuentran en Fuentes de geodata.

Sin embargo, geodata descarga los objetos en formato SpatVector (del paquete terra), que es muy rápido pero no sigue estrictamente la lógica de dataframe que necesita el tidyverse.

Por tanto, el paso crucial es la conversión.

Paso 1: Cargar las librerías necesarias

Necesitas tres paquetes:

  1. geodata: Para descargar los mapas de GADM.

  2. sf: Para convertir el mapa a formato tidy (Simple Features).

  3. tidyverse: Para manipular los datos (dplyr, ggplot2).

library(geodata)   # Descarga de datos 
library(sf)        # Manejo espacial tidy 
library(tidyverse) # Manipulación y gráficos 
library(tidyterra) # Métodos y gráficos para objetos 'terra'

Paso 2: Descargar datos con gadm()

El proyecto gadm.org se encarga de organizar y distribuir los esfuerzos para mantener una base de datos actualizada y completa de archivos digitales vectoriales de los límites internacionales y político administrativos:

La función gadm() descarga los límites. Sus argumentos clave son:

  • country: El código ISO del país (ej. “ESP” para España, “CHL” para Chile, “ARG” para Argentina).
  • level: El nivel de detalle administrativo.

    • 0: Límite país.

    • 1: Estados/Regiones/Comunidades Autónomas.

    • 2: Provincias/Municipios (depende del país).

    • 3: Distrito/Comuna (depende del país).

    • 4: Canton (ej. Francia).

    • 5: Commune (ej. Francia).

  • path: Carpeta donde se guardarán los archivos (para no descargarlos cada vez).

# Descargamos nivel 1 (Regiones/Estados) de un país
# Esto devuelve un objeto "SpatVector" (no es tidy aún) 
mapa_crudo <- gadm(country = "ARG", 
                   level = 1, 
                   path = tempdir()) 

Un paquete muy potente y útil es rvest para facilitar la descarga y luego manipulación de HTML y XML

Códigos Iso

library(rvest)

html = read_html('https://www.iban.com/country-codes')
isos = html |> 
  html_elements("tr") |> 
  html_elements("td") |> 
  html_text() |>
  matrix(ncol = 4, byrow = TRUE) |>
  as_tibble()

Paso 3: El Puente a Tidy (st_as_sf)

Aquí ocurre la magia. Transformamos el objeto opaco de terra en un objeto sf (que es un dataframe con geometría).

# Convertimos a Simple Feature (sf) 
mapa_tidy <- st_as_sf(mapa_crudo)  
# Podemos ver que es una tabla 
glimpse(mapa_tidy) 
Rows: 24
Columns: 12
$ GID_1     <chr> "ARG.1_1", "ARG.2_1", "ARG.3_1", "ARG.4_1", "ARG.5_1", "ARG.…
$ GID_0     <chr> "ARG", "ARG", "ARG", "ARG", "ARG", "ARG", "ARG", "ARG", "ARG…
$ COUNTRY   <chr> "Argentina", "Argentina", "Argentina", "Argentina", "Argenti…
$ NAME_1    <chr> "Buenos Aires", "Catamarca", "Chaco", "Chubut", "Ciudad de B…
$ VARNAME_1 <chr> "Baires|Buenos Ayres", NA, "El Chaco|Presidente Juan Peron",…
$ NL_NAME_1 <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ TYPE_1    <chr> "Provincia", "Provincia", "Provincia", "Provincia", "Distrit…
$ ENGTYPE_1 <chr> "Province", "Province", "Province", "Province", "Federal Dis…
$ CC_1      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ HASC_1    <chr> "AR.BA", "AR.CT", "AR.CC", "AR.CH", "AR.DF", "AR.CB", "AR.CN…
$ ISO_1     <chr> "AR-B", "AR-K", "AR-H", "AR-U", NA, NA, "AR-W", NA, "AR-P", …
$ geometry  <GEOMETRY [°]> MULTIPOLYGON (((-62.10181 -..., POLYGON ((-65.14583…

Sistemas coordenados

Hasta ahora, todos los datos se encuentran en coordenadas geográficas (latitud/longitud) expresadas en valores angulares.

Si intentamos, por ej. hacer un buffer de \(500\) metros sobre un mapa que está en grados, cometemos uno de los errores más comunes y frustrantes en R: el resultado suele ser deforme o minúsculo porque R interpreta \(500\) como grados, no como metros.

En la filosofía Tidyverse/sf, manejar proyecciones es explícito y fluye a través de las “tuberías” (pipes).

¿Por qué convertir?

Los datos de geodata (GADM) vienen por defecto en un sistema geográfico (WGS84).

  • Unidades: Grados decimales (Latitud/Longitud).
  • Forma: Esferoide (el mundo curvo).
  • Problema: No puedes medir distancias planas (metros) ni áreas con precisión (ni facilidad!) en una esfera deformada.

Necesitamos aplanar el mapa proyectándolo a un sistema métrico (como UTM o proyecciones nacionales).

Código: EPSG y st_transform()

En el mundo sf, usamos códigos EPSG (un número corto que identifica la proyección) y la función st_transform().

  • EPSG:4326: WGS84 (El estándar mundial de GPS/GADM). Unidad: Grados.
  • EPSG:3857: Pseudo-Mercator (Google Maps). Unidad: Metros (pero distorsiona áreas).
  • EPSG Locales: Depende del país (ej. ETRS89 para España, MAGNA para Colombia, SIRGAS para Chile). Unidad: Metros (alta precisión).

Preparemos nuestros datos para cálculos geométricos.

PASO 1. Recuperamos los datos y consultamos su “ADN” espacial. (st_crs)

#Conversión a Tidy (sf)
mapa_sf <- st_as_sf(mapa_crudo)

#Diagnóstico: ¿En qué sistema estamos?
st_crs(mapa_sf)$input
[1] "WGS 84"
# Debería decir "WGS 84" o algo similar.
# Nota importante: Si miras el dataframe, la columna 'geometry' muestra números como -77.7, -6.9 (Grados).
plot(mapa_sf$geometry)

PASO 2. La Transformación (st_transform)

Supongamos que queremos calcular áreas en metros cuadrados. Necesitamos pasar a una proyección por ej. UTM.

Aquí es donde brilla el estilo tidy: la transformación es solo un paso más en la cadena.

mapa_metros <- mapa_sf |>
  # Seleccionamos columnas de interés para limpiar
  select(nombre = NAME_1) |>
  # TRANSFORMACIÓN: Aquí ocurre la magia
  st_transform(crs = 32717) 

# Verificamos el cambio
glimpse(mapa_metros)
Rows: 24
Columns: 2
$ nombre   <chr> "Buenos Aires", "Catamarca", "Chaco", "Chubut", "Ciudad de Bu…
$ geometry <GEOMETRY [m]> MULTIPOLYGON (((2104164 533..., POLYGON ((2059871 67…
# Mira la columna 'geometry': ¡Ahora los números son gigantes! 
# (ej. 2059871 6723807). Son metros, no grados.

Una vez que tienes mapa en tidy, olvida que estás trabajando con mapas complejos. Piensa que es un Excel.

Escritura

st_write(mapa_metros, "data/mapa_metros.shp", append = FALSE)
Deleting layer `mapa_metros' using driver `ESRI Shapefile'
Writing layer `mapa_metros' to data source 
  `data/mapa_metros.shp' using driver `ESRI Shapefile'
Writing 24 features with 1 fields and geometry type Unknown (any).

RÁSTER: Elevación

Para mantener la filosofía Tidyverse mientras trabajamos con rasters (ej. datos de elevación), utilizaremos el paquete terra como motor de cálculo, ya que es mucho más rápido y moderno que el antiguo paquete raster, y se integra mejor con este flujo.

Necesitamos cargar elevatr para descargar el DEM (Modelo Digital de Elevación) y terra para procesarlo.

library(elevatr)
library(terra)

La fuente de datos para elevación, se encuentran detalladas en:

Fuente de Datos

El Flujo Tidy: Descarga y Procesamiento

Vamos a encadenar las operaciones. Nota cómo usamos rast() inmediatamente después de descargar para convertir el objeto al formato moderno de terra.

NOTA: usamos el objeto proyectado!!

# z = zoom (1 a 14). Un z=6 es ligero y bueno para ejemplos nacionales.
mapa_dtm = get_elev_raster(locations = mapa_metros, 
                           z = 2, 
                           src = "aws", 
                           clip = "locations", 
                           tmp_dir = tempdir(), 
                           ncpu = ifelse(future::availableCores() > 2, 2, 1)) |>
  rast()
mapa_dtm
class       : SpatRaster 
size        : 263, 160, 1  (nrow, ncol, nlyr)
resolution  : 14134.35, 14145.64  (x, y)
extent      : 1034997, 3296492, 3790062, 7510367  (xmin, xmax, ymin, ymax)
coord. ref. : +proj=utm +zone=17 +south +datum=WGS84 +units=m +no_defs 
source(s)   : memory
name        : file1359a5ae401c9 
min value   :              -439 
max value   :              5843 
# Verificamos nombres (importante para el extract más adelante)
names(mapa_dtm) <- "elevacion"

Transformación de un objeto ráster:

mapa_geo <- mapa_dtm |>
  project("+proj=longlat +datum=WGS84")

plot(mapa_geo)

Escritura

writeRaster(mapa_geo, "data/mapa_geo.tif", overwrite = T, NAflag=255)

RASTER: Sentinel-2

CDSE

API del ecosistema de datos de Copernicus

Este paquete proporciona la interfaz para la API del Ecosistema Espacial de Datos de Copernicus, principalmente para buscar en el catálogo de datos disponibles de las misiones Sentinel de Copernicus y obtener imágenes del área de interés según las bandas espectrales seleccionadas.

https://zivankaraman.github.io/CDSE/index.html

https://shapps.dataspace.copernicus.eu/dashboard/#/account/settings

Existe el paquete CDSE para trabajar con imágenes.

library(CDSE)
GetCollections(as_data_frame = TRUE)
                     id                   title
1        sentinel-2-l1c          Sentinel 2 L1C
2    sentinel-3-olci-l2      Sentinel 3 OLCI L2
3         landsat-ot-l1 Landsat 8-9 OLI-TIRS L1
4       sentinel-3-olci         Sentinel 3 OLCI
5      sentinel-3-slstr        Sentinel 3 SLSTR
6 sentinel-3-synergy-l2   Sentinel 3 Synergy L2
7        sentinel-1-grd          Sentinel 1 GRD
8        sentinel-2-l2a          Sentinel 2 L2A
9        sentinel-5p-l2    Sentinel 5 Precursor
                                                                   description
1                                     Sentinel 2 imagery processed to level 1C
2                 Sentinel 3 data derived from imagery captured by OLCI sensor
3                        Landsat 8-9 Collection 2 imagery processed to level 1
4                                   Sentinel 3 imagery captured by OLCI sensor
5                                  Sentinel 3 imagery captured by SLSTR sensor
6 Sentinel 3 data derived from imagery captured by both OLCI and SLSTR sensors
7                                     Sentinel 1 Ground Range Detected Imagery
8                                     Sentinel 2 imagery processed to level 2A
9                      Sentinel 5 Precursor imagery captured by TROPOMI sensor
                 since            instrument  gsd bands constellation long.min
1 2015-11-01T00:00:00Z                   msi   10    13    sentinel-2     -180
2 2016-04-17T11:33:13Z                  olci  300    NA          <NA>     -180
3 2013-03-08T00:00:00Z oli/tirs/oli-2/tirs-2   30    17          <NA>     -180
4 2016-04-17T11:33:13Z                  olci  300    21          <NA>     -180
5 2016-04-17T11:33:13Z                 slstr 1000    11          <NA>     -180
6 2016-04-17T11:33:13Z            olci/slstr  300    NA          <NA>     -180
7 2014-10-03T00:00:00Z                 c-sar   NA    NA    sentinel-1     -180
8 2016-11-01T00:00:00Z                   msi   10    12    sentinel-2     -180
9 2018-04-30T00:18:50Z               tropomi 5500    NA          <NA>     -180
  lat.min long.max lat.max
1     -56      180      83
2     -85      180      85
3     -85      180      85
4     -85      180      85
5     -85      180      85
6     -85      180      85
7     -85      180      85
8     -56      180      83
9     -85      180      85
oclient = GetOAuthClient(id = 'sh-0cb438ad-5ed4-4419-b3ef-c86e896049ed',
                         secret = 'wyovOi29kgwKiCiNYuiTdYFkTqNb9vum')
#'sh-d484c1de-042d-4910-b7d3-1b2afd85c537'
#'NYiVMEkLdOHXkairtDhqZs2L1yKIDkSq'

Datos que vamos a utilizar:

https://drive.google.com/drive/folders/1wv5MpMv8BEcBy80ue80C2eSA29kBBgQw?usp=drive_link

raw_script_text <- paste(readLines("data/UCIBands.js"), collapse = "\n")

Seleccionar un Estado.

chaco = mapa_sf |>   
  filter(NAME_1 == "Chaco") 
plot(chaco$geometry)
cutt = st_make_grid(chaco,n=c(35,45),crs = 4326)
#cutt = st_as_sf(cutt)

plot(chaco$geometry, col="red")
plot(cutt,add=T)
ct = cutt[100,]
images <- SearchCatalog(aoi = ct, from = "2025-10-01", to = "2025-12-31",
                        collection = "sentinel-2-l2a", with_geometry = TRUE,
                        client = oclient)
range(images$areaCoverage)
[1] 100 100
range(images$tileCloudCover)
[1]   0 100
img = images[images$tileCloudCover<10,]

day = img$acquisitionDate[3]


ras <- GetImage(aoi = ct, time_range = day, script = raw_script_text,
                       collection = "sentinel-2-l2a", format = "image/tiff",
                       mosaicking_order = "mostRecent", resolution = 10,
                       mask = T, buffer = 1, client = oclient)

plotRGB(ras,r=4,g=3,b=2,stretch="lin")
#writeRaster(ras, "data/mapa_tmp.tif", overwrite = T, NAflag=255)

ORDENAR

Paquete dplyr de Tidyverse es la gramática para la manipulación de datos.

Un conjunto consistente de verbos que ayuda a solucionar los retos de procesamiento de datos más comunes.

Los principales “verbos” (funciones) de esta gramática son:

  • select(): selecciona columnas (variables) con base en sus nombres.
  • filter(): selecciona filas (observaciones) con base en sus valores.
  • arrange(): cambia el orden de las filas.
  • mutate(): crea nuevas columnas, las cuales se expresan como funciones de columnas existentes.
  • summarize(): agrupa y resume valores.

1. Inspección

glimpse(datos)
Rows: 6
Columns: 5
$ `Student ID`   <dbl> 1, 2, 3, 4, 5, 6
$ `Full Name`    <chr> "Sunil Huffmann", "Barclay Lynn", "Jayendra Lyne", "Leo…
$ favourite.food <chr> "Strawberry yoghurt", "French fries", "N/A", "Anchovies…
$ mealPlan       <chr> "Lunch only", "Lunch only", "Breakfast and lunch", "Lun…
$ AGE            <chr> "4", "5", "7", NA, "five", "6"
glimpse(mapa_tidy)
Rows: 24
Columns: 12
$ GID_1     <chr> "ARG.1_1", "ARG.2_1", "ARG.3_1", "ARG.4_1", "ARG.5_1", "ARG.…
$ GID_0     <chr> "ARG", "ARG", "ARG", "ARG", "ARG", "ARG", "ARG", "ARG", "ARG…
$ COUNTRY   <chr> "Argentina", "Argentina", "Argentina", "Argentina", "Argenti…
$ NAME_1    <chr> "Buenos Aires", "Catamarca", "Chaco", "Chubut", "Ciudad de B…
$ VARNAME_1 <chr> "Baires|Buenos Ayres", NA, "El Chaco|Presidente Juan Peron",…
$ NL_NAME_1 <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ TYPE_1    <chr> "Provincia", "Provincia", "Provincia", "Provincia", "Distrit…
$ ENGTYPE_1 <chr> "Province", "Province", "Province", "Province", "Federal Dis…
$ CC_1      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ HASC_1    <chr> "AR.BA", "AR.CT", "AR.CC", "AR.CH", "AR.DF", "AR.CB", "AR.CN…
$ ISO_1     <chr> "AR-B", "AR-K", "AR-H", "AR-U", NA, NA, "AR-W", NA, "AR-P", …
$ geometry  <GEOMETRY [°]> MULTIPOLYGON (((-62.10181 -..., POLYGON ((-65.14583…

2. Manejo de Variables (columnas)

datos |> select(AGE, mealPlan)
datos |> select(Nombre = `Full Name`, edad = AGE)

Selección y Limpieza (select)

Los datos de GADM suelen traer muchas columnas de códigos que no necesitamos (GID_0, GID_1, ISO…). Con sf, si usas select, la columna geometry se mantiene automáticamente (a diferencia de los dataframes normales donde desaparece si no la seleccionas).

mapa_limpio <- mapa_tidy |>   
  select(nombre_region = NAME_1, tipo = TYPE_1)    
# Nota: No selecciono 'geometry', pero R sí sabe que debe conservarla. 
mapa_limpio <- mapa_tidy |>   
  rename(nombre_region = NAME_1, tipo = TYPE_1)    
# Nota: No selecciono 'geometry', pero R sí sabe que debe conservarla. 

3. Filtrado (filas)

Seleccionar observaciones (filas) por condición lógica.

datos |> filter(AGE == 6)
datos |> filter(AGE == 6 & mealPlan=="Lunch only")
datos |> filter(AGE == 6 | AGE == 4)

Seleccionar observaciones (filas) por posición.

datos |> slice(2)
datos |> slice(2:5)
datos |> slice((nrow(datos)-3):nrow(datos))

Ordenar filas.

datos |> arrange(favourite.food)

En piping…

datos |> slice(1:4) |> arrange(desc(favourite.food))
datos |> slice((nrow(datos)-3):nrow(datos)) |> arrange(favourite.food)

Podemos filtrar regiones específicas tal como filtraríamos filas en una tabla.

mapa_limpio |>   
  filter(nombre_region == "Chaco") 
Simple feature collection with 1 feature and 11 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: -63.42762 ymin: -28.00003 xmax: -58.36386 ymax: -24.09314
Geodetic CRS:  WGS 84
    GID_1 GID_0   COUNTRY nombre_region                      VARNAME_1
1 ARG.3_1   ARG Argentina         Chaco El Chaco|Presidente Juan Peron
  NL_NAME_1      tipo ENGTYPE_1 CC_1 HASC_1 ISO_1
1      <NA> Provincia  Province <NA>  AR.CC  AR-H
                        geometry
1 POLYGON ((-60.11628 -25.656...