CURSO DE R PARA LIDAR

Danilo Verdugo (dynageo@gmail.com)

Sesión Uno

https://rpubs.com/daniloverdugo/drone-ses-01

Introducción a R y RStudio para Análisis de Datos Espaciales.

¿Qué es R?

  • Más que estadística. Un entorno de software libre diseñado específicamente para el análisis de datos y la creación de gráficos de alta calidad.

  • Un SIG de código abierto. A través de sus paquetes (como sf, terra o lidR), R se transforma en un Sistema de Información Geográfica extremadamente potente, automatizable y reproducible.

  • Orientado a la comunidad. Tiene una de las comunidades de usuarios más activas del mundo, cualquier problema espacial que enfrentes, alguien más ya lo resolvió y documentó.

Entendiendo las piezas: R vs. RStudio

  • R (El Motor): Hace todo el cálculo pesado por detrás, pero su interfaz por defecto es muy básica (una consola de texto).

  • RStudio (El Tablero de Control): Es un Entorno de Desarrollo Integrado (IDE). Es la interfaz gráfica amable que usamos para escribir el código, ver los mapas, gestionar los archivos y revisar las variables, todo en una sola pantalla.

Instalación

  • Paso 1: Instalar el “Motor” (R) cran.r-project.org. Descargar e instalar la versión para tu sistema operativo (Windows, Mac o Linux). Dejar todas las opciones por defecto durante la instalación.

  • Paso 2: Instalar el “Tablero” (RStudio) posit.co/download/rstudio-desktop/. Descargar e instalar RStudio Desktop (versión gratuita).

  • Paso 3: Instalar las “Herramientas” (Paquetes) Abrir RStudio por primera vez y usar la consola para instalar nuestro ecosistema básico.

El Ecosistema Tidyverse como Motor de Datos

Objetivo: Lograr fluidez en la manipulación y limpieza de grandes volúmenes de datos, una habilidad crucial para manejar las extensas tablas de atributos de los archivos .las.

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 Puntos, líneas y polígonos. “pipe” %>% o |>
Rasters terra Muy rápido, se integra bien.
LiDAR lidR Nubes puntos. Filtrar, segmentar y analizar.
Á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", ""))

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"

2. Manejo de Variables (columnas)

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

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)

Crear nuevas columnas

mutate permite crear una nueva columna o variable, usar transmute para dejar solo la nueva.

datos |>
  mutate(edad = paste(AGE, "años", sep = " "))
# A tibble: 6 × 6
  `Student ID` `Full Name`      favourite.food     mealPlan          AGE   edad 
         <dbl> <chr>            <chr>              <chr>             <chr> <chr>
1            1 Sunil Huffmann   Strawberry yoghurt Lunch only        4     4 añ…
2            2 Barclay Lynn     French fries       Lunch only        5     5 añ…
3            3 Jayendra Lyne    N/A                Breakfast and lu… 7     7 añ…
4            4 Leon Rossini     Anchovies          Lunch only        <NA>  NA a…
5            5 Chidiegwu Dunkel Pizza              Breakfast and lu… five  five…
6            6 Güvenç Attila    Ice cream          Lunch only        6     6 añ…
datos |>
  transmute(edad = paste(AGE, "años", sep = " "), 
            `edad cuadrada` = as.numeric(AGE) ^ 2)
# A tibble: 6 × 2
  edad      `edad cuadrada`
  <chr>               <dbl>
1 4 años                 16
2 5 años                 25
3 7 años                 49
4 NA años                NA
5 five años              NA
6 6 años                 36

Resumen

datos |>
   summarise(
     rows = n(),
     alimentos = n_distinct(mealPlan))
# A tibble: 1 × 2
   rows alimentos
  <int>     <int>
1     6         2
datos |>
   summarise(
     rows = n(),
     `edad mínima` = min(AGE, na.rm = T))
# A tibble: 1 × 2
   rows `edad mínima`
  <int> <chr>        
1     6 4            

Escritura

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

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).

  4. lidR: Para trabajar con nubes de puntos LiDAR.

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'

Cargar data .las.

library(lidR) # Para trabajar con nubes de puntos LiDAR
# Obtenemos la ruta interna del archivo de prueba
ruta_las <- "data/las01_A1.las"

# Leemos el archivo LAS/LAZ
nube_puntos <- readLAS(ruta_las)

# Damos un vistazo rápido a la estructura espacial
print(nube_puntos)

# Revisar el contenido
las_check(nube_puntos)

De LAS a Tibble: El Puente Tidy

Para aplicar todo lo que aprendimos de dplyr, vamos a extraer la tabla de datos (“payload”) que vive dentro del objeto espacial y la convertiremos en un tibble.

Rows: 122,028
Columns: 16
$ X                 <dbl> 308983.3, 308983.4, 308983.3, 308983.2, 308983.4, 30…
$ Y                 <dbl> 6047094, 6047094, 6047094, 6047094, 6047094, 6047094…
$ Z                 <dbl> 576.149, 579.129, 578.819, 577.649, 582.309, 578.909…
$ gpstime           <dbl> 136000.3, 136000.3, 136000.3, 136000.3, 136000.3, 13…
$ Intensity         <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ ReturnNumber      <int> 2, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2…
$ NumberOfReturns   <int> 2, 3, 3, 3, 2, 3, 2, 2, 3, 3, 2, 2, 3, 3, 2, 2, 3, 3…
$ ScanDirectionFlag <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ EdgeOfFlightline  <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ Classification    <int> 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1…
$ Synthetic_flag    <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
$ Keypoint_flag     <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
$ Withheld_flag     <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
$ ScanAngleRank     <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ UserData          <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ PointSourceID     <int> 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5…

Aplicando los Verbos Tidy

Ahora podemos responder preguntas sobre nuestra nube de puntos usando tuberías (%>% o |>) y verbos clásicos.

A. Resumen por Clasificación (group_by + summarise)

¿Cuántos puntos tenemos por cada clase y cuál es la altura máxima en cada una? (Recordemos: la clase 2 suele ser el terreno, la clase 5 vegetación alta, etc.)

resumen_clases <- datos_lidar |> 
  group_by(Classification) |> 
  summarise(
    cantidad_puntos = n(),
    altura_maxima = max(Z),
    intensidad_media = mean(Intensity)
  ) |> 
  arrange(desc(cantidad_puntos))

glimpse(resumen_clases)
Rows: 2
Columns: 4
$ Classification   <int> 1, 2
$ cantidad_puntos  <int> 112607, 9421
$ altura_maxima    <dbl> 634.775, 621.555
$ intensidad_media <dbl> 1, 1

B. Filtrar, Seleccionar y Mutar (filter + select + mutate)

Vamos a crear una nueva tabla que contenga solo los puntos del terreno (Classification == 2). Nos quedaremos solo con las coordenadas y, a modo de ejemplo, pasaremos la elevación de metros a centímetros.

terreno_limpio <- datos_lidar |> 
  filter(Classification == 2) |> 
  select(X, Y, Z) |> 
  mutate(Z_cm = Z * 100)

head(terreno_limpio)
# A tibble: 6 × 4
        X        Y     Z   Z_cm
    <dbl>    <dbl> <dbl>  <dbl>
1 308983. 6047094.  576. 57615.
2 308983. 6047095.  577. 57674.
3 308983. 6047095.  577. 57681.
4 308983. 6047096.  577. 57694.
5 308983. 6047095.  577. 57692.
6 308983. 6047095.  577. 57686.

Visualización

MUESTREO (Crucial para ggplot2)

  1. ggplot2 no está diseñado para renderizar millones de puntos eficientemente.
  2. Para evitar colapsar la memoria y reducir el “overplotting” (puntos apilados),
  3. Tomar muestra aleatoria (ej. 25% de los puntos) usando un verbo de dplyr.
data_muestra <- datos_lidar |> 
  slice_sample(prop = 0.25) 
# Gráfico Básico (Feo pero funciona)
ggplot(
  data = data_muestra,
  mapping = aes(x = X, y = Y)
) +
  geom_point()
ggplot(
  data = data_muestra,
  mapping = aes(x = X, y = Y)
) +
  geom_point(
    aes(color = Z), # Mapeo DENTRO de aes()
    size = 0.2, # Tamaño FIJO fuera de aes()
    alpha = 0.8 # Transparencia FIJA
  ) +
  labs(title = "Vista en Planta (2.5D) de Nube de Puntos")
# 4. Construimos el gráfico
ggplot(data = data_muestra, aes(x = X, y = Y, color = Z)) +
  # Usamos puntos muy pequeños y algo de transparencia
  geom_point(size = 0.1, alpha = 0.8) +
  # Aplicamos una paleta de colores continua tipo topografía
  scale_color_viridis_c(option = "turbo") + 
  # CRÍTICO: Forzamos la relación de aspecto 1:1. 
  coord_equal() + 
  # Limpiamos el fondo para que parezca un mapa y no un gráfico estadístico
  theme_void() + 
  # Añadimos las etiquetas
  labs(
    title = "Vista en Planta (2.5D) de Nube de Puntos",
    subtitle = "Renderizado nativo con ggplot2 (Muestra del 25%)",
    color = "Elevación (m)"
  )

Análisis básico de nubes de puntos con lidR

Intersección Espacial: Recortando con geometrías sf

Para delimitar un área de Interés (AOI) mediante polígonos. Poder de sf para crear geometrías y la función clip_roi() de lidR para aislar los datos.

# 1. Calcular el centro aproximado de nuestra nube de puntos
centro_x <- mean(nube_puntos@data$X)
centro_y <- mean(nube_puntos@data$Y)

# 2. Crear una geometría sf (un punto) y asignarle el CRS correcto
# CRÍTICO: Para intersectar, sf y lidR deben "hablar" en el mismo sistema de coordenadas
crs_nube <- st_crs(nube_puntos)
aoi_punto <- st_point(c(centro_x, centro_y)) |> 
  st_sfc(crs = crs_nube)

# 3. Crear un buffer (área de influencia) de 50 metros alrededor del punto
aoi_poligono <- st_buffer(aoi_punto, dist = 50)

# 3a. Escribir un archivo shapefile para visualizarlo en QGIS o ArcGIS (opcional).
st_write(aoi_poligono, "data/aoi_poligono.shp")
# 4. El momento de la verdad: Recorte espacial (Clipping)
nube_recortada <- clip_roi(nube_puntos, aoi_poligono)

# 5. Preparamos los datos para ggplot2
df_recorte <- as_tibble(nube_recortada@data)
# 6. Visualización integrada: Polígono + Nube de Puntos
ggplot() +
  # Capa base vectorial: Dibujamos nuestro polígono de recorte vacío y con borde rojo
  geom_sf(data = aoi_poligono, fill = NA, color = "red", linewidth = 1) +
  
  # Capa LiDAR: Ploteamos los puntos que sobrevivieron al recorte
  geom_point(data = df_recorte, aes(x = X, y = Y, color = Z), size = 0.8, alpha = 0.9) +
  
  # Estética del mapa
  scale_color_viridis_c(option = "turbo") +
  
  # coord_sf se encarga de mantener la proporción espacial y dibujar la grilla de coordenadas
  coord_sf() + 
  theme_minimal() +
  labs(
    title = "Recorte Espacial (Clipping) de Nube LiDAR",
    subtitle = "Intersección entre geometría sf (buffer de 50m) y objeto LAS",
    color = "Elevación (m)",
    x = "Longitud (X)",
    y = "Latitud (Y)"
  )