Resumen. Este trabajo analiza la distribución espacial de los viajes registrados en la Encuesta de Origen y Destino de Cali (2015). Los viajes se asignan a las 22 comunas urbanas a partir de los códigos de ubicación de la encuesta, previa depuración de las categorías ambiguas, y se representan en mapas coropléticos interactivos de origen, destino y conexión con otros municipios del área metropolitana.
Conocer dónde empiezan y terminan los viajes es un insumo básico para la planeación del transporte: permite identificar las zonas que generan y atraen más movilidad y qué tanto depende cada sector de la ciudad de su relación con los municipios vecinos.
El objetivo de esta actividad es representar espacialmente el origen y el destino de los viajes encuestados en Cali por comuna, y responder tres preguntas:
Se utilizan dos fuentes:
| Fuente | Descripción | Formato |
|---|---|---|
| Encuesta de Origen y Destino de Cali | Encuesta de interceptación de vehículos realizada en 35 estaciones entre mayo y junio de 2015 | .xlsx |
| Comunas de Cali | Polígonos de las 22 comunas del perímetro urbano | Shapefile (.shp) |
La capa de comunas se reproyecta a WGS84 (EPSG:4326), el sistema de referencia que requiere Leaflet.
Nota metodológica. Los conteos corresponden a viajes encuestados, no al total de viajes de la ciudad. Como la encuesta es de interceptación, los resultados también reflejan la ubicación de las estaciones de aforo y deben interpretarse como un indicador relativo, no como una estimación de la demanda total.
comunas=read_sf("Casos/cali/Comunas.shp") %>%
st_transform(4326)
encuesta=read_xlsx("Casos/EncuestaOrigenDestino.xlsx")
viajes_brutos=encuesta[,c(7,9,11,13,26,27)]
names(viajes_brutos)=c("mun_origen","sdg_origen","mun_destino","sdg_destino",
"comuna_origen","comuna_destino")Las variables seleccionadas son el municipio, el código SDG (código de ubicación de la encuesta) y la comuna, tanto para el origen como para el destino de cada viaje.
La columna de comuna de la encuesta no se puede usar directamente, porque contiene dos categorías que mezclan casos distintos. La reclasificación se apoya en el código SDG, cuyos primeros dígitos identifican la comuna del lugar.
Perdida con municipio CALI), como la Avenida Roosevelt o La
14. Estos se separan como Sin ubicar; el resto se mantiene como
Fuera de Cali.ANULADO.clasificar=function(comuna, sdg, municipio) {
prefijo=substr(sdg, 1, nchar(sdg) - 2)
municipio=toupper(trimws(municipio))
case_when(
comuna %in% sprintf("%02d", 1:22) ~ sub("^0+", "", comuna),
prefijo == "26" ~ "Zona rural",
comuna == "0" & prefijo %in% as.character(1:22) ~ prefijo,
sdg == "Perdida" & (is.na(municipio) | municipio %in% c("CALI", "SIN DATOS")) ~ "Sin ubicar",
TRUE ~ "Fuera de Cali"
)
}
viajes=viajes_brutos %>%
filter(!(mun_origen %in% "ANULADO" | mun_destino %in% "ANULADO")) %>%
mutate(
zona_origen=clasificar(comuna_origen, sdg_origen, mun_origen),
zona_destino=clasificar(comuna_destino, sdg_destino, mun_destino)
)
n_anulados=nrow(viajes_brutos) - nrow(viajes)miles=function(x) format(x, big.mark = ".", decimal.mark = ",", trim = TRUE)
porcentaje=function(x) ifelse(is.na(x), "—", sub(".", ",", sprintf("%.1f%%", x), fixed = TRUE))De los 35.054 registros originales se eliminaron 33 anulados, de modo que el análisis trabaja con 35.021 viajes. La tabla 1 muestra cómo quedan distribuidos tras la reclasificación.
tipo_zona=function(z) ifelse(z %in% as.character(1:22), "Comunas urbanas (1–22)", z)
orden_zonas=c("Comunas urbanas (1–22)", "Zona rural", "Sin ubicar", "Fuera de Cali")
full_join(
count(viajes, Zona = tipo_zona(zona_origen), name = "Origen"),
count(viajes, Zona = tipo_zona(zona_destino), name = "Destino"),
by = "Zona"
) %>%
mutate(across(c(Origen, Destino), ~coalesce(.x, 0L)),
Zona = factor(Zona, levels = orden_zonas)) %>%
arrange(Zona) %>%
mutate(`% origen` = porcentaje(100 * Origen / sum(Origen)),
`% destino` = porcentaje(100 * Destino / sum(Destino)),
across(c(Origen, Destino), miles)) %>%
select(Zona, Origen, `% origen`, Destino, `% destino`) %>%
kable(align = "lrrrr",
caption = "Tabla 1. Viajes por tipo de zona después de la depuración")| Zona | Origen | % origen | Destino | % destino |
|---|---|---|---|---|
| Comunas urbanas (1–22) | 28.093 | 80,2% | 27.878 | 79,6% |
| Zona rural | 695 | 2,0% | 786 | 2,2% |
| Sin ubicar | 803 | 2,3% | 1.094 | 3,1% |
| Fuera de Cali | 5.430 | 15,5% | 5.263 | 15,0% |
Para cada comuna se calculan:
\[ \%\,\text{otros municipios}_i = 100 \times \frac{\text{hacia fuera}_i + \text{desde fuera}_i}{\text{origen}_i + \text{destino}_i} \]
conteo_origen=viajes %>%
count(zona_origen, name = "viajes_origen")
conteo_destino=viajes %>%
count(zona_destino, name = "viajes_destino")
hacia_fuera=viajes %>%
filter(zona_destino == "Fuera de Cali") %>%
count(zona_origen, name = "hacia_fuera")
desde_fuera=viajes %>%
filter(zona_origen == "Fuera de Cali") %>%
count(zona_destino, name = "desde_fuera")
comunas_final=comunas %>%
mutate(zona=as.character(comuna)) %>%
left_join(conteo_origen, by = c("zona" = "zona_origen")) %>%
left_join(conteo_destino, by = c("zona" = "zona_destino")) %>%
left_join(hacia_fuera, by = c("zona" = "zona_origen")) %>%
left_join(desde_fuera, by = c("zona" = "zona_destino")) %>%
mutate(
across(c(viajes_origen, viajes_destino, hacia_fuera, desde_fuera), ~coalesce(.x, 0L)),
total=viajes_origen + viajes_destino,
pct_fuera=if_else(total > 0, 100 * (hacia_fuera + desde_fuera) / total, NA_real_)
) %>%
arrange(comuna)
tabla_comunas=st_drop_geometry(comunas_final)
top_origen=slice_max(tabla_comunas, viajes_origen, n = 1, with_ties = FALSE)
top_destino=slice_max(tabla_comunas, viajes_destino, n = 1, with_ties = FALSE)
top_fuera=slice_max(tabla_comunas, pct_fuera, n = 1, with_ties = FALSE)
min_fuera=slice_min(tabla_comunas, pct_fuera, n = 1, with_ties = FALSE)El número sobre cada comuna es la cantidad de viajes. El selector de la esquina superior derecha permite cambiar entre origen y destino; ambas capas usan la misma escala de color, así que son directamente comparables. Al pasar el cursor sobre una comuna se ven sus dos valores, y al hacer clic se abre el detalle.
centros=st_coordinates(st_point_on_surface(st_geometry(comunas_final)))
etiqueta_numero=labelOptions(
noHide = TRUE, textOnly = TRUE, direction = "center",
style = list(
"color" = "#0b0b0b", "font-weight" = "600", "font-size" = "12px",
"background" = "rgba(255,255,255,0.8)", "border-radius" = "3px", "padding" = "0 3px"
)
)
hover=with(comunas_final, lapply(sprintf(
"<b>%s</b><br>Origen: %s<br>Destino: %s",
nombre, miles(viajes_origen), miles(viajes_destino)
), HTML))
popup=with(comunas_final, sprintf(
"<b>%s</b><br>Viajes con origen aquí: %s<br>Viajes con destino aquí: %s<br>
Hacia fuera de Cali: %s<br>Desde fuera de Cali: %s<br>Conexión con otros municipios: %s",
nombre, miles(viajes_origen), miles(viajes_destino),
miles(hacia_fuera), miles(desde_fuera), porcentaje(pct_fuera)
))
valores=c(comunas_final$viajes_origen, comunas_final$viajes_destino)
azules=c("#cde2fb", "#9ec5f4", "#6da7ec", "#3987e5", "#256abf", "#184f95", "#0d366b")
paleta=colorBin(azules, domain = valores, bins = pretty(valores, n = 6))
capa=function(mapa, columna, grupo) {
mapa %>%
addPolygons(
fillColor = paleta(comunas_final[[columna]]), fillOpacity = 0.85,
color = "#ffffff", weight = 1.5, group = grupo,
label = hover, popup = popup,
highlightOptions = highlightOptions(weight = 3, color = "#0b0b0b", bringToFront = TRUE)
) %>%
addLabelOnlyMarkers(
lng = centros[,1], lat = centros[,2], group = grupo,
label = miles(comunas_final[[columna]]), labelOptions = etiqueta_numero
)
}
leaflet(comunas_final, width = "100%", height = 680) %>%
addProviderTiles(providers$Esri.WorldGrayCanvas) %>%
capa("viajes_origen", "Origen") %>%
capa("viajes_destino", "Destino") %>%
addLayersControl(
baseGroups = c("Origen", "Destino"),
options = layersControlOptions(collapsed = FALSE)
) %>%
addScaleBar(position = "bottomleft", options = scaleBarOptions(imperial = FALSE)) %>%
addLegend(
position = "bottomright", pal = paleta, values = valores,
title = "Viajes encuestados", opacity = 0.85,
labFormat = labelFormat(big.mark = ".")
)Figura 1. Viajes encuestados con origen y destino en cada comuna de Cali. Fuente: elaboración propia con datos de la Encuesta de Origen y Destino de Cali (2015).
La comuna con más viajes de origen es Comuna 2 (2.968 viajes), y la que recibe más viajes de destino es Comuna 2 (4.810 viajes). Dado el diseño de la encuesta, estas diferencias combinan la intensidad real de la movilidad con la cercanía de cada comuna a las estaciones de aforo.
El shapefile cubre solo el perímetro urbano, por lo que los viajes de la zona rural, de otros municipios o sin ubicación no tienen polígono. Se reportan en la tabla 2 para que no se pierdan del análisis.
zonas_otras=c("Zona rural", "Fuera de Cali", "Sin ubicar")
tibble(Zona = zonas_otras) %>%
left_join(conteo_origen, by = c("Zona" = "zona_origen")) %>%
left_join(conteo_destino, by = c("Zona" = "zona_destino")) %>%
mutate(across(c(viajes_origen, viajes_destino), ~miles(coalesce(.x, 0L)))) %>%
rename(Origen = viajes_origen, Destino = viajes_destino) %>%
kable(align = "lrr", caption = "Tabla 2. Viajes sin polígono en el mapa de comunas")| Zona | Origen | Destino |
|---|---|---|
| Zona rural | 695 | 786 |
| Fuera de Cali | 5.430 | 5.263 |
| Sin ubicar | 803 | 1.094 |
Aunque los viajes fuera de Cali no caben en el mapa de comunas, sí muestran qué tan conectada está cada comuna con la región. La figura 2 representa el porcentaje de los viajes de cada comuna que salen hacia otro municipio o llegan desde otro.
valores_fuera=comunas_final$pct_fuera
naranjas=c("#fde3d8", "#f7b89e", "#f08f68", "#eb6834", "#bd4c1f", "#8f3410")
paleta_fuera=colorBin(naranjas, domain = valores_fuera,
bins = pretty(valores_fuera, n = 5), na.color = "#e0e0e0")
leaflet(comunas_final, width = "100%", height = 680) %>%
addProviderTiles(providers$Esri.WorldGrayCanvas) %>%
addPolygons(
fillColor = ~paleta_fuera(pct_fuera), fillOpacity = 0.85,
color = "#ffffff", weight = 1.5,
label = hover, popup = popup,
highlightOptions = highlightOptions(weight = 3, color = "#0b0b0b", bringToFront = TRUE)
) %>%
addLabelOnlyMarkers(
lng = centros[,1], lat = centros[,2],
label = ifelse(is.na(valores_fuera), "", sprintf("%.0f%%", valores_fuera)),
labelOptions = etiqueta_numero
) %>%
addScaleBar(position = "bottomleft", options = scaleBarOptions(imperial = FALSE)) %>%
addLegend(
position = "bottomright", pal = paleta_fuera, values = valores_fuera,
title = "% de viajes con<br>otros municipios", opacity = 0.85,
labFormat = labelFormat(suffix = "%")
)Figura 2. Porcentaje de los viajes de cada comuna que tienen su otro extremo en un municipio distinto de Cali. Fuente: elaboración propia con datos de la Encuesta de Origen y Destino de Cali (2015).
La conexión regional va de 10,7% en Comuna 16 a 20,1% en Comuna 3.
La tabla 3 cuenta los viajes con un extremo en Cali (incluida su zona rural) y el otro en un municipio distinto.
en_cali=function(zona) !zona %in% c("Fuera de Cali", "Sin ubicar")
sin_municipio=c("CALI", "SIN DATOS")
hacia_cali=viajes %>%
filter(zona_origen == "Fuera de Cali", en_cali(zona_destino)) %>%
count(municipio = toupper(trimws(mun_origen)), name = "Hacia Cali")
desde_cali=viajes %>%
filter(zona_destino == "Fuera de Cali", en_cali(zona_origen)) %>%
count(municipio = toupper(trimws(mun_destino)), name = "Desde Cali")
municipios=full_join(hacia_cali, desde_cali, by = "municipio") %>%
filter(!is.na(municipio), !municipio %in% sin_municipio) %>%
mutate(
across(c(`Hacia Cali`, `Desde Cali`), ~coalesce(.x, 0L)),
Total = `Hacia Cali` + `Desde Cali`,
Municipio = tools::toTitleCase(tolower(municipio))
) %>%
arrange(desc(Total))
municipios %>%
slice_head(n = 8) %>%
transmute(Municipio, `Hacia Cali` = miles(`Hacia Cali`),
`Desde Cali` = miles(`Desde Cali`), Total = miles(Total)) %>%
kable(align = "lrrr", caption = "Tabla 3. Ocho municipios con más viajes desde y hacia Cali")| Municipio | Hacia Cali | Desde Cali | Total |
|---|---|---|---|
| Yumbo | 1.354 | 1.642 | 2.996 |
| Jamundi | 1.072 | 667 | 1.739 |
| Palmira | 827 | 871 | 1.698 |
| Candelaria | 255 | 278 | 533 |
| Puerto Tejada | 54 | 155 | 209 |
| La Union | 112 | 82 | 194 |
| Santander De Quilichao | 80 | 70 | 150 |
| Guadalajara De Buga | 73 | 65 | 138 |
El municipio con mayor intercambio es Yumbo, con 2.996 viajes en ambos sentidos. Además, 636 viajes tienen los dos extremos fuera de Cali: pasan por la ciudad sin empezar ni terminar en ella, por lo que no se asignan a ninguna comuna.
tabla_comunas %>%
transmute(
Comuna = comuna, Nombre = nombre,
Origen = miles(viajes_origen), Destino = miles(viajes_destino),
`Hacia fuera` = miles(hacia_fuera), `Desde fuera` = miles(desde_fuera),
`% otros municipios` = porcentaje(pct_fuera)
) %>%
kable(align = "llrrrrr", caption = "Tabla 4. Indicadores de viajes por comuna")| Comuna | Nombre | Origen | Destino | Hacia fuera | Desde fuera | % otros municipios |
|---|---|---|---|---|---|---|
| 1 | Comuna 1 | 794 | 230 | 128 | 38 | 16,2% |
| 2 | Comuna 2 | 2.968 | 4.810 | 568 | 855 | 18,3% |
| 3 | Comuna 3 | 2.121 | 3.859 | 398 | 804 | 20,1% |
| 4 | Comuna 4 | 1.493 | 1.856 | 229 | 302 | 15,9% |
| 5 | Comuna 5 | 664 | 522 | 109 | 67 | 14,8% |
| 6 | Comuna 6 | 998 | 716 | 159 | 124 | 16,5% |
| 7 | Comuna 7 | 734 | 748 | 115 | 113 | 15,4% |
| 8 | Comuna 8 | 1.124 | 1.025 | 187 | 157 | 16,0% |
| 9 | Comuna 9 | 952 | 1.279 | 150 | 168 | 14,3% |
| 10 | Comuna 10 | 1.401 | 927 | 212 | 143 | 15,2% |
| 11 | Comuna 11 | 925 | 564 | 156 | 62 | 14,6% |
| 12 | Comuna 12 | 348 | 246 | 70 | 27 | 16,3% |
| 13 | Comuna 13 | 1.238 | 673 | 147 | 84 | 12,1% |
| 14 | Comuna 14 | 685 | 379 | 82 | 55 | 12,9% |
| 15 | Comuna 15 | 1.171 | 579 | 151 | 82 | 13,3% |
| 16 | Comuna 16 | 993 | 743 | 119 | 66 | 10,7% |
| 17 | Comuna 17 | 2.382 | 2.166 | 442 | 416 | 18,9% |
| 18 | Comuna 18 | 1.536 | 662 | 235 | 108 | 15,6% |
| 19 | Comuna 19 | 2.850 | 3.159 | 444 | 573 | 16,9% |
| 20 | Comuna 20 | 745 | 360 | 94 | 36 | 11,8% |
| 21 | Comuna 21 | 818 | 515 | 85 | 59 | 10,8% |
| 22 | Comuna 22 | 1.153 | 1.860 | 127 | 217 | 11,4% |
Perdida dentro de Cali no se pueden asignar a una comuna y
quedan como Sin ubicar.paquetes=c("sf", "dplyr", "leaflet", "htmltools", "readxl", "knitr")
tibble(Paquete = paquetes,
Versión = sapply(paquetes, function(p) as.character(packageVersion(p)))) %>%
kable(caption = paste("Tabla 5. Paquetes utilizados —", R.version.string))| Paquete | Versión |
|---|---|
| sf | 1.1.3 |
| dplyr | 1.2.1 |
| leaflet | 2.2.3 |
| htmltools | 0.5.9 |
| readxl | 1.6.0 |
| knitr | 1.51 |