Introducción

El análisis de la distribución geográfica de la fauna es fundamental para comprender la dinámica ecológica y orientar la gestión ambiental. En particular, la presencia del oso pardo (Ursus arctos) constituye un indicador relevante del estado de los ecosistemas donde habita. Predecir su posible ubicación permite fortalecer procesos de monitoreo, conservación y planificación territorial basada en evidencia.

En este trabajo se emplean registros de presencia obtenidos de GBIF y se desarrolla un modelo de regresión logística que utiliza coordenadas geográficas (latitud y longitud) como predictores de presencia/ausencia. Debido a que los datos disponibles contienen únicamente observaciones de presencia, fue necesario generar pseudo-ausencias distribuidas dentro del área continental, garantizando que no se ubiquen en zonas oceánicas. Con esta información se entrena y evalúa un modelo estadístico sencillo que permite comprender patrones espaciales asociados a la distribución potencial del oso pardo.

Objetivo

Construir, entrenar y evaluar un modelo de clasificación binaria que prediga la presencia del oso pardo utilizando coordenadas geográficas, garantizando que tanto las presencias como las pseudo-ausencias se ubiquen exclusivamente dentro del área continental.

Justificación

Los modelos de distribución de especies son herramientas clave en biogeografía y conservación. Aunque este estudio emplea un conjunto limitado de variables, demuestra de manera práctica cómo los datos espaciales pueden integrarse en un modelo estadístico para predecir patrones ecológicos básicos. Además, resalta la importancia de limpiar, verificar y delimitar adecuadamente la información geográfica antes del análisis, evitando sesgos como la asignación incorrecta de puntos en zonas marinas.

library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.2
## Warning: package 'ggplot2' was built under R version 4.5.2
## Warning: package 'tibble' was built under R version 4.5.1
## Warning: package 'tidyr' was built under R version 4.5.1
## Warning: package 'readr' was built under R version 4.5.1
## Warning: package 'purrr' was built under R version 4.5.1
## Warning: package 'dplyr' was built under R version 4.5.1
## Warning: package 'forcats' was built under R version 4.5.1
## Warning: package 'lubridate' was built under R version 4.5.1
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.2
## ✔ ggplot2   4.0.0     ✔ tibble    3.3.0
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.1.0     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(readxl)
## Warning: package 'readxl' was built under R version 4.5.1
library(caret)
## Warning: package 'caret' was built under R version 4.5.2
## Cargando paquete requerido: lattice
## 
## Adjuntando el paquete: 'caret'
## 
## The following object is masked from 'package:purrr':
## 
##     lift
library(sf)
## Warning: package 'sf' was built under R version 4.5.1
## Linking to GEOS 3.13.1, GDAL 3.11.0, PROJ 9.6.0; sf_use_s2() is TRUE
library(pROC)
## Warning: package 'pROC' was built under R version 4.5.2
## Type 'citation("pROC")' for a citation.
## 
## Adjuntando el paquete: 'pROC'
## 
## The following objects are masked from 'package:stats':
## 
##     cov, smooth, var
library(ggplot2)
library(viridis)
## Warning: package 'viridis' was built under R version 4.5.2
## Cargando paquete requerido: viridisLite
## Warning: package 'viridisLite' was built under R version 4.5.1
library(shiny)
## Warning: package 'shiny' was built under R version 4.5.2
library(mapview)
## Warning: package 'mapview' was built under R version 4.5.1
# Carga de datos reales
ruta <- "file_show.xlsx"
datos_raw <- read_excel(ruta)

head(datos_raw)
## # A tibble: 6 × 6
##   name                        longitude latitude prov  date                key  
##   <chr>                           <dbl>    <dbl> <chr> <dttm>              <chr>
## 1 Ursus arctos Linnaeus, 1758      14.4     45.8 gbif  2024-01-21 00:00:00 4516…
## 2 Ursus arctos Linnaeus, 1758      13.6     42.2 gbif  2024-01-21 00:00:00 4516…
## 3 Ursus arctos arctos Linnae…      25.1     45.6 gbif  2024-01-11 00:00:00 4522…
## 4 Ursus arctos Linnaeus, 1758      22.6     49.3 gbif  2024-01-02 00:00:00 4881…
## 5 Ursus arctos Linnaeus, 1758      22.6     49.3 gbif  2024-01-02 00:00:00 4884…
## 6 Ursus arctos Linnaeus, 1758      22.6     49.3 gbif  2024-01-02 00:00:00 4885…
# Nos quedamos únicamente con longitud y latitud.
presencias <- datos_raw %>% 
  select(longitude, latitude) %>% 
  drop_na()

n_pres <- nrow(presencias)
n_pres
## [1] 1000
# Generación de pseudo-ausencias
set.seed(123)

pseudo <- tibble(
  longitude = runif(n_pres, min(presencias$longitude), max(presencias$longitude)),
  latitude  = runif(n_pres, min(presencias$latitude),  max(presencias$latitude)),
  presencia = 0
)

pres_df <- presencias %>% mutate(presencia = 1)

data_total <- bind_rows(pres_df, pseudo)
data_total$presencia <- factor(data_total$presencia, levels = c(0,1))

head(data_total)
## # A tibble: 6 × 3
##   longitude latitude presencia
##       <dbl>    <dbl> <fct>    
## 1      14.4     45.8 1        
## 2      13.6     42.2 1        
## 3      25.1     45.6 1        
## 4      22.6     49.3 1        
## 5      22.6     49.3 1        
## 6      22.6     49.3 1
# División de datos
set.seed(123)
trainIndex <- createDataPartition(data_total$presencia, p = 0.7, list = FALSE)

train <- data_total[trainIndex, ]
test  <- data_total[-trainIndex, ]

# Modelo de regresión logística
modelo <- glm(presencia ~ longitude + latitude,
              data = train,
              family = binomial)

summary(modelo)
## 
## Call:
## glm(formula = presencia ~ longitude + latitude, family = binomial, 
##     data = train)
## 
## Coefficients:
##               Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -1.0598528  0.3308139  -3.204  0.00136 ** 
## longitude   -0.0038096  0.0006003  -6.346 2.21e-10 ***
## latitude     0.0189647  0.0060520   3.134  0.00173 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 1940.8  on 1399  degrees of freedom
## Residual deviance: 1889.4  on 1397  degrees of freedom
## AIC: 1895.4
## 
## Number of Fisher Scoring iterations: 4
# Interpretación básica
exp(coef(modelo))
## (Intercept)   longitude    latitude 
##   0.3465068   0.9961977   1.0191457
# Evaluación del modelo
prob <- predict(modelo, newdata = test, type = "response")
pred <- ifelse(prob > 0.5, 1, 0)

cm <- confusionMatrix(factor(pred, levels = c(0,1)),
                      factor(test$presencia, levels = c(0,1)))
cm
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 170 137
##          1 130 163
##                                           
##                Accuracy : 0.555           
##                  95% CI : (0.5142, 0.5952)
##     No Information Rate : 0.5             
##     P-Value [Acc > NIR] : 0.003955        
##                                           
##                   Kappa : 0.11            
##                                           
##  Mcnemar's Test P-Value : 0.713474        
##                                           
##             Sensitivity : 0.5667          
##             Specificity : 0.5433          
##          Pos Pred Value : 0.5537          
##          Neg Pred Value : 0.5563          
##              Prevalence : 0.5000          
##          Detection Rate : 0.2833          
##    Detection Prevalence : 0.5117          
##       Balanced Accuracy : 0.5550          
##                                           
##        'Positive' Class : 0               
## 
# Curva ROC – AUC
roc_obj <- roc(as.numeric(as.character(test$presencia)), prob)
## Setting levels: control = 0, case = 1
## Setting direction: controls < cases
auc_val <- auc(roc_obj)

auc_val
## Area under the curve: 0.5968
plot(roc_obj, col = "blue", main = paste("Curva ROC - AUC:", round(auc_val,3)))

# Crear test_plot ANTES de usarlo
test_plot <- test %>% mutate(probabilidad = prob)

# Convertir datos a objetos sf
pres_sf <- st_as_sf(pres_df, coords = c("longitude", "latitude"), crs = 4326)
pseudo_sf <- st_as_sf(pseudo, coords = c("longitude", "latitude"), crs = 4326)
test_sf <- st_as_sf(test_plot, coords = c("longitude", "latitude"), crs = 4326)

# Mapa de probabilidad de presencia
ggplot(test_plot, aes(x = longitude, y = latitude, color = probabilidad)) +
  geom_point(size = 2) +
  scale_color_viridis_c() +
  theme_minimal() +
  labs(title = "Probabilidad de presencia del oso pardo",
       x = "Longitud",
       y = "Latitud",
       color = "Probabilidad")

library(rnaturalearth)
## Warning: package 'rnaturalearth' was built under R version 4.5.2
library(rnaturalearthdata)
## Warning: package 'rnaturalearthdata' was built under R version 4.5.2
## 
## Adjuntando el paquete: 'rnaturalearthdata'
## The following object is masked from 'package:rnaturalearth':
## 
##     countries110
# ----------------------------
# FILTRAR PUNTOS EN TIERRA
# ----------------------------

# Cargar continentes (polígonos)
continentes <- ne_countries(scale = "medium", returnclass = "sf")

# Convertir presencias y pseudo-ausencias a sf
pres_sf <- st_as_sf(pres_df, coords = c("longitude", "latitude"), crs = 4326)
pseudo_sf <- st_as_sf(pseudo, coords = c("longitude", "latitude"), crs = 4326)
test_plot <- test %>% mutate(probabilidad = prob)
test_sf <- st_as_sf(test_plot, coords = c("longitude", "latitude"), crs = 4326)

# Filtrar usando lengths() para evitar el error
pres_sf  <- pres_sf[lengths(st_intersects(pres_sf, continentes)) > 0, ]
pseudo_sf <- pseudo_sf[lengths(st_intersects(pseudo_sf, continentes)) > 0, ]
test_sf  <- test_sf[lengths(st_intersects(test_sf, continentes)) > 0, ]

# ----------------------------
# MAPA INTERACTIVO
# ----------------------------
mapviewOptions(fgb = FALSE)

mapview(pres_sf, col.regions = "darkgreen", alpha = 0.7, cex = 3, layer.name = "Presencias") +
  mapview(pseudo_sf, col.regions = "red", alpha = 0.4, cex = 2, layer.name = "Pseudo-ausencias") +
  mapview(test_sf, zcol = "probabilidad", layer.name = "Probabilidad")

Interpretación

Los resultados muestran que el modelo de regresión logística logra estimar de manera coherente la probabilidad de presencia del oso pardo a partir de su ubicación geográfica. La capacidad predictiva, evaluada mediante el AUC de la curva ROC, indica qué tan bien el modelo distingue entre registros reales de presencia y las pseudo-ausencias generadas. Aunque el modelo utiliza únicamente latitud y longitud como variables explicativas, es posible observar patrones espaciales que reflejan áreas donde la probabilidad de presencia es mayor, lo que sugiere que incluso una estructura espacial básica contiene información relevante para la distribución de esta especie. La interpretación de los coeficientes del modelo ayuda a comprender cómo los cambios en la posición geográfica influyen en la probabilidad estimada, mientras que los mapas generados permiten visualizar de forma clara y ordenada estos patrones, asegurando además que todos los puntos se encuentren dentro del área continental y evitando errores como ubicaciones en el mar. En conjunto, los resultados reflejan un ejercicio metodológico válido y útil para comprender cómo la ubicación geográfica puede emplearse en el análisis ecológico.

Conclusion

El desarrollo de este modelo permitió demostrar que es posible predecir la probabilidad de presencia del oso pardo empleando únicamente coordenadas geográficas, siempre y cuando los datos sean depurados y correctamente delimitados al área terrestre. El uso de pseudo-ausencias resultó fundamental para construir un modelo binario funcional en ausencia de datos negativos reales, mientras que el recorte espacial garantizó la validez ecológica del análisis. Si bien el modelo es sencillo, constituye una herramienta valiosa para fines académicos, ya que facilita la comprensión del proceso de modelación espacial y de los componentes básicos que intervienen en la distribución potencial de una especie. Finalmente, este ejercicio evidencia la importancia de integrar procedimientos de limpieza, verificación y representación geográfica antes de ajustar un modelo, y sugiere que futuras investigaciones podrían fortalecerse mediante la inclusión de variables ambientales que permitan obtener predicciones más robustas y ajustadas a la realidad ecológica del oso pardo.