1 Introducción y objetivo

La Encuesta de Movilidad 2015 de Cali (Metro Cali S.A.) incluyó una encuesta de interceptación a vehículos privados en 35 estaciones de la ciudad. A cada conductor o pasajero se le preguntó de dónde venía (origen), hacia dónde iba (destino), qué iba a hacer allá (motivo), su estrato, edad, sexo y el tipo de vehículo. Cada registro tiene asignada la comuna de origen y la comuna de destino, lo que permite llevar la encuesta al espacio usando el shapefile de las 22 comunas de Cali.

Objetivo: explorar espacialmente los viajes en vehículo privado de Cali para responder:

  1. ¿Qué comunas generan más viajes y cuáles los atraen?
  2. ¿Qué comunas son “dormitorio” (más salidas que llegadas) y cuáles son polos de actividad (más llegadas que salidas)?
  3. ¿Cuáles son los principales corredores origen-destino entre comunas?
  4. ¿Cómo se relacionan estos patrones con el motivo del viaje y el estrato del viajero?

Se siguen los mismos pasos del ejemplo de la unidad (importar el shapefile, hacer consultas espaciales, crear geometrías a partir de una tabla, transformar el sistema de coordenadas, integrar la información a nivel de polígono y visualizar con coropletas). Como las librerías rgdal y raster del ejemplo fueron retiradas de CRAN en 2023, se usa su reemplazo moderno sf, que hace las mismas operaciones (st_read ≈ shapefile, st_transform ≈ spTransform, st_join/left_join ≈ over).

2 Librerías y datos

Los archivos del caso (Casos.zip) deben estar descomprimidos en una carpeta Casos/ junto a este archivo .Rmd.

library(sf)        # lectura, transformación y operaciones con datos espaciales
library(readxl)    # lectura de archivos Excel
library(dplyr)     # manipulación de datos
library(tidyr)     # reestructuración de tablas
library(ggplot2)   # gráficos y mapas estáticos
library(scales)    # formatos de ejes y leyendas
library(knitr)     # tablas
ruta_shape    <- "Casos/cali/Comunas.shp"
ruta_encuesta <- "Casos/EncuestaOrigenDestino.xlsx"

2.1 Datos espaciales: shapefile de comunas

comunas <- st_read(ruta_shape, quiet = TRUE)
comunas
## Simple feature collection with 22 features and 4 fields
## Geometry type: POLYGON
## Dimension:     XY
## Bounding box:  xmin: 1053868 ymin: 860190.2 xmax: 1068492 ymax: 879441.5
## Projected CRS: MAGNA_Colombia_Cali
## First 10 features:
##    OBJECTID gid comuna    nombre                       geometry
## 1         1 107      2  Comuna 2 POLYGON ((1059648 874236.3,...
## 2         2 108      1  Comuna 1 POLYGON ((1054094 875172.8,...
## 3         3 109      3  Comuna 3 POLYGON ((1061757 874615.5,...
## 4         4 110     19 Comuna 19 POLYGON ((1057817 873269.4,...
## 5         5 103     15 Comuna 15 POLYGON ((1065485 869864.1,...
## 6         6 104     17 Comuna 17 POLYGON ((1061675 868320.9,...
## 7         7 105     18 Comuna 18 POLYGON ((1059061 867223.6,...
## 8         8 106     22 Comuna 22 POLYGON ((1059486 864976.4,...
## 9         9  89      6  Comuna 6 POLYGON ((1065143 879409.5,...
## 10       10  90      4  Comuna 4 POLYGON ((1063833 877507.1,...
st_crs(comunas)$proj4string
## [1] "+proj=tmerc +lat_0=3.441883333 +lon_0=-76.5205625 +k=1 +x_0=1061900.18 +y_0=872364.63 +a=6379137 +rf=298.257222101 +units=m +no_defs"

El shapefile tiene 22 polígonos (una por comuna) y 4 atributos. Está en el sistema de coordenadas planas MAGNA-SIRGAS Cali (Transversa de Mercator, en metros), por lo que sirve para medir áreas y distancias. Para mapas web (leaflet) hay que transformarlo a coordenadas geográficas WGS84 (EPSG:4326).

comunas <- comunas %>%
  mutate(comuna  = as.integer(comuna),
         area_km2 = as.numeric(st_area(geometry)) / 1e6)   # área a partir de la geometría

ggplot(comunas) +
  geom_sf(fill = "grey92", colour = "grey40") +
  geom_sf_text(aes(label = comuna), size = 3.2) +
  labs(title = "Comunas de Cali", x = NULL, y = NULL) +
  theme_void()

Consulta espacial (como en el ejemplo, filtrando por un campo de la tabla de atributos): se resaltan las comunas 2, 3 y 19, que más adelante aparecen como los principales destinos de viaje.

comunas_interes <- comunas[comunas$comuna %in% c(2, 3, 19), ]

ggplot() +
  geom_sf(data = comunas, fill = "grey92", colour = "grey50") +
  geom_sf(data = comunas_interes, fill = "firebrick", alpha = 0.7) +
  geom_sf_text(data = comunas_interes, aes(label = comuna), colour = "white") +
  labs(title = "Consulta espacial: comunas 2, 3 y 19") +
  theme_void()

# Exportar el nuevo shapefile (equivale a shapefile(..., overwrite = TRUE) del ejemplo)
st_write(comunas_interes, file.path(tempdir(), "Comunas_interes.shp"),
         delete_layer = TRUE, quiet = TRUE)

2.2 Datos de la encuesta origen-destino

eod_raw <- read_excel(ruta_encuesta, sheet = "Hoja1", .name_repair = "unique_quiet")
dim(eod_raw)
## [1] 35054    28

La hoja tiene 35054 encuestas y 28 variables. Los nombres originales son largos y tienen tildes y signos, así que se renombran a nombres cortos.

eod <- eod_raw
names(eod) <- c("fecha", "id_estacion", "estacion", "acceso", "movimiento", "hora",
                "mun_origen", "lugar_origen", "cod_origen", "actividad_origen",
                "mun_destino", "lugar_destino", "cod_destino", "motivo",
                "estrato", "disponia_vehiculo", "otro_vehiculo", "antes", "despues",
                "edad", "sexo", "personas", "tipo_vehiculo", "otro_tipo",
                "tipo_viajero", "comuna_origen", "comuna_destino", "intracomuna")

Se decodifican las variables con las convenciones de la hoja Convenciones (2). La comuna “0” corresponde a viajes con origen o destino en la zona rural de Cali (corregimientos como Pance, La Buitrera o Montebello) y “Fuera de Cali” a otros municipios (Yumbo, Jamundí, Palmira, etc.).

etq_motivo <- c("1" = "Trabajar", "2" = "Asuntos de trabajo", "3" = "Estudiar",
                "4" = "Salud", "5" = "Ver a alguien", "6" = "Volver a casa",
                "7" = "Buscar/dejar a alguien", "8" = "Buscar/dejar algo",
                "9" = "Comer", "10" = "Compras", "11" = "Trámites",
                "12" = "Recreación", "13" = "Buscar trabajo", "77" = "Otra cosa")
etq_vehiculo <- c("1" = "Bicicleta", "2" = "Moto", "3" = "Automóvil",
                  "4" = "Campero", "5" = "Taxi", "6" = "Bus intermunicipal",
                  "7" = "Buseta intermunicipal", "8" = "Van intermunicipal",
                  "9" = "Taxi blanco", "77" = "Otro")

eod <- eod %>%
  mutate(
    motivo_txt   = factor(etq_motivo[as.character(motivo)], levels = etq_motivo),
    vehiculo_txt = factor(etq_vehiculo[as.character(tipo_vehiculo)], levels = etq_vehiculo),
    sexo_txt     = factor(sexo, levels = 1:2, labels = c("Femenino", "Masculino")),
    hora_num     = as.integer(format(hora, "%H")),
    # Tipo de lugar de origen y destino
    zona_origen  = case_when(comuna_origen  == "Fuera de Cali" ~ "Fuera de Cali",
                             comuna_origen  == "0" ~ "Rural Cali",
                             TRUE ~ "Urbano Cali"),
    zona_destino = case_when(comuna_destino == "Fuera de Cali" ~ "Fuera de Cali",
                             comuna_destino == "0" ~ "Rural Cali",
                             TRUE ~ "Urbano Cali"),
    # Comunas como número entero (NA si es rural o fuera de Cali) para unir con el shape
    com_o = suppressWarnings(as.integer(comuna_origen)),
    com_d = suppressWarnings(as.integer(comuna_destino)),
    com_o = ifelse(com_o == 0, NA, com_o),
    com_d = ifelse(com_d == 0, NA, com_d)
  )

# Validaciones de calidad: valores faltantes en las variables clave
eod %>%
  summarise(across(c(comuna_origen, comuna_destino, motivo, estrato, edad,
                     tipo_vehiculo), ~ sum(is.na(.)))) %>%
  kable(caption = "Valores faltantes por variable clave")
Valores faltantes por variable clave
comuna_origen comuna_destino motivo estrato edad tipo_vehiculo
0 0 154 1231 479 115

Las comunas de origen y destino están completas en todos los registros; los faltantes en motivo, estrato o edad son pocos (menos del 4 %) y solo se excluyen en los análisis que usan esa variable.

Para el análisis espacial interesa el tipo de viaje según dónde empieza y dónde termina:

tabla_zonas <- eod %>% count(zona_origen, zona_destino) %>%
  mutate(porcentaje = percent(n / sum(n), accuracy = 0.1)) %>%
  arrange(desc(n))
kable(tabla_zonas, caption = "Viajes según zona de origen y destino")
Viajes según zona de origen y destino
zona_origen zona_destino n porcentaje
Urbano Cali Urbano Cali 22151 63.2%
Urbano Cali Fuera de Cali 5261 15.0%
Fuera de Cali Urbano Cali 5180 14.8%
Fuera de Cali Fuera de Cali 1024 2.9%
Urbano Cali Rural Cali 664 1.9%
Rural Cali Urbano Cali 535 1.5%
Rural Cali Fuera de Cali 115 0.3%
Fuera de Cali Rural Cali 102 0.3%
Rural Cali Rural Cali 22 0.1%

Se separan los viajes urbanos (origen y destino en una comuna de Cali), que son la base de la matriz origen-destino entre comunas.

urbanos <- eod %>% filter(!is.na(com_o), !is.na(com_d))
nrow(urbanos)
## [1] 22151

Población por comuna (2015). La misma hoja de cálculo trae las proyecciones de población por comuna (hoja Sheet3), que se usan para calcular tasas por habitante.

poblacion <- read_excel(ruta_encuesta, sheet = "Sheet3", range = "U3:AB25",
                        .name_repair = "unique_quiet")
poblacion <- poblacion %>%
  select(nombre = 1, pob_2015 = `2015`) %>%
  mutate(comuna = as.integer(gsub("Comuna ", "", nombre))) %>%
  select(comuna, pob_2015)
head(poblacion)
## # A tibble: 6 × 2
##   comuna pob_2015
##    <int>    <dbl>
## 1      1    88432
## 2      2   114651
## 3      3    46400
## 4      4    53369
## 5      5   112089
## 6      6   189837

3 Exploración descriptiva de la encuesta

eod %>% filter(!is.na(vehiculo_txt)) %>%
  count(vehiculo_txt) %>%
  ggplot(aes(x = reorder(vehiculo_txt, n), y = n)) +
  geom_col(fill = "steelblue") +
  geom_text(aes(label = percent(n / sum(n), accuracy = 0.1)), hjust = -0.1, size = 3) +
  coord_flip() + scale_y_continuous(expand = expansion(mult = c(0, 0.12))) +
  labs(title = "Encuestados según tipo de vehículo", x = NULL, y = "Encuestas") +
  theme_minimal()

eod %>% filter(!is.na(motivo_txt)) %>%
  count(motivo_txt) %>%
  ggplot(aes(x = reorder(motivo_txt, n), y = n)) +
  geom_col(fill = "darkorange") +
  geom_text(aes(label = percent(n / sum(n), accuracy = 0.1)), hjust = -0.1, size = 3) +
  coord_flip() + scale_y_continuous(expand = expansion(mult = c(0, 0.12))) +
  labs(title = "Motivo del viaje (¿qué va a hacer en el destino?)", x = NULL, y = "Encuestas") +
  theme_minimal()

# Se excluyen 11 registros con hora 00:xx, que son errores de digitación
eod %>% filter(!is.na(hora_num), hora_num >= 5) %>%
  ggplot(aes(x = hora_num)) +
  geom_bar(fill = "grey40") +
  scale_x_continuous(breaks = 6:12) +
  labs(title = "Encuestas por hora del día", x = "Hora", y = "Encuestas") +
  theme_minimal()

eod %>% filter(!is.na(estrato)) %>%
  count(estrato) %>% mutate(p = n / sum(n)) %>%
  ggplot(aes(x = factor(estrato), y = p)) +
  geom_col(fill = "seagreen") +
  geom_text(aes(label = percent(p, accuracy = 0.1)), vjust = -0.3, size = 3) +
  scale_y_continuous(labels = percent) +
  labs(title = "Estrato de la vivienda del viajero", x = "Estrato", y = "% de encuestas") +
  theme_minimal()

eod %>% summarise(
  `Edad media`        = round(mean(edad, na.rm = TRUE), 1),
  `% hombres`         = percent(mean(sexo == 2, na.rm = TRUE), 0.1),
  `% conductores`     = percent(mean(tipo_viajero == 1, na.rm = TRUE), 0.1),
  `% viaja solo`      = percent(mean(personas == 1, na.rm = TRUE), 0.1),
  `% motivo laboral`  = percent(mean(motivo %in% c(1, 2), na.rm = TRUE), 0.1),
  `% volver a casa`   = percent(mean(motivo == 6, na.rm = TRUE), 0.1)
) %>% kable(caption = "Perfil general del viajero en vehículo privado")
Perfil general del viajero en vehículo privado
Edad media % hombres % conductores % viaja solo % motivo laboral % volver a casa
38 79.5% 82.9% 65.6% 60.6% 13.7%

4 Integración espacial: viajes generados y atraídos por comuna

Se cuentan los viajes generados (comuna de origen) y atraídos (comuna de destino) por cada comuna. Se incluyen todos los viajes que empiezan o terminan en una comuna urbana, también los que vienen de o van hacia fuera de Cali. Luego se unen al shapefile por el código de comuna; es el mismo resultado que da over() en el ejemplo, pero aquí la unión es por atributo porque la encuesta ya trae la comuna asignada.

generados <- eod %>% filter(!is.na(com_o)) %>% count(comuna = com_o, name = "generados")
atraidos  <- eod %>% filter(!is.na(com_d)) %>% count(comuna = com_d, name = "atraidos")
intra     <- urbanos %>% filter(com_o == com_d) %>% count(comuna = com_o, name = "intra")

comunas_viajes <- comunas %>%
  left_join(generados, by = "comuna") %>%
  left_join(atraidos,  by = "comuna") %>%
  left_join(intra,     by = "comuna") %>%
  left_join(poblacion, by = "comuna") %>%
  mutate(across(c(generados, atraidos, intra), ~ coalesce(., 0L)),
         balance       = atraidos - generados,                 # >0 polo atractor
         indice_atrac  = atraidos / generados,                 # >1 atrae más de lo que genera
         gen_x1000hab  = generados / pob_2015 * 1000,
         atr_x1000hab  = atraidos  / pob_2015 * 1000,
         pct_intra     = intra / generados)

comunas_viajes %>% st_drop_geometry() %>%
  select(comuna, pob_2015, area_km2, generados, atraidos, balance, indice_atrac,
         gen_x1000hab, pct_intra) %>%
  arrange(desc(atraidos)) %>%
  mutate(across(c(area_km2, indice_atrac, gen_x1000hab), ~ round(., 2)),
         pct_intra = percent(pct_intra, 0.1)) %>%
  kable(caption = "Viajes en vehículo privado por comuna (ordenado por viajes atraídos)")
Viajes en vehículo privado por comuna (ordenado por viajes atraídos)
comuna pob_2015 area_km2 generados atraidos balance indice_atrac gen_x1000hab pct_intra
2 114651 11.41 2968 4810 1842 1.62 25.89 16.7%
3 46400 3.70 2121 3859 1738 1.82 45.71 5.4%
19 112947 11.32 2850 3158 308 1.11 25.23 8.7%
17 139665 12.57 2377 2164 -213 0.91 17.02 4.5%
22 11160 10.59 1153 1860 707 1.61 103.32 2.9%
4 53369 4.53 1493 1856 363 1.24 27.98 4.8%
9 44994 2.90 952 1279 327 1.34 21.16 5.5%
8 102388 5.27 1124 1025 -99 0.91 10.98 2.5%
10 110854 4.30 1401 927 -474 0.66 12.64 1.7%
7 71334 5.11 734 748 14 1.02 10.29 4.9%
16 107170 4.26 993 743 -250 0.75 9.27 3.8%
6 189837 5.04 998 716 -282 0.72 5.26 2.2%
13 177641 4.74 1238 673 -565 0.54 6.97 2.8%
18 131453 5.43 1536 662 -874 0.43 11.68 3.5%
15 159369 4.06 1171 579 -592 0.49 7.35 2.0%
11 107339 3.70 925 564 -361 0.61 8.62 3.4%
5 112089 4.20 664 522 -142 0.79 5.92 1.2%
21 112336 4.83 818 515 -303 0.63 7.28 1.1%
14 172696 4.54 685 379 -306 0.55 3.97 2.2%
20 69331 2.44 733 351 -382 0.48 10.57 3.3%
12 66881 2.33 348 246 -102 0.71 5.20 2.0%
1 88432 3.84 794 230 -564 0.29 8.98 0.8%

4.1 Mapas de coropletas de generación y atracción

mapa_long <- comunas_viajes %>%
  select(comuna, generados, atraidos) %>%
  pivot_longer(c(generados, atraidos), names_to = "tipo", values_to = "viajes") %>%
  mutate(tipo = factor(tipo, levels = c("generados", "atraidos"),
                       labels = c("Viajes generados (origen)", "Viajes atraídos (destino)")))

ggplot(mapa_long) +
  geom_sf(aes(fill = viajes), colour = "white", linewidth = 0.3) +
  geom_sf_text(aes(label = comuna), size = 2.6) +
  facet_wrap(~ tipo) +
  scale_fill_distiller(palette = "YlOrRd", direction = 1, labels = comma) +
  labs(title = "Viajes en vehículo privado por comuna", fill = "Viajes") +
  theme_void() + theme(legend.position = "bottom", legend.key.width = unit(1.5, "cm"))

4.2 Balance entre atracción y generación

El balance (atraídos − generados) muestra qué comunas reciben más viajes de los que producen (polos de actividad, en rojo) y cuáles producen más de los que reciben (comunas principalmente residenciales, en azul).

lim <- max(abs(comunas_viajes$balance))
ggplot(comunas_viajes) +
  geom_sf(aes(fill = balance), colour = "grey30", linewidth = 0.2) +
  geom_sf_text(aes(label = comuna), size = 2.8) +
  scale_fill_distiller(palette = "RdBu", direction = -1, limits = c(-lim, lim),
                       labels = comma) +
  labs(title = "Balance de viajes: atraídos − generados",
       subtitle = "Rojo: polo atractor · Azul: comuna generadora (residencial)",
       fill = "Balance") +
  theme_void()

4.3 Generación de viajes por cada 1.000 habitantes

Los conteos absolutos dependen del tamaño de la comuna. Dividir por la población permite comparar mejor la intensidad de uso del vehículo privado.

ggplot(comunas_viajes) +
  geom_sf(aes(fill = gen_x1000hab), colour = "white", linewidth = 0.3) +
  geom_sf_text(aes(label = comuna), size = 2.8) +
  scale_fill_viridis_c(option = "magma", direction = -1) +
  labs(title = "Viajes generados por cada 1.000 habitantes (2015)",
       fill = "Viajes /\n1.000 hab") +
  theme_void()

4.4 Motivo del viaje y estrato por comuna

Se calcula, para cada comuna de destino, la proporción de viajes por motivo laboral (trabajar o asuntos de trabajo), y para cada comuna de origen el estrato promedio de los viajeros.

pct_trabajo <- eod %>% filter(!is.na(com_d), !is.na(motivo)) %>%
  group_by(comuna = com_d) %>%
  summarise(pct_trabajo = mean(motivo %in% c(1, 2)))

estrato_origen <- eod %>% filter(!is.na(com_o), !is.na(estrato)) %>%
  group_by(comuna = com_o) %>%
  summarise(estrato_medio = mean(estrato))

comunas_viajes <- comunas_viajes %>%
  left_join(pct_trabajo, by = "comuna") %>%
  left_join(estrato_origen, by = "comuna")

g1 <- ggplot(comunas_viajes) +
  geom_sf(aes(fill = pct_trabajo), colour = "white", linewidth = 0.3) +
  geom_sf_text(aes(label = comuna), size = 2.5) +
  scale_fill_distiller(palette = "Purples", direction = 1, labels = percent) +
  labs(title = "% de viajes atraídos por motivo laboral", fill = NULL) +
  theme_void() + theme(legend.position = "bottom")

g2 <- ggplot(comunas_viajes) +
  geom_sf(aes(fill = estrato_medio), colour = "white", linewidth = 0.3) +
  geom_sf_text(aes(label = comuna), size = 2.5) +
  scale_fill_distiller(palette = "Greens", direction = 1) +
  labs(title = "Estrato medio de quien viaja desde la comuna", fill = NULL) +
  theme_void() + theme(legend.position = "bottom")

g1

g2

cor_tasa_estrato <- with(st_drop_geometry(comunas_viajes),
                         cor.test(gen_x1000hab, estrato_medio, method = "spearman"))
cor_tasa_estrato
## 
##  Spearman's rank correlation rho
## 
## data:  gen_x1000hab and estrato_medio
## S = 1278, p-value = 0.2089
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
##       rho 
## 0.2783738

5 Matriz origen-destino entre comunas

od <- urbanos %>% count(com_o, com_d, name = "viajes")

ggplot(od, aes(x = factor(com_d), y = factor(com_o), fill = viajes)) +
  geom_tile(colour = "white") +
  scale_fill_distiller(palette = "Blues", direction = 1, trans = "log10", labels = comma) +
  labs(title = "Matriz origen-destino entre comunas (escala logarítmica)",
       x = "Comuna de destino", y = "Comuna de origen", fill = "Viajes") +
  coord_equal() + theme_minimal(base_size = 9)

od %>% filter(com_o != com_d) %>% arrange(desc(viajes)) %>% head(10) %>%
  mutate(`% de viajes urbanos` = percent(viajes / nrow(urbanos), 0.1)) %>%
  kable(col.names = c("Comuna origen", "Comuna destino", "Viajes", "% de viajes urbanos"),
        caption = "Los 10 pares origen-destino entre comunas con más viajes")
Los 10 pares origen-destino entre comunas con más viajes
Comuna origen Comuna destino Viajes % de viajes urbanos
19 2 492 2.2%
17 2 340 1.5%
2 19 306 1.4%
17 22 294 1.3%
18 2 256 1.2%
2 3 253 1.1%
3 2 244 1.1%
17 3 236 1.1%
3 19 216 1.0%
18 3 210 0.9%

5.1 Líneas de deseo

Las líneas de deseo unen los centroides de la comuna de origen y la de destino; su grosor es proporcional al número de viajes. Aquí se construye una geometría de líneas a partir de una tabla, igual que en el ejemplo se construyeron puntos a partir de coordenadas de Excel. Se muestran los 40 pares con más viajes, sin contar los viajes dentro de la misma comuna.

centroides <- st_centroid(st_geometry(comunas))
coords_cent <- data.frame(comuna = comunas$comuna, st_coordinates(centroides))

top_od <- od %>% filter(com_o != com_d) %>% slice_max(viajes, n = 40) %>%
  left_join(coords_cent, by = c("com_o" = "comuna")) %>%
  left_join(coords_cent, by = c("com_d" = "comuna"), suffix = c("_o", "_d"))

lineas <- lapply(seq_len(nrow(top_od)), function(i) {
  st_linestring(matrix(c(top_od$X_o[i], top_od$Y_o[i],
                         top_od$X_d[i], top_od$Y_d[i]), ncol = 2, byrow = TRUE))
})
lineas_od <- st_sf(top_od[, c("com_o", "com_d", "viajes")],
                   geometry = st_sfc(lineas, crs = st_crs(comunas)))

ggplot() +
  geom_sf(data = comunas_viajes, aes(fill = atraidos), colour = "white", linewidth = 0.3) +
  geom_sf(data = lineas_od, aes(linewidth = viajes), colour = "black", alpha = 0.55) +
  geom_sf_text(data = comunas, aes(label = comuna), size = 2.6, colour = "grey15") +
  scale_fill_distiller(palette = "YlOrRd", direction = 1, labels = comma) +
  scale_linewidth(range = c(0.3, 3)) +
  labs(title = "Líneas de deseo: 40 principales pares origen-destino",
       fill = "Viajes atraídos", linewidth = "Viajes del par") +
  theme_void()

6 Viajes con origen fuera de Cali

externos <- eod %>% filter(zona_origen == "Fuera de Cali", !is.na(com_d))

# 737 registros marcados "Fuera de Cali" tienen como municipio CALI (p. ej. Dapa o
# vías de salida); se dejan en el conteo por comuna pero no en la tabla de municipios
externos %>% filter(mun_origen != "CALI") %>%
  count(mun_origen, sort = TRUE) %>% head(8) %>%
  kable(col.names = c("Municipio de origen", "Viajes hacia comunas de Cali"),
        caption = "Principales municipios de origen de viajes que entran a Cali")
Principales municipios de origen de viajes que entran a Cali
Municipio de origen Viajes hacia comunas de Cali
YUMBO 1331
JAMUNDI 1055
PALMIRA 813
CANDELARIA 254
LA UNION 111
SANTANDER DE QUILICHAO 79
GUADALAJARA DE BUGA 72
PUERTO TEJADA 52
comunas_ext <- comunas %>%
  left_join(externos %>% count(comuna = com_d, name = "desde_fuera"), by = "comuna")

ggplot(comunas_ext) +
  geom_sf(aes(fill = desde_fuera), colour = "white", linewidth = 0.3) +
  geom_sf_text(aes(label = comuna), size = 2.6) +
  scale_fill_distiller(palette = "OrRd", direction = 1, na.value = "grey90") +
  labs(title = "Comuna de destino de los viajes que llegan desde otros municipios",
       fill = "Viajes") +
  theme_void()

7 Mapa interactivo

Como en el ejemplo, para usar leaflet el shape debe estar en coordenadas geográficas, por eso primero se transforma a WGS84 con st_transform (equivalente a spTransform).

library(leaflet)
comunas_wgs84 <- st_transform(comunas_viajes, crs = 4326)

pal <- colorNumeric("YlOrRd", domain = comunas_wgs84$atraidos)
leaflet(comunas_wgs84) %>%
  addTiles() %>%
  addPolygons(fillColor = ~pal(atraidos), fillOpacity = 0.7,
              color = "white", weight = 1,
              label = ~paste0("Comuna ", comuna, ": ", atraidos, " atraídos / ",
                              generados, " generados"),
              highlightOptions = highlightOptions(weight = 3, color = "black")) %>%
  addLegend(pal = pal, values = ~atraidos, title = "Viajes atraídos")

8 Interpretación de resultados

Antes de interpretar, hay que tener en cuenta cómo se tomaron los datos. Es una encuesta de interceptación: solo incluye vehículos privados (motos, carros, taxis, bicicletas) que pasaron por alguna de las 35 estaciones, entre las 6:00 y las 12:59 de días hábiles de mayo y junio de 2015. Los conteos no se expandieron con los factores de expansión. Por eso los resultados muestran la movilidad privada de la mañana y dependen en parte de dónde se ubicaron las estaciones. No representan todos los viajes de la ciudad.

Perfil del viaje. El viajero típico va en moto (46 %) o automóvil (40 %). Es hombre (80 %), conduce el vehículo (83 %), va solo (66 %), tiene unos 38 años y es de estrato 2 o 3 (65 %). El 61 % de los viajes tiene motivo laboral (trabajar o asuntos de trabajo) y apenas un 14 % es para volver a casa. Esto es lo que se espera en la mañana: la gente sale de su casa hacia el trabajo. Por eso la comuna de origen se puede leer en general como comuna de residencia y la de destino como comuna de actividad.

Generación y atracción (pregunta 1). Los destinos están muy concentrados. Las comunas 2, 3 y 19 atraen el 42 % de los viajes con destino urbano (4.810, 3.859 y 3.158 viajes). La comuna 2 incluye el norte empresarial y comercial (Granada, Chipichape, Centenario), la 3 es el centro tradicional y administrativo, y la 19 concentra clínicas, comercio y servicios (San Fernando, Tequendama). Las mismas comunas 2, 19 y 3 también son las que más viajes generan, junto con la 17, porque tienen mucha actividad y mucho tráfico privado en las dos direcciones.

Polos de actividad frente a comunas residenciales (pregunta 2). El mapa de balance divide la ciudad en dos:

  • Polos atractores (balance positivo): 2 (+1.842), 3 (+1.738), 22 (+707), 4 (+363), 9 (+327) y 19 (+308). La comuna 3 recibe 1,8 viajes por cada viaje que genera. La comuna 22 (Pance, Ciudad Jardín) atrae por sus universidades y colegios.
  • Comunas generadoras o residenciales (balance negativo): las del oriente (13, 14, 15, 21), las de ladera (1, 18, 20) y comunas del sur como la 17 y la 10. La comuna 18 es la que más viajes netos pierde (−874), y la comuna 1 genera 3,4 viajes por cada uno que atrae.

Según el mapa de tasas por habitante, en la comuna 22 (103 viajes por cada 1.000 habitantes) y la 3 (46) el uso del vehículo privado es mucho más intenso que en el oriente (14: 4; 6: 5; 13: 7). Esto coincide con las diferencias de ingreso y de tenencia de carro conocidas en Cali, aunque también influye que hay menos estaciones en esa zona. La relación entre la tasa de generación y el estrato medio del viajero por comuna es positiva pero no significativa (Spearman ρ = 0,28; p = 0,21). Además, el estrato medio varía muy poco entre comunas (2,7 a 3,1), así que el estrato reportado en esta encuesta no basta para explicar las diferencias espaciales. Lo mismo pasa con el porcentaje de viajes por trabajo según la comuna de destino, que va de 57 % a 64 %: el motivo laboral domina en toda la ciudad.

Corredores origen-destino (pregunta 3). Solo el 6,7 % de los viajes urbanos se queda en la misma comuna. Casi todos los viajes en vehículo privado cruzan límites de comuna, y la comuna 2 es la que más viajes internos tiene (17 %). La matriz OD y las líneas de deseo muestran un corredor sur-norte muy marcado. Los pares más fuertes son 19→2 (492 viajes), 17→2 (340), 2→19 (306), 17→22 (294), 18→2 (256) y 17→3 (236). Es decir, el sur residencial (17, 18, 19, 10) se dirige al norte (2) y al centro (3), y la comuna 17 también alimenta el polo universitario de la 22. Las comunas del oriente aparecen poco en las líneas de deseo.

Relación con la región. El 15 % de los viajes llega desde otro municipio, sobre todo Yumbo, Jamundí y Palmira, y se dirige otra vez a las comunas 2, 3 y 19. Esto confirma que estas comunas son centros de empleo para toda el área metropolitana y no solo para Cali.

Calidad de los datos. Las comunas de origen y destino están completas. Sin embargo, 737 registros marcados como “Fuera de Cali” tienen como municipio CALI, y hay 11 horas mal registradas (00:xx). Son pocos casos y no cambian los patrones, pero se deben revisar si se quiere expandir la muestra.

9 Conclusiones

  1. La movilidad privada de la mañana en Cali es radial y converge en pocas comunas. Las comunas 2 (norte), 3 (centro) y 19 reciben más de cuatro de cada diez viajes con destino urbano (42 %), también los que vienen de municipios vecinos. Allí debería concentrarse la gestión de la demanda (estacionamientos, cobros por congestión, priorizar el MIO).
  2. Hay una separación espacial clara entre dónde se vive y dónde se trabaja. Las comunas del oriente, la ladera y el sur generan más viajes de los que atraen, y las del centro-norte lo contrario. Como solo el 6,7 % de los viajes se queda en la misma comuna, casi todos los viajes recorren distancias largas, lo que aumenta la congestión en los corredores.
  3. El corredor sur→norte/centro (comunas 17, 18 y 19 hacia la 2 y la 3) es el principal eje de demanda en vehículo privado, con un ramal hacia el polo universitario de la comuna 22. Es el corredor donde más sentido tiene mejorar el transporte masivo.
  4. El uso del vehículo privado por habitante es muy desigual en el territorio. Es alto en el sur y en el centro, y bajo en el oriente y la ladera, donde la población depende más del transporte público. Las variables de la encuesta (estrato, motivo) no explican por sí solas estas diferencias: hacen falta datos de ingreso o de tenencia de vehículo por comuna.
  5. Integrar la encuesta con el shapefile de comunas permitió ver estos patrones, que no se ven en una tabla de 35.054 registros: los mapas de coropletas, el balance, la tasa por habitante y las líneas de deseo los muestran directamente. Para cuantificarlos mejor, el siguiente paso sería aplicar los factores de expansión y medir la autocorrelación espacial (por ejemplo, con el índice de Moran) de la generación y la atracción de viajes.