El crecimiento acelerado de las zonas urbanas y el incremento del parque automotor plantean desafíos complejos para la gestión de la movilidad y la planificación del territorio. Tradicionalmente, el monitoreo del tráfico dependía de infraestructura física costosa y estática, como cámaras de fotodetección, contadores neumáticos o sensores de lazo inductivo. Sin embargo, el paradigma de las ciudades inteligentes (Smart Cities) ha introducido el concepto de Crowdsourcing o sensorización ciudadana activa.
Waze representa una de las plataformas de navegación colaborativa más importantes del mundo. A través de la interacción de millones de usuarios en tiempo real, la aplicación recopila una densa infraestructura de datos espaciales y temporales que registran incidentes críticos como congestiones (jams), accidentes, peligros en la vía (hazards) y cierres viales.
El objetivo de este documento es replicar y profundizar en el análisis de patrones puntuales propuesto en la guía metodológica de David Arango Londoño. Utilizando técnicas de estadística espacial y análisis geoespacial interactivo en el entorno R, transformaremos registros de filas tabulares en estructuras de información geográfica. Esto nos permitirá identificar clústeres de accidentalidad o congestión, evaluar la densidad de los incidentes y proveer herramientas visuales útiles para la toma de decisiones en el diseño de políticas públicas de transporte y seguridad vial.
Para llevar a cabo un flujo de trabajo analítico reproducible, eficiente y visualmente atractivo, utilizaremos un ecosistema de librerías especializadas en ciencia de datos y geomática:
dplyr (Ecosistema Tidyverse):
Herramienta fundamental para la manipulación y transformación de datos
estructurados mediante el uso de operadores de tubería
(%>%), facilitando el filtrado, la selección y el
cálculo de nuevas variables de forma intuitiva.
lubridate: Paquete diseñado para
mitigar las complejidades del manejo de zonas horarias, formatos de
texto a marcas de tiempo (POSIXct) y la extracción rápida
de componentes temporales (días, horas, minutos).
leaflet y
leaflet.extras: Interfaz en R para la biblioteca
JavaScript ‘Leaflet’. Permite la construcción de mapas dinámicos sobre
capas base globales (OpenStreetMap, CartoDB), esenciales para la
exploración espacial mediante herramientas interactivas de zoom y la
generación de mapas de calor analíticos (Heatmaps).
mapview: Proporciona funciones de
visualización rápida de datos espaciales, facilitando la inspección
interactiva y la exportación de geometrías directamente desde la consola
de R hacia formatos listos para la web.
paquetes_requeridos <- c("dplyr", "lubridate", "leaflet", "leaflet.extras", "mapview", "ggplot2", "openxlsx")
# Función de verificación e instalación automatizada
paquetes_faltantes <- paquetes_requeridos[!(paquetes_requeridos %in% installed.packages()[, "Package"])]
if (length(paquetes_faltantes) > 0) {
message("Instalando los siguientes paquetes faltantes: ", paste(paquetes_faltantes, collapse = ", "))
install.packages(paquetes_faltantes, dependencies = TRUE)
} else {
message("Todos los paquetes requeridos ya se encuentran instalados en el entorno.")
}
# Carga de librerías en la sesión de R
library(dplyr)
library(lubridate)
library(leaflet)
library(leaflet.extras)
library(mapview)
library(ggplot2)
library(readxl)
library(openxlsx)
library(spatstat)En esta sección abordaremos un desafío metodológico crítico del
archivo original de David Arango Londoño. Los datos de Waze suelen
exportar las columnas de coordenadas (location_x y
location_y) como números enteros sin decimales para
optimizar el almacenamiento. Para poder proyectarlos correctamente en un
mapa base real (como OpenStreetMap mediante leaflet),
necesitamos aplicar una transformación matemática basada en la longitud
de caracteres para reubicar el punto decimal, y luego filtrar
geográficamente para limpiar ruidos o anomalías fuera de la zona de
estudio.
Además, realizaremos la conversión de las marcas de tiempo a objetos
POSIXct legibles usando lubridate.
Iniciamos con la lectura de la trama de datos de Waze. Dado que el
archivo original se encuentra estructurado en formato de texto plano con
delimitadores por comas (.csv), utilizaremos las funciones
nativas de R para una carga eficiente, asegurando que las cadenas de
texto no se conviertan automáticamente en factores para facilitar su
posterior manipulación secuencial.
# 2. Ingesta de Datos y Preprocesamiento Geoespacial
setwd("~/Estudio/MAESTRIA EN CIENCIA DE DATOS/Analisis_Datos_Geo/Actividad 4")
trama_waze <- read_excel("Trama Waze.xlsx")
View(trama_waze)
trama_waze$location_x <- paste0(substr(trama_waze$location_x, 1, 3),
".",
substr(trama_waze$location_x, 4, 9)) |>
as.numeric()
trama_waze$location_y <- paste0(substr(trama_waze$location_y, 1, 1),
".",
substr(trama_waze$location_y, 2, 9)) |>
as.numeric()
# Inspección básica de dimensiones y nombres de columnas
dimensiones <- dim(trama_waze)
message("El dataset contiene ", dimensiones[1], " registros y ", dimensiones[2], " variables.")
# Visualización de la estructura indexada
str(trama_waze, max.level = 1)|> tibble [5,070 × 17] (S3: tbl_df/tbl/data.frame)
# Transformación matemática y normalización posicional
trama_waze <- trama_waze %>%
mutate(
# Conversión dinámica de Latitud (Y)
lat = location_y,
# Conversión dinámica de Longitud (X)
long = location_x
)
# Forzar signo negativo en la longitud si los datos originales omitieron la orientación Oeste
trama_waze <- trama_waze %>%
mutate(long = ifelse(long > 0, -long, long))
# Resumen estadístico de las nuevas coordenadas geométricas
summary(trama_waze[, c("long", "lat")])|> long lat
|> Min. :-74.04 Min. :2.548
|> 1st Qu.:-74.03 1st Qu.:4.903
|> Median :-74.03 Median :4.915
|> Mean :-74.03 Mean :4.883
|> 3rd Qu.:-74.02 3rd Qu.:4.922
|> Max. :-73.99 Max. :4.949
Debido a posibles errores de transmisión o registros corruptos en la
base de datos (por ejemplo, coordenadas atípicas como
9383-04-01), es indispensable aplicar un filtro de umbral
geográfico. Delimitaremos nuestro marco analítico estrictamente al área
metropolitana y corredores circundantes del norte de la sabana,
restringiendo la latitud al intervalo útil entre \(4.0^\circ\) y \(5.0^\circ\) Norte.
# Filtrado de valores extremos y depuración geométrica
trama_waze_filtrada <- trama_waze %>%
filter(lat > 4.0 & lat < 5.0)
# Cuantificación de registros depurados
registros_eliminados <- nrow(trama_waze) - nrow(trama_waze_filtrada)
message("Registros analizados con éxito: ", nrow(trama_waze_filtrada))
message("Registros atípicos descartados: ", registros_eliminados)La columna creation_Date contiene la estampa cronológica
en formato de texto. Utilizando la flexibilidad de
lubridate, parseamos esta variable al formato estándar de
tiempo POSIXct y extraemos componentes temporales clave
(como el día del mes y la hora del reporte) que serán vitales para
segmentar los patrones puntuales viales.
# Conversión e indexación cronológica
trama_waze_filtrada <- trama_waze_filtrada %>%
mutate(
# Conversión a clase de tiempo estándar de R
fecha_tiempo = ymd_hms(creation_Date),
# Extracción del día de ocurrencia
dia = day(fecha_tiempo),
# Extracción de la hora entera (0-23) para perfiles de movilidad
hora_entera = hour(fecha_tiempo),
# Formato de texto indexado para etiquetas en mapas
hora_etiqueta = format(fecha_tiempo, "%H:%M")
)
# Mostrar una muestra del procesamiento final de las columnas calculadas
trama_waze_filtrada %>%
select(id, type, lat, long, dia, hora_etiqueta) %>%
head(5)|> # A tibble: 5 × 6
|> id type lat long dia hora_etiqueta
|> <dbl> <chr> <dbl> <dbl> <int> <chr>
|> 1 16 HAZARD 4.94 -74.0 26 01:53
|> 2 18 HAZARD 4.93 -74.0 26 01:53
|> 3 20 HAZARD 4.94 -74.0 26 01:54
|> 4 22 HAZARD 4.93 -74.0 26 01:54
|> 5 24 HAZARD 4.94 -74.0 26 01:56
En la guía de David Arango Londoño, una vez que los datos espaciales y temporales están normalizados, se realiza la transición conceptual hacia la estadística descriptiva espacial. Antes de pintar mapas ciegamente, necesitamos entender la composición del dataset mediante tablas de frecuencias globales. Luego, el análisis se concentra de manera específica en los eventos del día 26 (el día más denso en la muestra).
Separaremos los tipos de alerta fundamentales: HAZARD
(Peligros), ACCIDENT (Accidentes) y JAM
(Congestión), traduciéndolos conceptualmente e implementando mapas
interactivos agrupados por clústeres dinámicos
(markerClusterOptions) para evitar la saturación visual en
la pantalla.
El primer paso analítico consiste en cuantificar la naturaleza de los
datos reportados por los usuarios en la plataforma. Clasificar el
volumen total de registros según la variable type nos
permite entender cuál es la problemática vial predominante en nuestra
ventana de estudio (congestiones, seguridad vial o infraestructura).
# Matriz de frecuencias absolutas y relativas por tipo de evento
tabla_incidentes <- trama_waze_filtrada %>%
group_by(type) %>%
summarise(
Frecuencia_Absoluta = n(),
Porcentaje = round((n() / nrow(trama_waze_filtrada)) * 100, 2)
) %>%
arrange(desc(Frecuencia_Absoluta))
# Imprimir tabla resumida en el reporte HTML
knitr::kable(tabla_incidentes, caption = "Frecuencia Global de Incidentes Reportados (Waze)")| type | Frecuencia_Absoluta | Porcentaje |
|---|---|---|
| JAM | 3151 | 63.07 |
| ROAD_CLOSED | 1021 | 20.44 |
| HAZARD | 702 | 14.05 |
| ACCIDENT | 122 | 2.44 |
Siguiendo la metodología de referencia, delimitamos nuestro estudio a un marco temporal homogéneo de 24 horas seleccionando el Día 26 del mes analizado. Esta estrategia reduce el sesgo de acumulación multitemporal y nos permite evaluar un ciclo diario completo de movilidad urbana.
Para entender las dinámicas territoriales individuales, dividiremos el dataset del día 26 según sus categorías principales y las proyectaremos sobre la infraestructura cartográfica de OpenStreetMap mediante clústeres inteligentes.
Los reportes de tipo HAZARD denotan anomalías en la vía
(vehículos varados, baches, objetos en el carril). Agruparlos mediante
clústeres numéricos permite al planificador explorar densidades macro e
ir descendiendo con el zoom hasta el detalle de la hora específica del
reporte.
# Separación del subconjunto de datos de peligros
peligros_26 <- trama_dia26 %>%
filter(type == "HAZARD")
# Construcción del mapa interactivo con Leaflet
mapa_peligros <- leaflet(peligros_26) %>%
addTiles() %>%
addCircleMarkers(
lng = ~long,
lat = ~lat,
clusterOptions = markerClusterOptions(), # Agrupamiento dinámico anti-saturación
label = ~hora_etiqueta, # Muestra la hora al pasar el cursor
color = "#F7931E",
radius = 6,
fillOpacity = 0.8
) %>%
addControl(html = "<h3>Mapa de Riesgos y Peligros (Día 26)</h3>", position = "topleft")
# Renderizar mapa en el documento Rmd
mapa_peligrosLas retenciones de tráfico (JAM) reflejan los cuellos de
botella de la infraestructura vial. Al cartografiar estos incidentes se
evidencia la magnitud de la carga vehicular sobre los ejes conectores
principales de la región en estudio.
# Separación del subconjunto de datos de congestión viales
congestion_26 <- trama_dia26 %>%
filter(type %in% c("JAM", "CONGESTIÓN"))
# Construcción del mapa interactivo de congestión
mapa_congestion <- leaflet(congestion_26) %>%
addTiles() %>%
addCircleMarkers(
lng = ~long,
lat = ~lat,
clusterOptions = markerClusterOptions(),
label = ~hora_etiqueta,
color = "#3498DB", # Azul para congestión estática
radius = 6,
fillOpacity = 0.7
) %>%
addControl(html = "<h3>Mapa de Congestión Vehicular (Día 26)</h3>", position = "topleft")
# Renderizar mapa
mapa_congestionEn este apartado daremos el salto cualitativo de la simple visualización de puntos dispersos hacia la modelación del continuo espacial. El mapa de clústeres anterior es útil para contar incidentes individuales, pero no nos permite modelar superficies de riesgo continuo ni identificar zonas calientes (Hotspots) de manera matemática.
Para resolver esto, implementaremos la función
addHeatmap() de la extensión leaflet.extras,
configurando dinámicamente un radio de suavizado espacial y una rampa de
colores indexada para delimitar visualmente los cuellos de botella de la
movilidad urbana en el área de estudio.
El análisis de patrones puntuales puede verse limitado cuando nos enfrentamos a cientos de observaciones superpuestas en infraestructuras viales densas. Mientras que los mapas de marcadores tradicionales segmentan la información de manera binaria (existe o no existe un punto), la **Estimación de Densidad de Kernel (KDE)** estima una función matemática que distribuye la intensidad de los eventos sobre una superficie continua.
Cada reporte de congestión se convierte en el centro de una función probabilística bidimensional que decrece a medida que se distancia del origen geográfico. Al superponer e integrar estas funciones en una matriz regular (*grid*), el mapa resultante suaviza el ruido aleatorio y expone con claridad los ejes viales con problemas estructurales de saturación o los tramos con mayor vulnerabilidad operativa.
Utilizando el ecosistema de extensiones avanzadas de `leaflet.extras`, convertiremos el set de datos filtrado de congestión (`congestion_26`) en una capa de densidad interactiva. Ajustaremos un radio analítico de $25$ píxeles para optimizar el gradiente visual y mitigar la pixelación en escalas intermedias de zoom.
# Verificar que existan datos en el subconjunto para evitar errores de renderizado
if (nrow(congestion_26) > 0) {
# Construcción del Mapa de Calor Interactivo
mapa_calor_jam <- leaflet(congestion_26) %>%
# Usar una capa base limpia de CartoDB para priorizar el contraste del mapa de calor
addProviderTiles(providers$CartoDB.Positron) %>%
# Inyección de la capa analítica de densidad de Kernel
addHeatmap(
lng = ~long,
lat = ~lat,
intensity = 1, # Peso uniforme por evento registrado
blur = 20, # Grado de dispersión/suavizado en los bordes
max = 0.05, # Punto de saturación para el umbral del color rojo (pico)
radius = 25 # Radio de influencia geométrica en píxeles
) %>%
# Adición de una leyenda científica descriptiva para el tomador de decisiones
addLegend(
position = "bottomright",
title = "Intensidad de Congestión (KDE)",
colors = c("#0000FF", "#00FF00", "#FFFF00", "#FF0000"),
labels = c("Flujo Libre / Bajo", "Densidad Moderada", "Retención Alta", "Saturación Crítica")
) %>%
# Título del panel de control
addControl(
html = "<h3>Hotspots: Densidad de Kernel de Congestión (Día 26)</h3>",
position = "topleft"
)
# Renderizar el objeto cartográfico interactivo
mapa_calor_jam
} else {
message("Advertencia: No se detectaron registros de congestión para compilar el mapa de calor.")
}En esta sección cruzaremos la variable espacial con la dimensión temporal. Como bien plantea la metodología de David Arango Londoño, analizar la distribución horaria nos permite identificar formalmente las horas pico y horas valle de los incidentes en el territorio, validando estadísticamente el comportamiento del flujo de tráfico a lo largo de la jornada viales del Día 26.
La variabilidad espacial de los incidentes viales está íntimamente ligada a los ciclos de actividad humana (horarios laborales, escolares y comerciales). Un análisis estático que ignore la fluctuación horaria perdería la capacidad de predecir en qué momentos del día la infraestructura es más vulnerable.
Estudiar la distribución de frecuencias horarias desagregada por el tipo de reporte nos ayuda a comprender si los accidentes ocurren de manera uniforme o si se concentran en las transiciones de los picos de congestión.
A continuación, agruparemos los datos del día 26 por la variable `hora_entera` y `type`, para luego generar un gráfico de áreas o líneas que ilustre la evolución cronológica del estado de las vías.
# Agrupación y conteo temporal por tipo de incidente
perfil_temporal <- trama_dia26 %>%
group_by(hora_entera, type) %>%
summarise(Total = n(), .groups = 'drop')
# Generación del gráfico con ggplot2
ggplot(perfil_temporal, aes(x = hora_entera, y = Total, color = type, group = type)) +
geom_line(size = 1.2) +
geom_point(size = 2) +
scale_x_continuous(breaks = 0:23) +
scale_color_manual(
values = c("ACCIDENT" = "#E74C3C", "HAZARD" = "#F7931E", "JAM" = "#3498DB"),
labels = c("Accidentes", "Peligros en Vía", "Congestión (Jams)")
) +
labs(
title = "Evolución Horaria de Incidentes en Waze",
subtitle = "Análisis cronológico correspondiente al escenario crítico (Día 26)",
x = "Hora del Día (Formato 24h)",
y = "Cantidad de Reportes",
color = "Tipo de Evento"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5, color = "gray40"),
legend.position = "bottom",
panel.grid.minor = element_blank()
)Para visualizar de forma suavizada el comportamiento general de la jornada (independientemente del tipo de reporte), podemos graficar la función de densidad temporal. Esto nos permite determinar con precisión matemática los centroides de máxima acumulación de alertas en el día.
# Gráfico de densidad de la variable hora
ggplot(trama_dia26, aes(x = hora_entera)) +
geom_density(fill = "#2ECC71", alpha = 0.4, color = "#27AE60", size = 1) +
scale_x_continuous(breaks = 0:23) +
labs(
title = "Densidad Temporal Continua de Alertas",
subtitle = "Identificación de franjas horarias de máxima saturación vial",
x = "Hora del Día",
y = "Densidad de Probabilidad"
) +
theme_classic(base_size = 13) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5, color = "gray40")
)spatstat (Análisis
de Patrones Puntuales).Hasta este punto, hemos hecho una aproximación descriptiva y visual
sumamente atractiva con mapas de calor y gráficos horarias. Sin embargo,
para cumplir con el rigor metodológico del análisis de patrones
puntuales (PPP), debemos utilizar la librería especializada
spatstat. Esta herramienta nos permitirá
evaluar formalmente si la distribución de los incidentes en el espacio
responde a un patrón aleatorio, agrupado o regular.
En este bloque, configuraremos una ventana de observación
(Window o owin), crearemos el objeto de patrón
puntual (ppp) y aplicaremos la estimación analítica de la
función \(G\) (o
función \(F\) / \(K\) según la guía) para testear
científicamente la hipótesis de Aleatoriedad Espacial Completa
(CSR).
En la estadística espacial, un conjunto de coordenadas geográficas se define como un *proceso puntual* si los eventos ocurren en posiciones aleatorias dentro de una región continua bien delimitada. Para determinar si la infraestructura vial está condicionando la acumulación de incidentes, debemos evaluar tres tipos de patrones fundamentales: * **Aleatorio (Proceso de Poisson Homogéneo):** Los eventos ocurren de forma independiente y tienen la misma probabilidad de registrarse en cualquier coordenada. * **Agrupado (Clustered):** La presencia de un incidente incrementa la probabilidad de que ocurran otros en su vecindad inmediata (típico en intersecciones críticas o zonas de embotellamiento). * **Regular (Disperso):** Los eventos se repelen entre sí, manteniendo distancias mínimas homogéneas (poco común en eventos de movilidad).
Para migrar del formato tabular clásico a las clases nativas de `spatstat`, primero debemos asegurarnos de aislar una ventana de observación espacial (`owin`) que encierre de forma estricta los puntos para evitar efectos de borde sesgados.
#Instalar y cargar spatstat si no se encuentra en el entorno
if (!require("spatstat")) {
install.packages("spatstat", dependencies = TRUE)
library(spatstat)
} else {
library(spatstat)
}
# 1. Definición de límites geográficos extremos para la ventana de observación (owin)
x_min <- min(trama_dia26$long, na.rm = TRUE)
x_max <- max(trama_dia26$long, na.rm = TRUE)
y_min <- min(trama_dia26$lat, na.rm = TRUE)
y_max <- max(trama_dia26$lat, na.rm = TRUE)
ventana <- owin(c(x_min, x_max), c(y_min, y_max))
# 2. Construcción del objeto PPP para los incidentes de Congestión (JAM) del Día 26
# Eliminamos duplicados geométricos estrictos para evitar singularidades matemáticas
congestion_clean <- congestion_26 %>%
distinct(long, lat, .keep_all = TRUE)
patron_jam <- ppp(
x = congestion_clean$long,
y = congestion_clean$lat,
window = ventana
)
# Inspección estadística del objeto puntual creado
summary(patron_jam)|> Planar point pattern: 80 points
|> Average intensity 27442.86 points per square unit
|>
|> Coordinates are given to 15 decimal places
|>
|> Window: rectangle = [-74.04304, -73.99451] x [4.888059, 4.948128] units
|> (0.04853 x 0.06007 units)
|> Window area = 0.00291515 square units
La Función G (\(G(r)\)) mide la distribución de la distancia desde cada evento en el mapa hasta su vecino más cercano. Al contrastar nuestra curva observada (\(\hat{G}(r)\)) contra la curva teórica de un modelo completamente aleatorio (\(G_{theo}(r)\)), podemos diagnosticar la estructura espacial del fenómeno:
Si la curva observada sube más rápido y se sitúa por encima de la teórica, indica un claro patrón de agrupamiento (distancias inter-evento muy cortas).
Si se sitúa por debajo, indica un patrón regular.
# Cálculo de la función G empírica con correcciones de borde Kaplan-Meier
g_eval <- Gest(patron_jam)
# Gráfico científico de diagnóstico espacial
plot(g_eval, main = "Función G: Diagnóstico de Patrón Puntual de Congestión",
xlab = "Distancia r (grados)", ylab = "Proporción Acumulada G(r)",
lwd = 2)A través de la réplica metodológica y la expansión analítica ejecutada en este documento, se ha logrado transformar datos crudos y desestructurados de la API de Waze en un modelo analítico de patrones puntuales útil para la planeación urbana. Los hallazgos clave se resumen en los siguientes puntos:
Basados en el comportamiento espacial de los clústeres identificados, se sugieren las siguientes intervenciones estratégicas: * **Despliegue Dinámico de Operativos:** Utilizar los centroides de las horas pico y las zonas rojas del mapa de calor para reubicar personal de tránsito de manera anticipada en los nodos viales identificados, agilizando la respuesta ante vehículos varados (`HAZARD`) que detonan colas de congestión. * **Optimización de Infraestructura:** Evaluar las intersecciones y retornos dentro de las zonas críticas del mapa de calor para implementar mejoras de diseño geométrico, señalización inteligente o fases semafóricas adaptativas. * **Uso de Datos en Tiempo Real:** Fomentar la integración de este tipo de metodologías de *crowdsourcing* en los centros de control de tráfico locales, permitiendo una transición desde la planeación reactiva hacia una gestión predictiva del territorio.