Este ejercicio consiste en mostrar espacialmente, los patrones de movilidad en la Ciudad de Cali, a partir la base de datos suministrada denominada “EncuestaOrigenDestino”, con el popósito de identificar cuáles son las comunas de mayor cantidad de viajes de origen y destino, y los modos de transporte más utilizados (bicicleta, moto y automóvil).
A continuación se garantizaron los paquetes y librerías necesarias para el desarrollo del ejericicio, se organizaron las entradas y salidas de la información y se realizó con código de limpieza de los resultados y configuración global del documento.
# Paquetes necesarios
req <- c("sf","dplyr","readxl","janitor","stringr","tmap","lwgeom","units","rlang")
inst <- req[!req %in% installed.packages()[,1]]
if(length(inst)) install.packages(inst)
invisible(lapply(req, require, character.only = TRUE))Se optó por crear una sección de funciones auxiliareas, ya que permiten ser usadas sin tener que repetir el código varias veces, y se deja al inicio del documento para garantizar que se encuentren creadas al momento de su utilización.
A Continuación se define una una función personalizada llamada contar_viajes(), que se usa después para calcular cuántos viajes se originan o terminan en cada comuna.
Esta función permite contar el número de viajes por comuna, ya sea de origen o de destino, y opcionalmente filtrar por tipo de vehículo (Bicicleta, Moto o Automóvil). En otras palabras: Recibe los datos y la columna de comuna,Filtra por tipo de vehículo si se requiere Y cuenta viajes válidos por comuna.
# FUNCIONES AUXILIARES
# Crea función contar_viajes
contar_viajes <- function(df, var_comuna, tipo = NULL){
var <- rlang::ensym(var_comuna) # tidy-eval del nombre de columna
tmp <- df
if (!is.null(tipo)) {
tmp <- dplyr::filter(tmp, .data$tipo_simple == tipo)
}
tmp %>%
dplyr::filter(!is.na(!!var)) %>%
dplyr::count(!!var, name = "n_viajes") %>%
dplyr::rename(comuna = !!var) %>%
dplyr::mutate(comuna = as.integer(comuna))
}Este bloque define la función make_map para producir todos los mapas con el mismo estilo y exporta los mapas en formato.png en la carpeta del proyecto 2000 × 1500 px, lo cual es aproximadamente 200 dpi. que es una buena resolución .
# ---- Creación de la función make_map --------------------------
make_map <- function(shape, variable, titulo, archivo,
palette = "YlOrRd") {
# Validaciones básicas
stopifnot(variable %in% names(shape))
shp <- shape[!is.na(shape[[variable]]), ]
if (nrow(shp) == 0) stop("El shape quedó vacío para '", variable, "'")
# Quita el texto 'Comuna' del nombre para etiquetas más limpias
shp$nombre_sin <- gsub("Comuna\\s*", "", shp$nombre)
# Construcción del mapa
m <- tm_shape(shp) +
tm_polygons(
col = variable,
palette = c("#1a9850", "#fee08b", "#d73027"), # verde–amarillo–rojo
style = "quantile",
border.col = "grey40",
border.alpha = 0.6,
border.lwd = 0.8,
title = "# de viajes"
) +
tm_borders("grey30", lwd = 0.7) +
tm_text("nombre_sin",
size = 0.6, col = "black", fontface = "bold",
bg.color = "white", bg.alpha = 0.5,
just = "center", auto.placement = TRUE) +
tm_scale_bar(
breaks = c(0, 2, 4),
text.size = 0.6,
color.dark = "grey20",
position = c("left", "bottom")
) +
tm_compass(
type = "8star", size = 2,
position = c("right", "top"),
color.light = "white", color.dark = "grey30"
) +
tm_credits("Fuente: Encuesta Origen–Destino Cali 2025",
position = c("left", "bottom"),
size = 0.6, col = "grey35") +
tm_layout(
main.title = titulo,
main.title.size = 1.3,
main.title.position = c("center", "top"),
frame = FALSE,
legend.outside = TRUE,
legend.outside.position = "right",
legend.title.size = 1.0,
legend.text.size = 0.8,
inner.margins = c(0.02, 0.02, 0.02, 0.02),
bg.color = "white"
)
# Guardar imagen con buena resolución
tmap_save(m, filename = archivo,
width = 2000, height = 1500, units = "px", dpi = 200)
# Devuelve el objeto tmap
m
}Esta función cumple un papel fundamental en el flujo de este análisis. sirve para unir la tabla de conteos de viajes (por comuna) con la cartografía de las comunas (shapefile).
En esta sección se carga el shapefile de comunas, se verifica la correcta geometría del archivo, la correcta nomenclatura del nombre de los atributos y el sistema de referencia espacial.
comunas <- sf::st_read(file.path(cali_dir, "comunas.shp"), quiet = TRUE) %>%
sf::st_make_valid() %>% janitor::clean_names() #Corregir geometrías - evitar errores topológicos
stopifnot("comuna" %in% names(comunas)) #Verificar existencia del campo comuna
comunas <- comunas %>% mutate(comuna = as.integer(comuna))#Convertir códigos de comuna a número
crs_work <- st_crs(comunas)#Extraer el sistema de referencia espacialEn esta sección se prepara la base de datos suministrada “EncuestaOrigenDestino” para luego poder contar los viajes y mapear. Identificamos las columnas importantes de la base de datos, filtramos solamente por los viajes de la ciudada de Cali y verificamos la cofificación por tipo de vehículo : tipo_codigo = 1 ~ “Bicicleta”,tipo_codigo = 2 ~ “Moto”,tipo_codigo =3 ~ “Automovil”.
Con el fin de obtener una visión más completa de los desplazamientos urbanos, se incorporó la base denominada eod_cali_total, la cual incluye todos los modos de transporte reportados en la encuesta, sin aplicar filtros por tipo de vehículo, para la elaboración de los mapas generales. Esto permite que los mapas “eod_cali_total – Origen (general)” y “eod_cali_total – Destino (general)” reflejen la totalidad de los viajes observados en la encuesta, abarcando tanto medios motorizados como no motorizados, así como viajes a pie, en transporte público, y otros modos registrados.
# Lectura de la hoja1 donde se encuentra la información".
eod_raw <- readxl::read_excel(path_eod, sheet = 1)
eod <- eod_raw %>% janitor::clean_names()
# Identifica automáticamente las columnas importantes del archivo Excel —como municipio, comuna de origen, comuna de destino y tipo de vehículo— aunque los nombres no sean exactamente iguales
nms <- names(eod)
# municipio
cand_mpio <- nms[grepl("^muni|municipio", nms, ignore.case = TRUE)]
a_mpio <- cand_mpio[1]
# comuna origen/destino
cand_ori <- nms[grepl("comuna.*origen|origen.*comuna|comuna_origen", nms, ignore.case = TRUE)]
cand_des <- nms[grepl("comuna.*destino|destino.*comuna|comuna_destino", nms, ignore.case = TRUE)]
a_ori <- cand_ori[1]
a_des <- cand_des[1]
# tipo de vehículo: nos quedamos con la columna de códigos 1,2,3 en la hoja 1
cand_veh <- nms[grepl("veh", nms, ignore.case = TRUE) & (grepl("tipo", nms, ignore.case = TRUE) | grepl("vehiculo", nms, ignore.case = TRUE))]
a_veh <- if (length(cand_veh)) cand_veh[1] else NA_character_
# Verificación mínima: Si falta alguna columna crítica, el proceso se detiene con un mensaje claro.
needed <- c(a_mpio, a_ori, a_des)
if(any(is.na(needed))) stop("Faltan columnas requeridas (municipio/origen/destino).")
# Filtrar solo ciudad de Cali y preparar variables
eod_cali <- eod %>%
filter(.data[[a_mpio]] %in% c("CALI","Cali","cali")) %>%
mutate(
comuna_origen = as.integer(suppressWarnings(as.numeric(.data[[a_ori]]))),
comuna_destino = as.integer(suppressWarnings(as.numeric(.data[[a_des]])))
) %>%
filter(!is.na(comuna_origen), !is.na(comuna_destino))
# Mapear tipo de vehículo exclusivamente por CÓDIGOS 1,2,3 de la hoja 1.
if (!is.na(a_veh)) {
eod_cali <- eod_cali %>%
mutate(tipo_codigo = suppressWarnings(as.integer(.data[[a_veh]])),
tipo_simple = dplyr::case_when(
tipo_codigo == 1 ~ "Bicicleta",
tipo_codigo == 2 ~ "Moto",
tipo_codigo == 3 ~ "Automovil",
TRUE ~ NA_character_
))
} else {
eod_cali <- eod_cali %>% mutate(tipo_codigo = NA_integer_, tipo_simple = NA_character_)
}eod_cali_total <- eod %>%
dplyr::filter(.data[[a_mpio]] %in% c("CALI","Cali","cali")) %>%
dplyr::mutate(
comuna_origen = as.integer(suppressWarnings(as.numeric(.data[[a_ori]]))),
comuna_destino = as.integer(suppressWarnings(as.numeric(.data[[a_des]])))
) %>%
dplyr::filter(!is.na(comuna_origen), !is.na(comuna_destino))
#eod_cali sería la base que debería utilizar si deseo hacer los "mapas generales" de los tres medios de transporte seleccionados
#eod_cali_total es la base que utilizo para hacer los "mapas generales" incluyendo todos los medios de transporte que hay en la base de datos originalEste bloque de código prepara los datos agregados por comuna (origen y destino), y también por tipo de vehículo (Bicicleta, Moto, Automóvil), dejándolos listos para unir con la cartografía y mapear. ,
# usa la función contar_viajes() para contar n° de viajes por comuna.
ori_gen <- contar_viajes(eod_cali_total, comuna_origen)
des_gen <- contar_viajes(eod_cali_total, comuna_destino)
# Si se requiere el mapa general solo para los medios de transporte 1–2–3 (bici/moto/auto): se reemplaza por estas dos líneas
#ori_gen <- contar_viajes(eod_cali, comuna_origen)
#des_gen <- contar_viajes(eod_cali, comuna_destino)
#La función devuelve dos columnas estándar: comuna y n_viajes.Seguido de esto, se continúa con toda la preparación de datos necesaria para dibujar los 8 mapas. Toma los conteos por comuna de origen (ori_gen) y destino (des_gen), los une con el shapefile de comunas (join_cont hace un left_join con comunas) y rellena con 0 donde no hubo viajes y genera los sf listos para mapear: com_ori_gen y com_des_gen.
También lista los tipos de vehículos que se van a analizar y realiza un conteo por comuna y por tipo de vehículo para origen y destino, generando como resultado dos tablas de conteo “largas”: ori_tipo y des_tipo.luego se Crean dos listas: una para origen y otra para destino, con una capa por cada vehículo y une con el shapefile con ayuda de la función “join_cont” y rellena ceros donde falten viajes.
#Toma los conteos por comuna de origen (ori_gen) y destino (des_gen).
com_ori_gen <- join_cont(ori_gen) # comunas + viajes de origen
com_des_gen <- join_cont(des_gen) # comunas + viajes de destino
# Las dos capas com_ori_gen y com_des_gen son las que posteriormente se utilizan para la construcción de los mapas generales.
#Definir los tres tipos de vehículos que se van a tener en cuenta en el análisis
# calculo, para cada tipo de vehículo, los conteos por comuna de origen y destino
veh <- c("Bicicleta","Moto","Automovil")
ori_tipo <- lapply(veh, function(v) contar_viajes(eod_cali, comuna_origen, v) %>% mutate(tipo=v)) %>% dplyr::bind_rows()
des_tipo <- lapply(veh, function(v) contar_viajes(eod_cali, comuna_destino, v) %>% mutate(tipo=v)) %>% dplyr::bind_rows()#----Armar listas por tipo (para mapear fácilmente)----
#Crea dos listas con tres elementos (uno por tipo). Cada elemento es una capa sf (cartografía + n_viajes), ya unida y con ceros donde no hubo viajes, para que si un tipo no aparece en una comuna, queda con 0.
com_ori_tipo <- setNames(vector("list", length(veh)), veh)
com_des_tipo <- setNames(vector("list", length(veh)), veh)
for (v in veh) {
com_ori_tipo[[v]] <- join_cont(ori_tipo %>% dplyr::filter(tipo==v) %>% dplyr::select(comuna, n_viajes))
com_des_tipo[[v]] <- join_cont(des_tipo %>% dplyr::filter(tipo==v) %>% dplyr::select(comuna, n_viajes))
}Con el paso anterior, se dejaron los datos listos para 6 mapas por tipo (3 de origen + 3 de destino) y 2 mapas generales (origen/destino) para pasarlos a la función make_map() y exportarlos y mostrarlos en grillas (tmap_arrange).
En este bloque se automatiza la creación y exportación de los mapas temáticos de la Encuesta Origen–Destino (EOD) para la ciudad de Cali.
El script genera un total de ocho mapas que representan el número de viajes por comuna, diferenciados según el tipo de análisis:
Mapas generales (2): muestran la distribución total de los viajes de origen y destino para toda la ciudad, utilizando la variable n_viajes.
Mapas específicos por tipo de vehículo (6): se generan de forma independiente para bicicleta, moto y automóvil, tanto para el origen como para el destino de los desplazamientos.
Cada mapa es creado mediante la función make_map(), la cual emplea la librería tmap para representar la intensidad de los viajes mediante una escala de colores (paleta tipo semáforo donde Rojo muestra mayor cantidad de viajes y verde menor cantidad de viajes).
# ----Crear y guardar los 8 mapas---------
out_dir <- file.path(getwd(), "output")
if (!dir.exists(out_dir)) dir.create(out_dir, recursive = TRUE)
map_ori_gen <- make_map(com_ori_gen, "n_viajes", "EOD Cali – Origen (general)",
file.path(out_dir, "01_origen_general.png"))
map_des_gen <- make_map(com_des_gen, "n_viajes", "EOD Cali – Destino (general)",
file.path(out_dir, "02_destino_general.png"))
map_ori_bici <- make_map(com_ori_tipo[["Bicicleta"]], "n_viajes", "Origen – Bicicleta",
file.path(out_dir, "03_origen_bicicleta.png"))
map_ori_moto <- make_map(com_ori_tipo[["Moto"]], "n_viajes", "Origen – Moto",
file.path(out_dir, "04_origen_moto.png"))
map_ori_auto <- make_map(com_ori_tipo[["Automovil"]], "n_viajes", "Origen – Automóvil",
file.path(out_dir, "05_origen_automovil.png"))
map_des_bici <- make_map(com_des_tipo[["Bicicleta"]], "n_viajes", "Destino – Bicicleta",
file.path(out_dir, "06_destino_bicicleta.png"))
map_des_moto <- make_map(com_des_tipo[["Moto"]], "n_viajes", "Destino – Moto",
file.path(out_dir, "07_destino_moto.png"))
map_des_auto <- make_map(com_des_tipo[["Automovil"]], "n_viajes", "Destino – Automóvil",
file.path(out_dir, "08_destino_automovil.png"))Además, el código incluye una verificación que crea automáticamente la carpeta output en el directorio de trabajo si esta no existe, y guarda los resultados en formato PNG con nombres consecutivos (01_origen_general.png, 02_destino_general.png, etc.).
El bloque a continuación, no genera mapas nuevos, sino que muestra en el documento los mapas ya creados en el paso anterior, ordenados en grillas para facilitar su comparación visual.
# Mostrar los 8 mapas ordenados en el HTML usando grillas
tmap_arrange(map_ori_gen, map_des_gen, ncol = 2)Los mapas temáticos elaborados a partir de la Encuesta Origen–Destino permiten observar de manera espacial la distribución de los flujos de movilidad en la ciudad de Cali.
Mapas generales (todos los modos de transporte):
*Las comunas en rojo intenso (por ejemplo, 19,2,17, 18 y 3) concentran las mayores cantidades de viajes de origen, lo que indica que son zonas altamente emisoras de movilidad.
*Las comunas del norte y nororiente (como la 5, 7, 6) están en verde, lo que significa menor generación de viajes.
*Observación Procedimental:Es importante tener claridad si los mapas que se consideran generales se realizan como generales para los tres medios de transporte seleccionados, o generales para todos lo medios de transporte. A lo largo de documento, en la explicación del texto y el código se muestran los cambios que deberían realizarse para una opción u otra. A efectos de este informe se tomaron como Mapas Generales aquellos que incluyen todos los medios de transporte de transporte reportados en la encuesta, sin aplicar filtros.
Modos de Transporte Bicicleta, moto y automovil:
En cuanto a viajes de origen en todos los medios de transporte las comunas 2,19,17 son las que presentan mayor cantidad de viajes y la 4, la 9 y la 12 las que menos de cantidad de viajes tienen.
En Bicicleta la comuna 22 no tiene tan alto desplazamiento de origen como si lo tiene en moto o en automóvil.
la comuna 22 tiene baja cantidad de viajes de origen en todos los medios de transporte.
En cuanto a los viajes de destino las comunas del nororiente tienen menor cantidad de viajes de destino, y las que reciben mayor de cantidad de viajes son las del centro y noroccidente en todos los medios de transporte.
Las comunas del oriente de Cali son las que menos cantidad de viajes en moto reciben.
A continuación se presentan algunas estadísticas que refuerzan lo observado gráficamente en los mapas:
# ---- Top 3 comunas de origen y destino por tipo de vehículo ----
# Filtro general para los tres tipos de vehículo
vehiculos_validos <- c("Bicicleta", "Moto", "Automóvil")
# 🔹 Top 3 comunas de ORIGEN por cada tipo de vehículo
top_origen <- eod_cali %>%
filter(!is.na(tipo_simple) & tipo_simple %in% vehiculos_validos) %>%
group_by(tipo_simple, comuna_origen) %>%
summarise(n_viajes = n(), .groups = "drop") %>%
arrange(tipo_simple, desc(n_viajes)) %>%
group_by(tipo_simple) %>%
slice_head(n = 3) %>%
ungroup() %>%
left_join(
comunas %>% st_drop_geometry() %>% select(comuna, nombre),
by = c("comuna_origen" = "comuna")
) %>%
transmute(
`Tipo de vehículo` = tipo_simple,
`Comuna (origen)` = nombre,
`# de viajes` = n_viajes
)
# 🔹 Top 3 comunas de DESTINO por cada tipo de vehículo
top_destino <- eod_cali %>%
filter(!is.na(tipo_simple) & tipo_simple %in% vehiculos_validos) %>%
group_by(tipo_simple, comuna_destino) %>%
summarise(n_viajes = n(), .groups = "drop") %>%
arrange(tipo_simple, desc(n_viajes)) %>%
group_by(tipo_simple) %>%
slice_head(n = 3) %>%
ungroup() %>%
left_join(
comunas %>% st_drop_geometry() %>% select(comuna, nombre),
by = c("comuna_destino" = "comuna")
) %>%
transmute(
`Tipo de vehículo` = tipo_simple,
`Comuna (destino)` = nombre,
`# de viajes` = n_viajes
)# ---- Mostrar tablas con formato ----
library(kableExtra)
top_origen %>%
knitr::kable(caption = "Top 3 comunas de origen por tipo de vehículo") %>%
kableExtra::kable_styling(
full_width = FALSE,
bootstrap_options = c("striped", "hover", "condensed"),
position = "center"
)| Tipo de vehículo | Comuna (origen) | # de viajes |
|---|---|---|
| Bicicleta | Comuna 19 | 58 |
| Bicicleta | Comuna 2 | 53 |
| Bicicleta | Comuna 17 | 50 |
| Moto | Comuna 19 | 46 |
| Moto | Comuna 2 | 43 |
| Moto | Comuna 17 | 41 |
top_destino %>%
knitr::kable(caption = "Top 3 comunas de destino por tipo de vehículo") %>%
kableExtra::kable_styling(
full_width = FALSE,
bootstrap_options = c("striped", "hover", "condensed"),
position = "center"
)| Tipo de vehículo | Comuna (destino) | # de viajes |
|---|---|---|
| Bicicleta | Comuna 2 | 101 |
| Bicicleta | Comuna 19 | 84 |
| Bicicleta | Comuna 3 | 65 |
| Moto | Comuna 2 | 76 |
| Moto | Comuna 3 | 60 |
| Moto | Comuna 19 | 46 |
# Helper para poner nombres de comuna
nom_comuna <- comunas %>% st_drop_geometry() %>% select(comuna, nombre)
# --- Origen TOP 10 ---
top_origen <- eod_cali %>%
count(comuna_origen, name = "n_viajes") %>%
mutate(
pct = round(n_viajes / sum(n_viajes), 3) # redondeo para evitar muchos decimales
) %>%
arrange(desc(n_viajes)) %>%
left_join(nom_comuna, by = c("comuna_origen" = "comuna"))
# --- Destino TOP 10 ---
top_destino <- eod_cali %>%
count(comuna_destino, name = "n_viajes") %>%
mutate(
pct = round(n_viajes / sum(n_viajes), 3)
) %>%
arrange(desc(n_viajes)) %>%
left_join(nom_comuna, by = c("comuna_destino" = "comuna"))
# --- Mostrar tablas resumidas (Top 5) ---
library(kableExtra)
top_origen %>%
slice_head(n = 5) %>%
transmute(
Comuna = nombre,
`# viajes` = n_viajes,
`%` = percent(pct)
) %>%
knitr::kable(caption = "Top 5 comunas emisoras (origen)") %>%
kableExtra::kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| Comuna | # viajes | % |
|---|---|---|
| Comuna 2 | 2319 | 10.0% |
| Comuna 19 | 2318 | 10.0% |
| Comuna 17 | 1859 | 8.1% |
| Comuna 3 | 1650 | 7.1% |
| Comuna 18 | 1242 | 5.4% |
top_destino %>%
slice_head(n = 5) %>%
transmute(
Comuna = nombre,
`# viajes` = n_viajes,
`%` = percent(pct)
) %>%
knitr::kable(caption = "Top 5 comunas receptoras (destino)") %>%
kableExtra::kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))| Comuna | # viajes | % |
|---|---|---|
| Comuna 2 | 3836 | 16.6% |
| Comuna 3 | 2931 | 12.7% |
| Comuna 19 | 2466 | 10.7% |
| Comuna 17 | 1631 | 7.1% |
| Comuna 22 | 1571 | 6.8% |
# --- Gráficos de barras (Top 10) ---
p_origen <- top_origen %>%
slice_head(n = 10) %>%
ggplot(aes(x = reorder(nombre, n_viajes), y = n_viajes)) +
geom_col(fill = "#2E86C1") + # color azul institucional
geom_text(aes(label = scales::percent(pct)),
hjust = -0.1, size = 3.2) + # agrega etiqueta con % al final de la barra
coord_flip() +
labs(
x = NULL,
y = "# viajes",
title = "Top 10 Origen por comuna"
) +
theme_bw() +
theme(
plot.title = element_text(face = "bold", size = 12),
axis.text.y = element_text(size = 10)
)
p_destino <- top_destino %>%
slice_head(n = 10) %>%
ggplot(aes(x = reorder(nombre, n_viajes), y = n_viajes)) +
geom_col(fill = "#28B463") + # color verde institucional
geom_text(aes(label = scales::percent(pct)),
hjust = -0.1, size = 3.2) +
coord_flip() +
labs(
x = NULL,
y = "# viajes",
title = "Top 10 Destino por comuna"
) +
theme_bw() +
theme(
plot.title = element_text(face = "bold", size = 12),
axis.text.y = element_text(size = 10)
)
# Mostrar ambos gráficos
p_origen; p_destino