options(repos = c(CRAN = "https://cloud.r-project.org"))
knitr::opts_chunk$set(
echo = TRUE,
warning = FALSE,
message = FALSE,
fig.width = 9,
fig.height = 6,
fig.align = "center",
cache = FALSE
)
# ── Instalación automática ────────────────────────────────────────────────────
paquetes <- c(
"rgbif", "spatstat", "spatstat.geom", "spatstat.explore",
"terra", "sf", "stars",
"rnaturalearth", "rnaturalearthdata", "countrycode",
"dplyr", "purrr","furrr", "ggplot2", "viridis", "scales", "gridExtra",
"tidymodels", "corrplot", "broom",
"palmerpenguins", "ggpubr", "emmeans", "rstatix",
"knitr", "kableExtra", "mapview"
)
instalar_si_falta <- function(pkg) {
if (!requireNamespace(pkg, quietly = TRUE)) install.packages(pkg)
}
invisible(lapply(paquetes, instalar_si_falta))
suppressPackageStartupMessages({
library(rgbif); library(spatstat)
library(spatstat.geom); library(spatstat.explore)
library(terra); library(sf); library(stars)
library(rnaturalearth); library(rnaturalearthdata)
library(countrycode); library(dplyr)
library(purrr); library(ggplot2)
library(viridis); library(scales)
library(gridExtra); library(tidymodels)
library(corrplot); library(broom)
library(palmerpenguins); library(ggpubr)
library(emmeans); library(rstatix)
library(knitr); library(kableExtra)
library(mapview)
})
# ═══════════════════════════════════════════════════════════════════════
# >>> ESTUDIANTE: modifique SOLO este bloque con sus especies y país
# ═══════════════════════════════════════════════════════════════════════
MI_PAIS <- "CO"
mis_especies <- c(
"Bothrops asper",
"Bothrops atrox",
"Bothrops punctatus",
"Lachesis muta",
"Crotalus durissus",
"Micrurus mipartitus",
"Micrurus dumerilii",
"Boa constrictor",
"Leptodeira annulata",
"Spilotes pullatus"
)
SIGMA_KDE <- 1.0
# ═══════════════════════════════════════════════════════════════════════
cat("País:", MI_PAIS, "\n")
cat("Especies ingresadas:\n")
for (i in seq_along(mis_especies)) cat(sprintf(" %2d. %s\n", i, mis_especies[i]))
# ── Función de descarga con manejo de errores ─────────────────────────────────
descargar_especie <- function(nombre, pais, n_max = 500) {
tryCatch({
res <- occ_search(
scientificName = nombre,
country = pais,
hasCoordinate = TRUE,
hasGeospatialIssue = FALSE,
limit = n_max,
fields = c("decimalLongitude", "decimalLatitude",
"year", "basisOfRecord", "species")
)
df <- res$data
if (is.null(df) || nrow(df) == 0) {
message(" Sin registros: ", nombre); return(NULL)
}
df %>%
filter(!is.na(decimalLongitude), !is.na(decimalLatitude)) %>%
rename(lon = decimalLongitude, lat = decimalLatitude) %>%
mutate(especie = nombre)
}, error = function(e) {
message(" Error en ", nombre, ": ", conditionMessage(e)); NULL
})
}
lista_occ <- list()
for (sp in mis_especies) {
message("Descargando: ", sp)
df <- descargar_especie(sp, MI_PAIS)
if (!is.null(df) && nrow(df) > 0) {
lista_occ[[sp]] <- df
} else {
message(" Sin datos: ", sp)
}
Sys.sleep(1.2)
}
ocurrencias <- bind_rows(lista_occ) %>%
filter(especie %in% mis_especies)
resumen_desc <- ocurrencias %>%
count(especie, name = "n_registros") %>%
arrange(desc(n_registros))
kable(resumen_desc,
caption = "Registros de presencia descargados por especie") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
cat(sprintf("\nTotal de registros: %d\n", nrow(ocurrencias)))
nombre_pais <- countrycode(MI_PAIS, "iso2c", "country.name")
pais_sf <- ne_countries(country = nombre_pais, scale = "medium",
returnclass = "sf")
ggplot() +
geom_sf(data = pais_sf, fill = "#f0ede4", color = "gray35", linewidth = 0.4) +
geom_point(
data = ocurrencias %>%
filter(especie %in% mis_especies) %>%
filter(!is.na(lon), !is.na(lat)),
aes(x = lon, y = lat, color = especie),
size = 1.5, alpha = 0.65
) +
scale_color_viridis_d(option = "turbo", name = "Especie") +
labs(
title = paste("Registros de presencia —", nombre_pais),
subtitle = paste("Total registros:", nrow(ocurrencias)),
x = "Longitud", y = "Latitud"
) +
theme_minimal(base_size = 11) +
theme(legend.text = element_text(face = "italic", size = 8))

# ── Construcción robusta de la ventana owin ───────────────────────────────────
pais_union <- pais_sf %>% st_union() %>% st_simplify(dTolerance = 0.05)
pais_partes <- st_cast(pais_union, "POLYGON")
areas <- st_area(pais_partes)
pais_main <- pais_partes[which.max(areas)]
coords_ext <- st_coordinates(pais_main)
anillo_ext <- coords_ext[coords_ext[, "L2"] == 1, c("X", "Y")]
n_pts <- nrow(anillo_ext)
if (all(anillo_ext[1, ] == anillo_ext[n_pts, ])) {
anillo_ext <- anillo_ext[-n_pts, ]
}
ventana_pais <- tryCatch({
owin(poly = list(x = anillo_ext[, "X"],
y = anillo_ext[, "Y"]))
}, error = function(e) {
message("Polígono complejo — usando ventana rectangular como respaldo")
bb <- st_bbox(pais_sf)
owin(xrange = c(bb["xmin"], bb["xmax"]),
yrange = c(bb["ymin"], bb["ymax"]))
})
cat("Ventana creada exitosamente\n")
cat("Tipo:", ventana_pais$type, "\n")
cat("Rango X (lon):", paste(round(ventana_pais$xrange, 2), collapse = " – "), "\n")
cat("Rango Y (lat):", paste(round(ventana_pais$yrange, 2), collapse = " – "), "\n")
# ── Función: construir ppp y estimar intensidad KDE ───────────────────────────
analizar_ppp <- function(df_sp, ventana, sigma) {
nombre <- unique(df_sp$especie)[1]
pts <- df_sp %>%
filter(lon >= ventana$xrange[1], lon <= ventana$xrange[2],
lat >= ventana$yrange[1], lat <= ventana$yrange[2])
if (nrow(pts) < 10) {
message(sprintf(" Omitida (<%d pts): %s", 10, nombre)); return(NULL)
}
ppp_obj <- tryCatch(
ppp(x = pts$lon, y = pts$lat, window = ventana),
error = function(e) {
message(" Error ppp en ", nombre, ": ", conditionMessage(e)); NULL
}
)
if (is.null(ppp_obj)) return(NULL)
ppp_obj <- unique.ppp(ppp_obj)
kde <- density.ppp(ppp_obj, sigma = sigma, eps = 0.08)
message(sprintf(" OK %-38s | n = %d", nombre, npoints(ppp_obj)))
list(nombre = nombre, n_pts = npoints(ppp_obj), ppp = ppp_obj, kde = kde)
}
resultados_ppp <- ocurrencias %>%
group_by(especie) %>%
group_split() %>%
map(~ analizar_ppp(.x, ventana_pais, sigma = SIGMA_KDE)) %>%
compact()
cat(sprintf("\nEspecies procesadas exitosamente: %d / %d\n",
length(resultados_ppp), length(mis_especies)))
n_sp <- length(resultados_ppp)
if (n_sp > 0) {
n_col <- min(3, n_sp)
n_row <- ceiling(n_sp / n_col)
par(mfrow = c(n_row, n_col), mar = c(1.2, 1.2, 3.2, 0.8))
for (r in resultados_ppp) {
plot(r$kde,
main = paste0(r$nombre, " (n=", r$n_pts, ")"),
col = hcl.colors(64, "Inferno"),
las = 1,
ribside = "right",
cex.main = 0.82)
plot(ventana_pais, add = TRUE, lwd = 0.9, border = "white")
points(r$ppp, pch = ".", cex = 1.6, col = "white")
}
par(mfrow = c(1, 1))
}

if (length(resultados_ppp) > 0) {
tabla_int <- map_dfr(resultados_ppp, function(r) {
vals <- as.vector(as.matrix(r$kde))
vals <- vals[!is.na(vals) & vals > 0]
tibble(
Especie = r$nombre,
N_registros = r$n_pts,
lambda_media = round(mean(vals), 6),
lambda_max = round(max(vals), 6),
lambda_p75 = round(quantile(vals, 0.75), 6)
)
})
kable(tabla_int,
caption = "Estadísticos de intensidad estimada λ̂(x,y) por especie",
col.names = c("Especie", "n", "λ media", "λ máximo", "λ p75")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE)
}
kde_a_raster_bin <- function(r_obj) {
kde <- r_obj$kde
mat <- as.matrix(kde)
rst <- rast(
nrows = nrow(mat), ncols = ncol(mat),
xmin = min(kde$xcol), xmax = max(kde$xcol),
ymin = min(kde$yrow), ymax = max(kde$yrow),
crs = "EPSG:4326"
)
values(rst) <- as.vector(t(mat))
v <- values(rst)
umbral <- median(v[!is.na(v) & v > 0])
ifel(rst > umbral, 1, 0)
}
pais_vect <- terra::vect(pais_sf)
rasters_bin <- map(resultados_ppp, function(r) {
rst <- kde_a_raster_bin(r)
rst <- terra::crop(rst, pais_vect)
rst <- terra::mask(rst, pais_vect)
return(rst)
})
if (length(rasters_bin) > 0) {
rst_ref <- rasters_bin[[1]]
riqueza <- rst_ref
for (i in 2:length(rasters_bin)) {
rst_i <- resample(rasters_bin[[i]], rst_ref, method = "near")
riqueza <- riqueza + rst_i
}
riqueza <- terra::crop(riqueza, pais_vect)
riqueza <- terra::mask(riqueza, pais_vect)
cat("Riqueza mínima:", global(riqueza, "min", na.rm = TRUE)[[1]], "\n")
cat("Riqueza máxima:", global(riqueza, "max", na.rm = TRUE)[[1]], "especies\n")
}
## Riqueza mínima: 0
## Riqueza máxima: 10 especies
if (exists("riqueza")) {
pal_azul_disc <- c("#deebf7", "#9ecae1", "#6baed6", "#3182bd", "#08519c")
vals <- terra::values(riqueza)
vals <- vals[!is.na(vals)]
cortes <- seq(0, max(vals), by = 2)
mapview(
riqueza,
col.regions = pal_azul_disc,
at = cortes,
alpha.regions = 0.8,
legend = TRUE,
layer.name = paste("Riqueza -", nombre_pais)
) +
mapview(
pais_sf,
color = "#4a6fa5",
alpha.regions = 0
)
}