lapply(c(
  "sf", "readxl", "dplyr", "stringr", "summarytools",
  "leaflet", "htmltools", "mapview", "leafsync", "kableExtra",
  "spatstat.geom", "spatstat.explore", "lubridate", "base64enc", "RColorBrewer", 
  "leaflet.extras", "leaflet.extras2"
), library, character.only = TRUE)

Definición del problema objeto de estudio

Este análisis explora los reportes de tráfico realizados por usuarios de la aplicación Waze en el municipio de Chía, Cundinamarca, durante el 26 de septiembre de 2024. A partir de la información georreferenciada sobre accidentes, peligros, congestiones y cierres de vía, se busca caracterizar la distribución espacial de los eventos y contribuir a la comprensión de patrones viales útiles para la gestión urbana y la movilidad sostenible.

Carga y preparación de los datos

La primera etapa del análisis consistió en la carga, limpieza y transformación inicial de los datos provenientes de la aplicación Waze. Se utilizó la librería readxl para importar el archivo de Excel con los reportes, y lubridate para convertir y estandarizar la información temporal en variables de fecha y hora. Durante la limpieza, se ajustaron las coordenadas de latitud (lat) y longitud (lon) para corregir inconsistencias en el formato y garantizar una representación precisa sobre el mapa. Además, se tradujeron y homogenizaron las categorías de los tipos de evento (type) y sus subtipos (subtype), se rellenaron valores faltantes relevantes (por ejemplo, subtipos según el tipo de reporte y nombres de calle conocidos a partir de coordenadas), y se filtraron los registros para conservar únicamente los reportes de interés en el área del municipio de Chía y fecha seleccionada 2024-09-26. Estas transformaciones permiten asegurar la calidad y la coherencia de la información antes de su exploración y análisis espacial.

# data

waze <- read_excel("Data/Trama Waze.xlsx") %>%
mutate(
    datetime = lubridate::ymd_hms(creation_Date),
    date     = as.Date(datetime),
    time     = format(datetime, "%H:%M:%S"),
    type = dplyr::recode(type,
      ACCIDENT     = "Accidente",
      HAZARD       = "Peligro",
      JAM          = "Congestión",
      ROAD_CLOSED  = "Cierre de vía"),
    lat = if_else(abs(location_y) > 100,
      location_y / 10^(nchar(as.character(location_y)) - 1),
      as.double(location_y)),
    lon = if_else(abs(location_x) > 100,
      location_x / 10^(nchar(as.character(location_x)) - 3),
      as.double(location_x)),
    subtype = case_when(
      type == "Cierre de vía" & (is.na(subtype) | subtype == "ROAD_CLOSED_EVENT") ~ "Evento de cierre de vía",
      type == "Accidente" & (is.na(subtype) | subtype == "ACCIDENT") ~ "Choque",
      type == "Congestión" & (is.na(subtype) | subtype == "CONGESTION") ~ "Tráfico",
      subtype == "JAM_HEAVY_TRAFFIC" ~ "Tráfico pesado",
      subtype == "JAM_STAND_STILL_TRAFFIC" ~ "Tráfico detenido",
      subtype == "HAZARD_ON_SHOULDER_CAR_STOPPED" ~ "Vehículo detenido en el costado                                                       de la vía",
      subtype == "HAZARD_ON_ROAD" ~ "Peligro en la vía",
      subtype == "HAZARD_ON_ROAD_OBJECT" ~ "Objeto en la vía",
      subtype == "HAZARD_WEATHER" ~ "Condición climática peligrosa",
      subtype == "HAZARD_WEATHER_FOG" ~ "Niebla",
      subtype == "HAZARD_WEATHER_HEAVY_SNOW" ~ "Nieve intensa",
      TRUE ~ subtype),
      street = case_when(
      lat == 4.938376 & lon == -74.01693  ~ "Zipaquirá-Cajicá / RN45A-04 >(S)",
      lat == 4.919299 & lon == -74.01520  ~ "Calle 7ª",
      lat == 4.901528 & lon == -74.02999  ~ "Puente hacia Cajicá",
      lat == 4.899889 & lon == -74.03041  ~ "Variante Cajicá / RD45A Ramal A >(S)",
      lat == 4.920176 & lon == -73.99840  ~ "Hatogrande",
      lat == 4.921338 & lon == -73.99727  ~ "Bogotá-Tocancipá / RN55-01 >(N)",
      TRUE ~ street)) %>%
filter(date == as.Date("2024-09-26"), lat > 4) %>%
select(id, date, time, type, subtype, street, lon, lat)
Resumen de eventos reportados en Waze (2024-09-26)
Variable Stats / Values Freqs (% of Valid) Graph Missing
id [numeric]
Mean (sd) : 1251.9 (534)
min ≤ med ≤ max:
16 ≤ 1264 ≤ 2153
IQR (CV) : 905 (0.4)
1747 distinct values 0 (0.0%)
date [Date] 1. 2024-09-26
1747(100.0%)
0 (0.0%)
time [character]
1. 23:38:02
2. 23:40:03
3. 23:44:02
4. 19:52:02
5. 19:54:02
6. 23:36:02
7. 23:42:02
8. 23:46:02
9. 23:48:02
10. 23:50:02
[ 396 others ]
11(0.6%)
11(0.6%)
11(0.6%)
10(0.6%)
10(0.6%)
10(0.6%)
10(0.6%)
10(0.6%)
10(0.6%)
10(0.6%)
1644(94.1%)
0 (0.0%)
type [character]
1. Accidente
2. Cierre de vía
3. Congestión
4. Peligro
36(2.1%)
657(37.6%)
923(52.8%)
131(7.5%)
0 (0.0%)
subtype [character]
1. Choque
2. Condición climática pelig
3. Evento de cierre de vía
4. Niebla
5. Nieve intensa
6. Objeto en la vía
7. Peligro en la vía
8. Tráfico
9. Tráfico detenido
10. Tráfico pesado
11. Vehículo detenido en el c
36(2.1%)
10(0.6%)
657(37.6%)
7(0.4%)
6(0.3%)
14(0.8%)
25(1.4%)
105(6.0%)
360(20.6%)
458(26.2%)
69(3.9%)
0 (0.0%)
street [character]
1. Calle 9ª Sur
2. Zipaquirá-Cajicá / RN45A-
3. Hatogrande
4. Cajicá-Chía / RN45A-04
5. Cajicá-Tabio
6. Puente hacia Cajicá
7. Calle 3ª
8. Tocancipá-Bogotá / RN55-0
9. Bogotá-Tocancipá / RN55-0
10. Carrera 9ª
[ 16 others ]
636(37.6%)
216(12.8%)
143(8.5%)
112(6.6%)
87(5.1%)
78(4.6%)
43(2.5%)
41(2.4%)
35(2.1%)
33(2.0%)
266(15.7%)
57 (3.3%)
lon [numeric]
Mean (sd) : -74 (0)
min ≤ med ≤ max:
-74 ≤ -74 ≤ -74
IQR (CV) : 0 (0)
103 distinct values 0 (0.0%)
lat [numeric]
Mean (sd) : 4.9 (0)
min ≤ med ≤ max:
4.9 ≤ 4.9 ≤ 4.9
IQR (CV) : 0 (0)
102 distinct values 0 (0.0%)

Exploración y visualización de los datos

La exploración descriptiva de los eventos reportados en Waze para el municipio de Chía durante el 26 de septiembre de 2024 revela que las incidencias más frecuentes corresponden a Congestiónvial (52.8%) y Cierres de vía (37.6%), seguidas por los reportes de Peligro (7.5%) y Accidente (2.1%). Entre los subtipos, sobresalen los relacionados con tráfico detenido o pesado, eventos de cierre de vías, y vehículos detenidos, evidenciando que los principales retos de la jornada se asociaron a la movilidad y la gestión de incidentes menores.

Al analizar conjuntamente la tabla resumen y la distribución espacial en el mapa, se observa que la mayoría de los eventos se concentran a lo largo de los corredores viales más transitados, especialmente en la Calle 9ª Sur, la vía Zipaquirá-Cajicá / RN45A-04, y el sector de Hatogrande. La Calle 9ª Sur destaca visualmente en el mapa por la acumulación de puntos correspondientes principalmente a cierres de vía, mientras que otros tramos como la Cajicá-Chía / RN45A-04, Variante Cajicá, y Puente hacia Cajicá presentan densos agrupamientos de reportes de congestión. Los accidentes, aunque menos frecuentes, se visualizan puntualmente en sectores como la Carrera 9ª y la vía Zipaquirá-Cajicá. Así, la observación integrada de la información tabular y cartográfica permite identificar áreas críticas donde se concentran los problemas viales y que requieren mayor atención para la gestión de la movilidad urbana.

# icons
icon_files <- c(
  Accidente       = "icons_waze/accidente.png",
  Peligro         = "icons_waze/peligro.png",
  Congestión      = "icons_waze/congestion.png",
  `Cierre de vía` = "icons_waze/cierre_via.png")

iconos <- do.call(iconList, 
          lapply(icon_files, makeIcon, iconWidth = 32, iconHeight = 32))

icon_b64 <- lapply(icon_files, 
            function(fp) paste0("data:image/png;base64,",         
            base64enc::base64encode(fp)))

#legend
leyenda <- htmltools::tags$div(
  style = "background:rgba(255,255,255,0.8);padding:6px;",
  htmltools::tags$strong("Tipo de reporte"), htmltools::tags$br(),
  mapply(function(nm, img) htmltools::tags$div(
  style = "margin:3px 0;",htmltools::tags$img(
  src = img, width = "24px", height = "24px",
  style = "vertical-align:middle;"),
  htmltools::tags$span(style = "margin-left:5px;", nm)), 
  names(icon_b64), icon_b64, SIMPLIFY = FALSE))

# map cluster
mclus <- leaflet(waze) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addMarkers(
    lng = ~lon, lat = ~lat,
    icon = iconos[as.character(waze$type)],
    popup = ~paste0("<strong>", type, "</strong><br/>",
    "<em>", subtype, "</em><br/>",street, "<br/>", time),
    clusterOptions = markerClusterOptions()) %>%
    addControl(leyenda, position = "bottomright")

# map points
mpoints <- leaflet(waze) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addMarkers(
    lng = ~lon, lat = ~lat,
    icon = iconos[as.character(waze$type)],
    popup = ~paste0("<strong>", type, "</strong><br/>",
    "<em>", subtype, "</em><br/>",street, "<br/>", time))

# print
sync(mclus,mpoints)

Mapas de densidad Espacial

En esta etapa del análisis se generaron mapas de calor individuales para cada tipo de evento reportado en Waze durante el día seleccionado. Se empleó una visualización comparativa , permitiendo observar y contrastar la distribución espacial de accidentes, peligros, congestiones y cierres de vía a lo largo del municipio. Los mapas de calor revelan diferencias claras en la concentración de los reportes según el tipo de evento. Por ejemplo, los reportes de congestión muestran varias zonas críticas, con alta densidad especialmente en los principales corredores viales y accesos a Chía, lo que sugiere focos recurrentes de tráfico intenso. En contraste, los accidentes y cierres de vía tienden a concentrarse en áreas puntuales, mientras que los peligros aparecen dispersos pero también destacan ciertos tramos con mayor incidencia.

Esta visualización resulta clave para la identificación rápida de áreas de riesgo y para priorizar intervenciones en puntos donde la ocurrencia de eventos es más frecuente. El enfoque comparativo facilita reconocer patrones y diferencias en el comportamiento espacial de cada tipo de incidente, información esencial para la gestión urbana y la planificación de estrategias orientadas a mejorar la movilidad y la seguridad vial.

pal <- colorNumeric(
palette = rev(brewer.pal(9, "Spectral")),
domain = c(0.1, 0.3))

# FUN legend
gradient_legend <- function(titulo = "Intensidad de eventos", size = "13px") {
htmltools::tags$div(style = "background: rgba(255,255,255,0.8); padding:7px; border-radius:8px; width:185px;",
 htmltools::tags$div(titulo,
  style = paste("font-weight: bold; margin-bottom: 4px;", 
  sprintf("font-size:%s;", size))),
 htmltools::tags$div(
  style = sprintf("height: 18px; margin-bottom:3px; 
                  background: linear-gradient(to right, %s);",
  paste(pal(seq(0.1, 0.3, length.out=7)), collapse = ","))),
 htmltools::tags$div(
  style="display:flex; justify-content:space-between; font-size:11px;",
 htmltools::tags$span("Bajo"),
 htmltools::tags$span("Medio"),
 htmltools::tags$span("Alto")))}

# FUN heatmap
mapa_calor_tipo <- function(df, tipo_evento) {
  leaflet(df %>% filter(type == tipo_evento)) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addHeatmap(lng = ~lon, lat = ~lat,
    blur = 20, max = 0.08, radius = 15,
    gradient = pal(seq(0.1, 0.3, length.out = 7)) %>% 
    setNames(round(seq(0.1, 0.3, length.out = 7), 2))) %>%
    addControl(gradient_legend(paste("Heatmap", tipo_evento), 
               size = "15px"),position = "bottomright")}

# list types
tipos <- c("Accidente", "Peligro", "Congestión", "Cierre de vía")

# map apply
mapas <- lapply(tipos, function(t) mapa_calor_tipo(waze, t))

# maps print
sync(mapas[[1]], mapas[[2]], mapas[[3]], mapas[[4]])

Análisis de agrupamiento espacial: Conteo por cuadrantes y prueba de CSR

Para evaluar el grado de agrupamiento o dispersión espacial de los eventos reportados en Waze, se aplicó el método de conteo por cuadrantes (quadrat count) sobre el área de estudio, diferenciando cada tipo de reporte. En los gráficos se observa la ubicación puntual de los eventos, superpuestos a una cuadrícula que permite identificar la densidad local en cada sector del municipio. En general, se evidencia un patrón de agrupamiento espacial, con reportes que tienden a concentrarse en celdas específicas del territorio y no se distribuyen de forma homogénea. Esto es especialmente notorio en los casos de congestión y cierre de vía, donde destacan celdas con conteos significativamente altos en comparación con el resto del área.

Estos patrones visuales son consistentes con los resultados de la prueba de cuadrantes para aleatoriedad espacial (CSR: Complete Spatial Randomness), cuyos valores se resumen en la tabla. En todos los casos, el estadístico de Chi-cuadrado es muy alto y los valores de p son prácticamente cero, lo que lleva a rechazar la hipótesis de aleatoriedad espacial. En otras palabras, los reportes de eventos no se distribuyen de manera aleatoria, sino que presentan una marcada tendencia a la agrupación o concentración en áreas específicas.

# # bounding box
bb <- matrix(c(
  min(waze$lon), min(waze$lat),
  max(waze$lon), max(waze$lat)
), ncol=2, byrow=TRUE, dimnames = list(c("xrange", "yrange"), c("lon", "lat")))

win <- spatstat.geom::owin(xrange=range(bb[,"lon"]), yrange=range(bb[,"lat"]))

# Asignar colores para cada tipo
colores <- c("Accidente" = "#8db4ba", "Peligro" = "#fde544", 
             "Congestión" = "#ff5752", "Cierre de vía" = "#ff8043")

# FUN quadrat
quadrat_analisis <- function(df, tipo, nx=4, ny=4) {
  pts <- spatstat.geom::ppp(
    x = df$lon, y = df$lat, window = win
  )
  qcount <- spatstat.geom::quadratcount(pts, nx=nx, ny=ny)
  qtest <- spatstat.explore::quadrat.test(pts, nx=nx, ny=ny)
  list(tipo = tipo, qcount = qcount, qtest = qtest, pts = pts)
}

tipos <- c("Accidente", "Peligro", "Congestión", "Cierre de vía")
resultados <- lapply(tipos, function(tipo){
  df_tipo <- waze[waze$type == tipo, ]
  quadrat_analisis(df_tipo, tipo)
})

# plots
par(mfrow=c(2,2), mar=c(0.5,0.5,1,0.5))  
for(i in seq_along(resultados)){
  res <- resultados[[i]]
  plot(res$qcount, main = paste("Conteo por cuadrantes:", res$tipo),
  cex.main=2, cex.axis=1.4, cex.lab=1.4, frame.plot=FALSE, legend=FALSE)
  points(res$pts, col=colores[res$tipo], pch=20, cex=1.5)
}

par(mfrow=c(1,1))

# table
tab_res <- data.frame(
  Tipo = sapply(resultados, function(x) x$tipo),
  Chi_sq = sapply(resultados, function(x) round(x$qtest$statistic, 2)),
  gl = sapply(resultados, function(x) x$qtest$parameter),
  p_value = sapply(resultados, function(x) signif(x$qtest$p.value, 3))
)

cat('<h5>`Resultados del test de cuadrantes (CSR: Complete Spatial Randomness) `</h5>') 
Resultados del test de cuadrantes (CSR: Complete Spatial Randomness)
kable(tab_res, col.names = c("Tipo", "Chi_sq", "gl", "p_value")) %>%
  kable_styling(full_width = FALSE)
Tipo Chi_sq gl p_value
Accidente 479.56 15 0
Peligro 364.02 15 0
Congestión 1490.02 15 0
Cierre de vía 9204.48 15 0

Prueba de aleatoriedad espacial: Envelopes de la función K

Para evaluar en mayor detalle la aleatoriedad espacial de los reportes, se aplicó la prueba de envelopes de la función K de Ripley, comparando el patrón observado de cada tipo de evento con el esperado bajo un proceso de Poisson homogéneo (CSR). En los gráficos, la función K estimada para los datos reales (línea continua en color) se contrasta con el envelope (banda sombreada) obtenido a partir de 99 simulaciones bajo CSR y con la función teórica (línea punteada). La comparación visual revela que, en todos los tipos de reporte (Accidente, Peligro, Congestión y Cierre de vía), la curva observada de la función K se sitúa fuera del envelope de confianza, superándolo para distintas distancias. Esto permite rechazar la hipótesis de aleatoriedad espacial, evidenciando un agrupamiento significativo de los eventos reportados.

# FUN envelope
envelope_kest <- function(df, tipo, nsim=99) {
  pts <- spatstat.geom::ppp(x = df$lon, y = df$lat, window = win)
  spatstat.explore::envelope(
    pts, 
    fun = spatstat.explore::Kest, 
    nsim = nsim, 
    correction = c("iso", "trans", "border", "bord.modif"),
    simulate = expression(rpoispp(pts$n / spatstat.geom::area.owin(win), win=win)),
    savefuns = TRUE, 
    rank = 1)
}

# envelopes apply
envelopes <- lapply(tipos, function(tipo) {
  df_tipo <- waze[waze$type == tipo, ]
  if(nrow(df_tipo) > 4) {
    envelope_kest(df_tipo, tipo, nsim = 99)
  } else {NA}})
par(mfrow = c(2,2), mar = c(4,4,2,1))
for (i in seq_along(tipos)) {
  if (is.list(envelopes[[i]])) {
    plot(
      envelopes[[i]],
      main = paste("Ktest:", tipos[i]),
      col = colores[tipos[i]],  
      legend = TRUE,
      lwd = 2)}}

Conclusiones

En conjunto, los resultados del análisis exploratorio, la visualización espacial y las pruebas estadísticas de agrupamiento ponen en evidencia patrones de concentración no aleatoria en los reportes de tráfico recopilados mediante la plataforma Waze. La integración de información proveniente de usuarios en tiempo real permitió identificar zonas críticas, horarios y tipos de eventos donde la congestión, los cierres de vía y otros incidentes se agrupan de manera significativa, superando lo que se esperaría bajo un patrón aleatorio. El uso de métodos como el conteo por cuadrantes y la función K de Ripley aporta evidencia robusta sobre la existencia de estos focos de agrupamiento, lo que refuerza la utilidad del crowdsourcing como insumo estratégico para la gestión de movilidad. Integrar este tipo de análisis en la toma de decisiones no solo permite optimizar la respuesta ante emergencias, sino que también facilita la planificación preventiva y el diseño de políticas de movilidad urbana más eficaces y basadas en datos. De este modo, se promueve un enfoque proactivo hacia la construcción de ciudades más seguras, resilientes y orientadas al bienestar colectivo.