Encuesta de Movilidad Uniandina - EMU 2024
1. Configuración inicial
Cargar librerías
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ ggplot2 3.5.1 ✔ tibble 3.2.1
## ✔ lubridate 1.9.4 ✔ tidyr 1.3.1
## ✔ purrr 1.0.4
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(readxl)
library(janitor)
##
## Attaching package: 'janitor'
##
## The following objects are masked from 'package:stats':
##
## chisq.test, fisher.test
library(lubridate)
library(stringr)
library(corrplot)
## corrplot 0.95 loaded
library(scales)
##
## Attaching package: 'scales'
##
## The following object is masked from 'package:purrr':
##
## discard
##
## The following object is masked from 'package:readr':
##
## col_factor
library(GGally)
## Registered S3 method overwritten by 'GGally':
## method from
## +.gg ggplot2
library(ggrepel)
library(showtext)
## Loading required package: sysfonts
## Loading required package: showtextdb
library(sysfonts)
library(ggplot2)
# Fuente (puedes cambiar "Roboto" por "Roboto" si prefieres)
font_add_google("Roboto", "Roboto")
showtext_auto()
# Tema base con Roboto
theme_set(theme_minimal(base_size = 12, base_family = "Roboto"))
# Defaults de texto/label de ggplot2
ggplot2::update_geom_defaults("text", list(family = "Roboto"))
ggplot2::update_geom_defaults("label", list(family = "Roboto"))
# Defaults específicos para ggrepel (¡ojo: pasa el objeto geom!)
ggplot2::update_geom_defaults(ggrepel::GeomTextRepel, list(family = "Roboto"))
ggplot2::update_geom_defaults(ggrepel::GeomLabelRepel, list(family = "Roboto"))
Cargar datos
Cargo la base y el diccionario desde Excel, estandarizo nombres y
limpio formatos. Uso readxl::read_excel()para leer las
hojas, janitor::clean_names()para homogeneizar nombres y
dplyr/stringr para sanear espacios. También convierto
tiempos tipo HH:MM con una función auxiliar
(parse_hhmm) y acoto duraciones imposibles.
# Ajusta la ruta a tu archivo local
data_path <- "C:/Users/tomas/Documents/UniAndes/MCIAM/2. Segundo Semestre/Planeación del Transporte/Talleres R/EMU24_BD.xlsx"
# Se recomienda leer los datos en cada ejecución para asegurar la reproducibilidad
dicc <- read_excel(data_path, sheet = "Diccionario") %>% clean_names()
bd <- read_excel(data_path, sheet = "BD_COD") %>% clean_names()
2. DEFINICIÓN DE FUNCIONES
Convierto códigos numéricos en etiquetas legibles uniendo con el
diccionario. Uso una función get_label_map()para extraer
mapas código→etiqueta y recode_from_dict() (un left_join)
para añadir columnas *_lbl. Esto mejora la legibilidad sin
alterar los códigos originales.
Mapa de etiquetas del diccionario
# Mapa code/label desde el diccionario
get_label_map <- function(dicc_tbl, var_name){
dicc_tbl %>%
filter(variable == var_name) %>%
select(starts_with("opcion")) %>%
pivot_longer(everything(), names_to = "op", values_to = "raw") %>%
filter(!is.na(raw)) %>%
separate(raw, into = c("label","code"), sep = "~", fill = "right") %>%
mutate(
label = str_squish(label),
code = as.numeric(str_squish(code))
) %>%
select(code, label) %>%
distinct()
}
# Re-etiqueta una columna codificada añadiendo *_lbl (evita .x/.y)
recode_from_dict <- function(data, var, map_tbl){
v <- rlang::enquo(var)
col <- rlang::quo_name(v)
lbl <- paste0(col, "_lbl")
left <- data %>%
# normaliza el tipo de la clave (si es factor/chr con dígitos)
mutate(!!col := {
x <- .data[[col]]
if (is.numeric(x)) x else suppressWarnings(as.numeric(as.character(x)))
}) %>%
# si ya existía la etiqueta, elimínala para evitar sufijos .x/.y
select(-any_of(lbl))
right <- map_tbl %>%
rename(!!col := code, !!lbl := label)
dplyr::left_join(left, right, by = col)
}
# Parse horas en "HH:MM", "HH:MM:SS", "hh:mm AM/PM" o fracción del día (0-1)
parse_hhmm <- function(x, tz = "America/Bogota"){
x <- trimws(as.character(x))
base <- as.POSIXct("1970-01-01 00:00:00", tz = tz)
out <- as.POSIXct(NA_real_, origin = "1970-01-01", tz = tz)[seq_along(x)]
# fracción del día
is_num <- suppressWarnings(!is.na(as.numeric(x)))
if (any(is_num)) {
v <- suppressWarnings(as.numeric(x[is_num]))
is_frac <- is.finite(v) & v >= 0 & v <= 1
if (any(is_frac)) {
secs <- v[is_frac] * 86400
out[which(is_num)[is_frac]] <- base + secs
}
}
# formatos horario
need <- is.na(out) & !is.na(x)
if (any(need)) {
dt <- suppressWarnings(
lubridate::parse_date_time(
x[need],
orders = c("H:M:S","H:M","I:M p","I:M:S p"),
tz = tz, truncated = 2
)
)
secs <- hour(dt)*3600 + minute(dt)*60 + second(dt)
out[need] <- base + secs
}
out
}
Re-etiquetar variables clave
# 3.1 Mapas de etiquetas (una sola vez)
map_modo <- get_label_map(dicc, "hacia_modo_ppal")
map_freq <- get_label_map(dicc, "frecuencia_viaje_u")
map_vinculo <- get_label_map(dicc, "vinculo")
map_genero <- get_label_map(dicc, "genero")
map_prog <- get_label_map(dicc, "programa")
# 3.2 Re-etiquetar y estandarizar
emu <- bd %>%
recode_from_dict(hacia_modo_ppal, map_modo) %>%
recode_from_dict(frecuencia_viaje_u, map_freq) %>%
recode_from_dict(vinculo, map_vinculo) %>%
recode_from_dict(genero, map_genero) %>%
recode_from_dict(programa, map_prog) %>%
mutate(
# limpieza etiquetas
across(ends_with("_lbl"), str_squish),
# tiempo de viaje
t_sal = parse_hhmm(hora_salida_hacia),
t_lleg = parse_hhmm(hora_llegada_hacia),
dur_min_hacia = as.numeric(difftime(t_lleg, t_sal, units = "mins")),
dur_min_hacia = if_else(dur_min_hacia < 0 | dur_min_hacia > 300, NA_real_, dur_min_hacia),
# distancia
dist_km = suppressWarnings(as.numeric(distancia_campus_km)),
# variables socio
estrato_int = suppressWarnings(as.integer(estrato)),
estrato_g3 = case_when(
estrato_int %in% 1:2 ~ "Bajo (1-2)",
estrato_int %in% 3:4 ~ "Medio (3-4)",
estrato_int %in% 5:6 ~ "Alto (5-6)",
TRUE ~ NA_character_
),
# flag bici (códigos bicicleta propia 4 y pública 5, según tu nota)
usa_bici = if_else(hacia_modo_ppal %in% c(4,5), 1L, 0L, missing = 0L),
# velocidad
vel_kmh = if_else(is.finite(dist_km) & is.finite(dur_min_hacia) & dur_min_hacia > 0,
60 * dist_km / dur_min_hacia, NA_real_),
# rol7 (coincidencia exacta con etiquetas del diccionario)
rol7 = case_when(
vinculo_lbl == "Profesor de planta" ~ "Profesor de planta",
vinculo_lbl == "Profesor de cátedra" ~ "Profesor de cátedra",
vinculo_lbl == "Empleado administrativo" ~ "Empleado administrativo",
vinculo_lbl == "Estudiante (incluye asistentes graduados)" & programa_lbl == "Pregrado" ~ "Estudiantes - Pregrado",
vinculo_lbl == "Estudiante (incluye asistentes graduados)" & programa_lbl == "Especialización" ~ "Estudiantes - Especialización",
vinculo_lbl == "Estudiante (incluye asistentes graduados)" & programa_lbl == "Maestría" ~ "Estudiantes - Maestría",
vinculo_lbl == "Estudiante (incluye asistentes graduados)" & programa_lbl == "Doctorado" ~ "Estudiantes - Doctorado",
TRUE ~ NA_character_
)
)
# 3.3 Categorías modales por listas cerradas (ajusta estos nombres a tu Diccionario)
lbl_walk <- c("Caminata")
lbl_micro <- c("Bicicleta (propia)","Bicicleta pública (Tembici)","Patineta eléctrica","Patineta eléctrica")
lbl_pub <- c("Transmilenio (con o sin alimentador)","SITP","Bus intermunicipal")
lbl_veh <- c("Carro particular como conductor", "Pasajero(a) en carro con un(a) amigo(a) o un familiar (o miembro de su hogar)")
lbl_moto <- c("Moto (propia)","Moto por aplicación (Picap, Didi moto, etc)")
lbl_taxi <- c("Taxi (incluye si usa Apps para taxis, etc)","Aplicaciones de servicios de transporte (Uber, Cabify, etc.)")
lbl_carp <- c("Carro compartido (grupo WhatsApp, Wheels Uniandes, vecino(a), etc.)")
lvl_modo_cat <- c("Caminata","Micromovilidad","Transp. público","Vehículo particular","Moto","Taxi / App","Carro compartido")
emu <- emu %>%
mutate(
modo_cat = case_when(
hacia_modo_ppal_lbl %in% lbl_carp ~ "Carro compartido",
hacia_modo_ppal_lbl %in% lbl_taxi ~ "Taxi / App",
hacia_modo_ppal_lbl %in% lbl_moto ~ "Moto",
hacia_modo_ppal_lbl %in% lbl_micro ~ "Micromovilidad",
hacia_modo_ppal_lbl %in% lbl_walk ~ "Caminata",
hacia_modo_ppal_lbl %in% lbl_pub ~ "Transp. público",
hacia_modo_ppal_lbl %in% lbl_veh ~ "Vehículo particular",
TRUE ~ NA_character_
),
modo_cat = factor(modo_cat, levels = lvl_modo_cat),
# bins de distancia para cortes
dist_bin = cut(dist_km,
breaks = c(0,2,5,10,15,20,50, Inf),
right = FALSE,
labels = c("[0-2)","[2-5)","[5-10)","[10-15)","[15-20)","[20-50)","50+"))
)
Factor de expansión
Calculo pesos por rol (estudiantes desagregados, profesores,
administrativos) para llevar la muestra a la población objetivo. Con
count() obtengo el n_muestra por estrato/rol,
lo junto con los N poblacionales y defino el peso
w = N_pop / n_muestra. A partir de aquí, todos los
indicadores “de campus” usan weight = w.
# Población objetivo por rol7
pop_uni7 <- tibble(
rol7 = c("Estudiantes - Pregrado","Estudiantes - Especialización","Estudiantes - Maestría","Estudiantes - Doctorado",
"Profesor de planta","Profesor de cátedra","Empleado administrativo"),
N_pop = c(14494, 470, 4012, 419, 726, 755, 2252)
)
# Tabla de pesos y asignación
w_tbl <- emu %>%
filter(!is.na(rol7)) %>%
count(rol7, name = "n_muestra") %>%
left_join(pop_uni7, by = "rol7") %>%
mutate(w_exp = N_pop / n_muestra)
emu <- emu %>%
left_join(w_tbl %>% select(rol7, w_exp), by = "rol7")
3. Análisis de variables
Análisis exploratorio
# Tamaño muestral
tibble(
n_encuestados = nrow(emu),
n_con_modo = sum(!is.na(emu$hacia_modo_ppal)),
n_con_genero = sum(!is.na(emu$genero)),
n_con_estrato = sum(!is.na(emu$estrato_int))
)
## # A tibble: 1 × 4
## n_encuestados n_con_modo n_con_genero n_con_estrato
## <int> <int> <int> <int>
## 1 1278 1022 1058 937
# Por vinculo (rol)
emu %>%
count(rol7, sort = TRUE) %>%
mutate(pct = percent(n/sum(n)))
## # A tibble: 8 × 3
## rol7 n pct
## <chr> <int> <chr>
## 1 Empleado administrativo 356 27.86%
## 2 Estudiantes - Pregrado 345 27.00%
## 3 <NA> 199 15.57%
## 4 Profesor de planta 135 10.56%
## 5 Estudiantes - Maestría 113 8.84%
## 6 Profesor de cátedra 106 8.29%
## 7 Estudiantes - Doctorado 16 1.25%
## 8 Estudiantes - Especialización 8 0.63%
# Distribución modal principal hacia el campus
emu %>%
filter(!is.na(hacia_modo_ppal_lbl)) %>%
count(hacia_modo_ppal_lbl, name = "total") %>%
mutate(pct = total/sum(total)*100) %>%
arrange(desc(total))
## # A tibble: 15 × 3
## hacia_modo_ppal_lbl total pct
## <chr> <int> <dbl>
## 1 Transmilenio (con o sin alimentador) 365 35.7
## 2 Carro particular como conductor 192 18.8
## 3 SITP 90 8.81
## 4 Caminata 76 7.44
## 5 Moto (propia) 62 6.07
## 6 Taxi (incluye si usa Apps para taxis, etc) 43 4.21
## 7 Carro compartido (grupo WhatsApp, Wheels Uniandes, vecino(a), e… 41 4.01
## 8 Pasajero(a) en carro con un(a) amigo(a) o un familiar (o miembr… 41 4.01
## 9 Bicicleta (propia) 39 3.82
## 10 Aplicaciones de servicios de transporte (Uber, Cabify, etc.) 32 3.13
## 11 Bus intermunicipal 15 1.47
## 12 Otro. ¿Cuál? 9 0.881
## 13 Patineta eléctrica 8 0.783
## 14 Moto por aplicación (Picap, Didi moto, etc) 6 0.587
## 15 Bicicleta pública (Tembici) 3 0.294
Análisis de repartición modal
## ---- paletas-de-colores-y-tema ----
# Paleta para las categorías modales principales (variable: modo_cat)
paleta_modos_agregados <- c(
"Caminata" = "#339933",
"Micromovilidad" = "#33cc33",
"Transp. público" = "#2266bf",
"Vehículo particular" = "#505050", # Unificamos "Carro" a tu categoría
"Carro compartido" = "#994499",
"Moto" = "#aa2211",
"Taxi / App" = "#cc6600" # Unificamos "Servicios de transporte" a tu categoría
)
# Paleta para el detalle de sub-modos (variable: micro_sub)
paleta_submodos_micro <- c(
"Bici propia" = "#33cc33",
"Tembici" = "#aaffa3", # Un tono más claro para diferenciar
"Patineta" = "#99d699" # Otro tono de verde para patineta
)
# Tema base personalizado para todos los gráficos del proyecto
theme_uniandes <- function(base_size = 12, base_family = "Roboto") {
theme_minimal(base_size = base_size, base_family = base_family) +
theme(
# Títulos y Ejes
plot.title = element_text(face = "bold", size = rel(1.3), hjust = 0.5),
plot.subtitle = element_text(size = rel(1.1), hjust = 0.5, color = "gray40"),
axis.title = element_text(face = "bold", size = rel(1.0)),
axis.text = element_text(color = "gray30"),
# Leyenda
legend.position = "bottom",
legend.title = element_text(face = "bold"),
# Panel y Grillas
panel.grid.major = element_line(color = "grey92", size = 0.5),
panel.grid.minor = element_blank(),
# Título de Facetas (si se usan)
strip.text = element_text(face = "bold", size = rel(1.0), hjust = 0)
)
}
# Eliminación de Otro y Prefiero no responder
emu <- emu %>%
mutate(
# códigos
across(c(hacia_modo_ppal, frecuencia_viaje_u, genero, estrato_int, programa),
~ ifelse(. %in% c(98, 99), NA, .)),
# etiquetas
across(ends_with("_lbl"),
~ ifelse(. %in% c("Otro", "Prefiero no responder"), NA_character_, .)),
# ingreso (si viene codificado)
ingreso_personal = ifelse(suppressWarnings(as.numeric(ingreso_personal)) %in% c(98, 99),
NA, ingreso_personal)
)
# Repartición modal: antes vs después de la expansión
bind_rows(
emu %>%
filter(!is.na(modo_cat)) %>%
count(modo_cat, name = "total") %>%
mutate(share = total/sum(total), tipo = "Muestra (sin expansión)"),
emu %>%
filter(!is.na(modo_cat), !is.na(w_exp)) %>%
group_by(modo_cat) %>%
summarise(total = sum(w_exp), .groups = "drop") %>%
mutate(share = total/sum(total), tipo = "Expandido (rol7)")
) %>%
ggplot(aes(x = modo_cat, y = share, fill = tipo)) +
geom_col(position = position_dodge(width = .75), width = .75) +
geom_text(aes(label = scales::percent(share, accuracy = 0.1)),
position = position_dodge(width = .75), hjust = -0.1, size = 3.5) +
coord_flip(clip = "off") +
scale_y_continuous(labels = scales::label_percent(),
limits = c(0, 1), expand = expansion(mult = c(.0, .10))) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Repartición modal: antes vs después de la expansión",
x = NULL, y = "%") +
theme(legend.title = element_blank(),
plot.margin = margin(r = 18))

# Repartición modal por ROL (ponderada) + etiquetas Internas
emu %>%
filter(!is.na(rol7), !is.na(modo_cat), !is.na(w_exp)) %>%
group_by(rol7, modo_cat) %>%
summarise(w = sum(w_exp), .groups = "drop_last") %>%
mutate(pct = w / sum(w)) %>%
ungroup() %>%
ggplot(aes(x = rol7, y = pct, fill = modo_cat)) +
geom_col(position = "fill") +
geom_text(aes(label = ifelse(pct >= .07, scales::percent(pct, 1), "")),
position = position_stack(vjust = 0.5), color = "white", size = 3.2) +
coord_flip() +
scale_y_continuous(labels = scales::label_percent()) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Repartición modal por rol (ponderado)",
x = NULL, y = "% dentro del rol", fill = "Categoría modal")

# Repartición modal por ESTRATO (ponderada) + orden Bajo→Medio→Alto
emu %>%
filter(!is.na(estrato_g3), !is.na(modo_cat), !is.na(w_exp)) %>%
mutate(estrato_g3 = factor(estrato_g3, levels = c("Bajo (1-2)","Medio (3-4)","Alto (5-6)"))) %>%
group_by(estrato_g3, modo_cat) %>%
summarise(w = sum(w_exp), .groups = "drop_last") %>%
mutate(pct = w / sum(w)) %>%
ungroup() %>%
ggplot(aes(x = estrato_g3, y = pct, fill = modo_cat)) +
geom_col(position = "fill") +
geom_text(aes(label = ifelse(pct >= .07, scales::percent(pct, 1), "")),
position = position_stack(vjust = 0.5), color = "white", size = 3.2) +
scale_y_continuous(labels = scales::label_percent()) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Repartición modal por grupo de estrato (ponderado)",
x = "Grupo de estrato", y = "% dentro del grupo", fill = "Categoría modal")

# Repartición modal por DISTANCIA (ponderada; por tramos) + etiquetas
emu %>%
filter(!is.na(dist_bin), !is.na(modo_cat), !is.na(w_exp)) %>%
group_by(dist_bin, modo_cat) %>%
summarise(w = sum(w_exp), .groups = "drop_last") %>%
mutate(pct = w / sum(w)) %>%
ungroup() %>%
ggplot(aes(x = dist_bin, y = pct, fill = modo_cat)) +
geom_col(position = "fill") +
geom_text(aes(label = ifelse(pct >= .07, scales::percent(pct, 1), "")),
position = position_stack(vjust = 0.5), color = "white", size = 3.0) +
scale_y_continuous(labels = scales::label_percent()) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Repartición modal por tramos de distancia (ponderado)",
x = "Distancia al campus (km)", y = "% dentro del tramo", fill = "Categoría modal")

# Velocidad por modo
emu %>%
filter(!is.na(vel_kmh), vel_kmh > 0,
vel_kmh <= quantile(vel_kmh, 0.99, na.rm = TRUE),
!is.na(modo_cat)) %>%
ggplot(aes(x = modo_cat, y = vel_kmh, fill = modo_cat)) +
geom_boxplot(width = .7, outlier.alpha = .15) +
stat_summary(
fun = median,
aes(label = scales::number(after_stat(y), accuracy = 0.1)),
geom = "text", vjust = -0.5, size = 3.3, color = "black"
) +
coord_flip(clip = "off") +
scale_fill_brewer(palette = "Set2", guide = "none") +
labs(title = "Velocidad (km/h) por categoría modal",
x = NULL, y = "km/h") +
scale_y_continuous(expand = expansion(mult = c(.02, .08)))

# Velocidad promedio ponderada por modo
emu %>%
filter(!is.na(vel_kmh), !is.na(modo_cat), !is.na(w_exp)) %>%
group_by(modo_cat) %>%
summarise(vel_prom_w = sum(w_exp * vel_kmh, na.rm = TRUE) / sum(w_exp, na.rm = TRUE),
.groups = "drop") %>%
ggplot(aes(x = modo_cat, y = vel_prom_w, fill = modo_cat)) +
geom_col(width = .65) +
geom_text(aes(label = scales::number(vel_prom_w, accuracy = 0.1)),
vjust = -0.25, size = 3.3) +
coord_flip(clip = "off") +
scale_fill_brewer(palette = "Set2", guide = "none") +
labs(title = "Velocidad promedio (ponderada) por categoría modal",
x = NULL, y = "km/h")

Análisis de distancias y tiempos de viaje
# Boxplot tiempos (sin ponderar) + etiqueta mediana
emu %>%
filter(!is.na(dur_min_hacia), !is.na(modo_cat)) %>%
ggplot(aes(x = modo_cat, y = dur_min_hacia, fill = modo_cat)) +
geom_boxplot(outlier.alpha = .2, width = .7) +
stat_summary(fun = median,
aes(label = scales::number(after_stat(y), accuracy = 0.1)),
geom = "text", vjust = -0.5, size = 3.3) +
coord_flip(clip = "off") +
scale_fill_brewer(palette = "Set2", guide = "none") +
labs(title = "Duración hacia el campus por categoría modal",
x = NULL, y = "Minutos") +
scale_y_continuous(expand = expansion(mult = c(.02, .08))) +
theme(plot.margin = margin(r = 18))

# Boxplot distancias + etiqueta mediana
emu %>%
filter(!is.na(dist_km), !is.na(modo_cat)) %>%
ggplot(aes(x = modo_cat, y = dist_km, fill = modo_cat)) +
geom_boxplot(outlier.alpha = .2, width = .7) +
stat_summary(fun = median,
aes(label = scales::number(after_stat(y), accuracy = 0.1)),
geom = "text", vjust = -0.5, size = 3.3) +
coord_flip(clip = "off") +
scale_fill_brewer(palette = "Set2", guide = "none") +
labs(title = "Distancia al campus por categoría modal",
x = NULL, y = "Kilómetros") +
scale_y_continuous(expand = expansion(mult = c(.02, .08))) +
theme(plot.margin = margin(r = 18))

# Dispersión tiempo vs distancia (mantengo LOESS; estilo consistente)
emu %>%
filter(!is.na(dist_km), !is.na(dur_min_hacia), !is.na(modo_cat)) %>%
ggplot(aes(x = dist_km, y = dur_min_hacia, color = modo_cat)) +
geom_point(position = position_jitter(width = .2, height = 2),
alpha = .5, size = 1) +
geom_smooth(method = "loess", se = FALSE, linewidth = .8) +
scale_color_brewer(palette = "Set2") +
labs(title = "Relación tiempo–distancia por categoría modal",
x = "Distancia (km)", y = "Duración (min)", color = "Modo")
## `geom_smooth()` using formula = 'y ~ x'

# Distribución de distancias (ponderada) por estrato
emu %>%
filter(!is.na(dist_km), !is.na(estrato_g3), !is.na(w_exp)) %>%
mutate(estrato_g3 = factor(estrato_g3, levels = c("Bajo (1-2)","Medio (3-4)","Alto (5-6)"))) %>%
{ d <- .; p99 <- suppressWarnings(quantile(d$dist_km, .99, na.rm = TRUE));
xmax <- if (is.finite(p99)) p99 else max(d$dist_km, na.rm = TRUE);
ggplot(d, aes(x = dist_km, color = estrato_g3, fill = estrato_g3, weight = w_exp)) +
geom_density(alpha = .18) +
scale_color_brewer(palette = "Set2") +
scale_fill_brewer(palette = "Set2", guide = "none") +
labs(title = "Distribución de distancias al campus por estrato (ponderado)",
x = "Distancia (km)", y = "Densidad ponderada",
color = "Estrato") +
coord_cartesian(xlim = c(0, xmax))
}

Análisis bivariado
# Frecuencia vs repartición modal (ponderado) + etiquetas
emu %>%
filter(!is.na(frecuencia_viaje_u_lbl), !is.na(modo_cat), !is.na(w_exp)) %>%
group_by(frecuencia_viaje_u_lbl, modo_cat) %>%
summarise(w = sum(w_exp), .groups = "drop_last") %>%
mutate(pct = w / sum(w)) %>%
ungroup() %>%
ggplot(aes(x = frecuencia_viaje_u_lbl, y = pct, fill = modo_cat)) +
geom_col(position = "fill") +
geom_text(aes(label = ifelse(pct >= .07, scales::percent(pct, 1), "")),
position = position_stack(vjust = 0.5), color = "white", size = 3) +
coord_flip() +
scale_y_continuous(labels = scales::label_percent()) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Frecuencia de asistencia a campus vs repartición modal (ponderado)",
x = "Frecuencia (días/semana)", y = "% dentro de la frecuencia", fill = "Modo")

# Género vs repartición modal (ponderado) + etiquetas
emu %>%
filter(!is.na(genero_lbl), !is.na(modo_cat), !is.na(w_exp)) %>%
group_by(genero_lbl, modo_cat) %>%
summarise(w = sum(w_exp), .groups = "drop_last") %>%
mutate(pct = w / sum(w)) %>%
ungroup() %>%
ggplot(aes(x = genero_lbl, y = pct, fill = modo_cat)) +
geom_col(position = "fill") +
geom_text(aes(label = ifelse(pct >= .07, scales::percent(pct, 1), "")),
position = position_stack(vjust = 0.5), color = "white", size = 3) +
scale_y_continuous(labels = scales::label_percent()) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Género vs repartición modal (ponderado)",
x = "Género", y = "% dentro del género", fill = "Modo")

# Ingreso personal vs distancia y tiempo
emu_ing <- emu %>%
mutate(ingreso_num = suppressWarnings(as.numeric(ingreso_personal)),
ingreso_num = if_else(ingreso_num %in% c(98,99), NA_real_, ingreso_num),
ingreso_m = ingreso_num/1e6)
# Ingreso vs distancia
# Boxplots de duración por quintiles de ingreso, con rangos (M COP/mes) anotados
# Datos limpios básicos
d <- emu %>%
mutate(
ingreso_num = suppressWarnings(as.numeric(ingreso_personal)),
ingreso_m = if_else(ingreso_num %in% c(98, 99), NA_real_, ingreso_num) / 1e6,
dur_min = suppressWarnings(as.numeric(dur_min_hacia))
) %>%
filter(is.finite(ingreso_m), is.finite(dur_min), dur_min > 0)
# Quintiles y etiquetas
brks <- unique(quantile(d$ingreso_m, probs = seq(0, 1, 0.2), na.rm = TRUE))
labs <- paste0("Q", seq_len(length(brks) - 1))
d$ing_q <- cut(d$ingreso_m, breaks = brks, include.lowest = TRUE, labels = labs)
# Rango de anotación (truncamos al p99 para lectura)
y99 <- quantile(d$dur_min, 0.99, na.rm = TRUE)
lab_txt <- paste0(
format(round(brks[-length(brks)], 1), nsmall = 1), "–",
format(round(brks[-1], 1), nsmall = 1), " M"
)
ggplot(d, aes(x = ing_q, y = pmin(dur_min, y99), fill = ing_q)) +
geom_boxplot(width = 0.65, outlier.alpha = 0.15) +
stat_summary(fun = median, geom = "point", size = 2, color = "black") +
annotate("text",
x = seq_along(labs),
y = y99 * 1.05,
label = lab_txt,
size = 3.6) +
stat_summary(fun = median,
aes(label = scales::number(after_stat(y), accuracy = 0.1)),
geom = "text", vjust = -0.5, size = 3.3) +
scale_fill_brewer(palette = "Set2", guide = "none") +
labs(
title = "Duración del viaje por quintiles de ingreso",
subtitle = "Rango del quintil en millones COP/mes",
x = "Quintil de ingreso",
y = "Duración (min)"
) +
theme_minimal(base_size = 12) +
coord_cartesian(ylim = c(NA, y99 * 1.12))

Gráfica bivariada
emu %>%
transmute(
Dist_km = as.numeric(dist_km),
Dur_min = as.numeric(dur_min_hacia),
Freq_u = as.numeric(frecuencia_viaje_u),
LogIng = log1p(suppressWarnings(as.numeric(ingreso_personal))),
Edad = suppressWarnings(2024 - as.numeric(ano_nacimiento))
) %>%
filter(is.finite(Dist_km), is.finite(Dur_min), is.finite(Freq_u), is.finite(LogIng), is.finite(Edad)) %>%
sample_n(size = min(600, n()), replace = FALSE) %>%
GGally::ggpairs(title = "Exploración bivariada: Distancia, Duración, Frecuencia, Ingreso (log), Edad")

0
## [1] 0
4. Análisis específico de micromovilidad
Micromovilidad (bici propia, Tembici, patineta eléctrica)
## 4. Análisis específico de micromovilidad
### Micromovilidad (bici propia, Tembici, patineta eléctrica)
# Etiquetas (ajusta si tu diccionario difiere)
lbl_bici_prop <- c("Bicicleta (propia)")
lbl_bici_pub <- c("Bicicleta pública (Tembici)")
lbl_patineta <- c("Patineta eléctrica", "Patineta (eléctrica)")
# Estandarizo variables SOLO una vez (submodo, estrato ordenado, rol3)
emu <- emu %>%
mutate(
micro_sub = case_when(
hacia_modo_ppal_lbl %in% lbl_bici_prop ~ "Bici propia",
hacia_modo_ppal_lbl %in% lbl_bici_pub ~ "Tembici",
hacia_modo_ppal_lbl %in% lbl_patineta ~ "Patineta",
TRUE ~ NA_character_
),
micro_sub = factor(micro_sub, levels = c("Bici propia","Tembici","Patineta")),
estrato_g3 = factor(estrato_g3, levels = c("Bajo (1-2)", "Medio (3-4)", "Alto (5-6)")),
rol3 = case_when(
rol7 %in% c("Profesor de planta","Profesor de cátedra") ~ "Profesores",
rol7 == "Empleado administrativo" ~ "Administrativos",
str_detect(rol7, "^Estudiantes") ~ "Estudiantes",
TRUE ~ NA_character_
),
rol3 = factor(rol3, levels = c("Estudiantes","Profesores","Administrativos"))
)
# 4.1 Pie: composición Interna de micromovilidad (ponderada)
emu %>%
filter(!is.na(micro_sub), !is.na(w_exp)) %>%
group_by(micro_sub) %>%
summarise(w = sum(w_exp), .groups = "drop") %>%
mutate(share = w / sum(w),
lbl = paste0(micro_sub, "\n", scales::percent(share, accuracy = 0.1))) %>%
ggplot(aes(x = "", y = share, fill = micro_sub)) +
geom_col(width = 1, color = "white") +
coord_polar(theta = "y") +
geom_text(aes(label = lbl), color = "white",
position = position_stack(vjust = 0.5), size = 3.6, lineheight = 0.9) +
scale_fill_brewer(palette = "Set2", guide = "none") +
labs(title = "Micromovilidad: composición ponderada", x = NULL, y = NULL) +
theme(axis.text = element_blank(), panel.grid = element_blank())

# 4.2 Barras por GÉNERO (conteos ponderados + % Internos)
emu %>%
filter(!is.na(genero_lbl), !is.na(micro_sub), !is.na(w_exp)) %>%
group_by(genero_lbl, micro_sub) %>%
summarise(w = sum(w_exp), .groups = "drop_last") %>%
mutate(pct_Interno = w / sum(w)) %>%
ungroup() %>%
ggplot(aes(x = genero_lbl, y = w, fill = micro_sub)) +
geom_col(width = .75) +
geom_text(aes(label = scales::percent(pct_Interno, accuracy = 1)),
position = position_stack(vjust = 0.5), size = 3, color = "white") +
scale_fill_brewer(palette = "Set2") +
labs(title = "Micromovilidad por género (ponderado)",
x = "Género", y = "Viajes (ponderados)", fill = "Submodo")

# 4.3 Barras por ESTRATO (orden Bajo→Medio→Alto)
emu %>%
filter(!is.na(estrato_g3), !is.na(micro_sub), !is.na(w_exp)) %>%
group_by(estrato_g3, micro_sub) %>%
summarise(w = sum(w_exp), .groups = "drop_last") %>%
mutate(pct_Interno = w / sum(w)) %>%
ungroup() %>%
ggplot(aes(x = estrato_g3, y = w, fill = micro_sub)) +
geom_col(width = .75) +
geom_text(aes(label = scales::percent(pct_Interno, accuracy = 1)),
position = position_stack(vjust = 0.5), size = 3, color = "white") +
scale_fill_brewer(palette = "Set2") +
labs(title = "Micromovilidad por grupo de estrato (ponderado)",
x = "Estrato (agrupado)", y = "Viajes (ponderados)", fill = "Submodo")

# 4.4 Barras por VÍNCULO (rol3): conteo + % Internos
emu %>%
filter(!is.na(rol3), !is.na(micro_sub), !is.na(w_exp)) %>%
group_by(rol3, micro_sub) %>%
summarise(w = sum(w_exp), .groups = "drop_last") %>%
mutate(pct_Interno = w / sum(w)) %>%
ungroup() %>%
ggplot(aes(x = rol3, y = w, fill = micro_sub)) +
geom_col(width = .75) +
geom_text(aes(label = scales::percent(pct_Interno, accuracy = 1)),
position = position_stack(vjust = 0.5), size = 3, color = "white") +
scale_fill_brewer(palette = "Set2") +
labs(title = "Micromovilidad por vínculo (ponderado)",
x = "Vínculo", y = "Viajes (ponderados)", fill = "Submodo")

# 4.5 Tenencia de bicicleta por ESTRATO (porcentaje ponderado)
emu %>%
mutate(tiene_bici = suppressWarnings(as.numeric(tenenencia_bicicleta)),
tiene_bici = as.integer(replace_na(tiene_bici, 0) > 0)) %>%
filter(!is.na(estrato_g3), !is.na(w_exp)) %>%
group_by(estrato_g3) %>%
summarise(pct_tiene = sum(w_exp * tiene_bici, na.rm = TRUE) / sum(w_exp, na.rm = TRUE),
.groups = "drop") %>%
ggplot(aes(x = estrato_g3, y = pct_tiene)) +
geom_col(width = .75, fill = "#66C2A5") +
geom_text(aes(label = scales::percent(pct_tiene, accuracy = 0.1)),
vjust = -0.25, size = 3.3) +
scale_y_continuous(labels = scales::percent_format(), limits = c(0, NA),
expand = expansion(mult = c(.02, .08))) +
labs(title = "Tenencia de bicicleta por grupo de estrato (ponderado)",
x = "Estrato (agrupado)", y = "% con bicicleta")

# 4.6 Micromovilidad por DISTANCIA (tramos, % Internos dentro de cada tramo)
emu %>%
mutate(
dist_bin = cut(
dist_km,
breaks = c(0, 2, 5, 10, 15, 20, 50, Inf),
right = FALSE,
labels = c("[0-2)","[2-5)","[5-10)","[10-15)","[15-20)","[20-50)","50+")
)
) %>%
filter(!is.na(dist_bin), !is.na(micro_sub), !is.na(w_exp)) %>%
group_by(dist_bin, micro_sub) %>%
summarise(w = sum(w_exp, na.rm = TRUE), .groups = "drop_last") %>%
mutate(pct = w / sum(w)) %>% # % dentro de cada tramo (sigue agrupado por dist_bin)
ungroup() %>%
ggplot(aes(x = dist_bin, y = pct, fill = micro_sub)) +
geom_col(position = "fill", width = .8) +
geom_text(
aes(label = ifelse(pct >= 0.07, scales::percent(pct, accuracy = 1), "")),
position = position_stack(vjust = 0.5),
size = 3, color = "white", lineheight = 0.9
) +
scale_y_continuous(labels = scales::percent_format()) +
scale_fill_brewer(palette = "Set2") +
labs(
title = "Micromovilidad por tramos de distancia (ponderado)",
subtitle = "% Interno dentro de cada tramo",
x = "Distancia al campus (km)",
y = "% dentro del tramo",
fill = "Submodo"
)

# 4.7 Km TOTALES en micromovilidad por VÍNCULO (suma ponderada de km)
# Km totales en micromovilidad por vínculo (ponderado) con submodos
{
d <- emu %>%
filter(!is.na(rol3), !is.na(micro_sub), !is.na(w_exp),
is.finite(dist_km), dist_km >= 0) %>%
mutate(rol3 = factor(rol3, levels = c("Estudiantes","Profesores","Administrativos"))) %>%
group_by(rol3, micro_sub) %>%
summarise(km_tot = sum(w_exp * dist_km, na.rm = TRUE), .groups = "drop")
tot <- d %>% group_by(rol3) %>% summarise(km_tot = sum(km_tot), .groups = "drop")
ggplot(d, aes(x = rol3, y = km_tot, fill = micro_sub)) +
geom_col(width = .75) +
# etiqueta por segmento (texto blanco dentro de cada submodo)
geom_text(aes(label = scales::number(km_tot, accuracy = 1)),
position = position_stack(vjust = 0.5), color = "white", size = 3) +
# etiqueta de TOTAL por barra (encima)
geom_text(data = tot,
aes(x = rol3, y = km_tot,
label = scales::number(km_tot, accuracy = 1)),
vjust = -0.25, size = 3.5, inherit.aes = FALSE) +
scale_fill_brewer(palette = "Set2") +
labs(title = "Kilómetros totales en micromovilidad por vínculo (ponderado)",
x = "Vínculo", y = "km totales (ponderados)", fill = "Submodo") +
theme_minimal(base_size = 12)
}

# 4.8 Cantidad de viajes de micromovilidad vs. % modal dentro de cada rol
# Micromovilidad por vínculo: cantidad vs % modal (ambos ponderados)
{
d <- emu %>%
mutate(es_micro = !is.na(micro_sub)) %>%
filter(!is.na(rol3), !is.na(w_exp)) %>%
group_by(rol3) %>%
summarise(
viajes_micro_w = sum(w_exp * es_micro, na.rm = TRUE),
pct_micro_w = sum(w_exp * es_micro, na.rm = TRUE) / sum(w_exp, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(rol3 = factor(rol3, levels = c("Estudiantes","Profesores","Administrativos")))
k <- max(d$viajes_micro_w, na.rm = TRUE) # escala para el eje secundario
ggplot(d, aes(x = rol3)) +
# BARRA: cantidad de viajes (ponderados)
geom_col(aes(y = viajes_micro_w, fill = "Cantidad de viajes (micro)"), width = .65) +
geom_text(aes(y = viajes_micro_w, label = scales::number(viajes_micro_w, accuracy = 1)),
vjust = -0.25, size = 3.5) +
# PUNTO: % modal (ponderado) reescalado al eje primario
geom_point(aes(y = pct_micro_w * k, shape = "% modal de micromovilidad"), size = 3) +
geom_text(aes(y = pct_micro_w * k,
label = scales::percent(pct_micro_w, accuracy = 0.1)),
vjust = 1.5, size = 3.5) +
scale_y_continuous(
name = "Cantidad (ponderada)",
limits = c(0, k * 1.15),
sec.axis = sec_axis(~ . / k, name = "% modal (ponderado)", labels = scales::percent)
) +
scale_fill_manual(values = c("Cantidad de viajes (micro)" = "#69b3a2")) +
scale_shape_manual(values = c("% modal de micromovilidad" = 16)) +
labs(title = "Micromovilidad por vínculo: cantidad vs. participación modal (ponderado)",
x = "Vínculo", fill = NULL, shape = NULL) +
theme_minimal(base_size = 12)
}

5. Modelo logístico de uso de bicicleta
Modelo logístico
bd_model <- emu %>%
transmute(
usa_bici = if_else(hacia_modo_ppal %in% c(4,5), 1L, 0L, missing = 0L),
genero = genero_lbl, # ya etiquetado si lo necesitas como factor
estrato_n = if_else(estrato_int %in% 1:6, estrato_int, NA_integer_),
estrato = as.numeric(estrato_n),
dist_km = dist_km,
freq_u = suppressWarnings(as.numeric(frecuencia_viaje_u)),
ten_bici = replace_na(suppressWarnings(as.numeric(tenenencia_bicicleta)), 0),
ten_car = as.integer(replace_na(suppressWarnings(as.numeric(tenencia_carro_camioneta)), 0) > 0),
ingreso = suppressWarnings(as.numeric(ingreso_personal)),
ingreso = if_else(ingreso %in% c(98,99), NA_real_, ingreso),
log_ing = log1p(ingreso),
edad = suppressWarnings(2024 - as.numeric(ano_nacimiento)),
) %>%
select(-estrato_n) %>%
drop_na(usa_bici, genero, estrato, dist_km, freq_u, ten_bici, ten_car, log_ing, edad)
modelo_bici <- glm(
usa_bici ~ genero + estrato + dist_km + freq_u + ten_bici + ten_car + log_ing + edad,
data = bd_model, family = binomial()
)
summary(modelo_bici)
##
## Call:
## glm(formula = usa_bici ~ genero + estrato + dist_km + freq_u +
## ten_bici + ten_car + log_ing + edad, family = binomial(),
## data = bd_model)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -7.13488 3.82477 -1.865 0.06212 .
## generoMasculino 1.34829 0.45170 2.985 0.00284 **
## estrato 0.33980 0.19432 1.749 0.08035 .
## dist_km -0.09901 0.04512 -2.195 0.02819 *
## freq_u -0.13285 0.13641 -0.974 0.33011
## ten_bici 0.61985 0.13150 4.714 2.43e-06 ***
## ten_car -1.87229 0.48123 -3.891 1.00e-04 ***
## log_ing 0.12559 0.26976 0.466 0.64153
## edad 0.03551 0.01947 1.824 0.06819 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 278.64 on 769 degrees of freedom
## Residual deviance: 213.10 on 761 degrees of freedom
## AIC: 231.1
##
## Number of Fisher Scoring iterations: 7
modelo_nulo <- glm(usa_bici ~ 1, data = bd_model, family = binomial())
rho2 <- 1 - as.numeric(logLik(modelo_bici) / logLik(modelo_nulo))
rho2
## [1] 0.2351931
Matriz de correlación
# Matriz de diseño solo con predictores (incluye dummies de estrato; excluye Robotocepto)
X <- model.matrix(~ genero + estrato + dist_km + freq_u + ten_bici + ten_car + log_ing + edad,
data = bd_model)[, -1, drop = FALSE]
# Quitar columnas constantes (sd = 0) por seguridad
X <- X[, apply(X, 2, sd, na.rm = TRUE) > 0, drop = FALSE]
# Correlaciones (pairwise para tolerar algún NA residual)
M <- cor(X, use = "pairwise.complete.obs")
# Corrplot (triángulo superior, coeficientes sobre las celdas)
corrplot(M, method = "color", type = "upper", tl.col = "black",
tl.cex = 0.8, addCoef.col = "black", number.cex = 0.6, diag = FALSE)

Exportar datos para análisis geográfico en ArcGIS
# Helper por si lo quieres reutilizar
as_num <- function(x) suppressWarnings(as.numeric(x))
emu <- emu %>%
mutate(
dist_km = as_num(dist_km),
dur_min_hacia = as_num(dur_min_hacia),
longitude = as_num(longitude),
latitude = as_num(latitude),
ingreso_personal = as_num(ingreso_personal),
# la w ya la definimos arriba
)
# 3) Exportar. IMPORTANTE: na="" para no escribir "NA" como texto
emu %>%
select(encuesta_no, longitude, latitude,
genero_lbl, estrato_g3, rol3, rol7,
hacia_modo_ppal_lbl, modo_cat, micro_sub, frecuencia_viaje_u_lbl,
ingreso_personal, dur_min_hacia, dist_km, vel_kmh, w_exp) %>%
readr::write_csv("EMU24_puntos_WGS84.csv", na = "")
Lectura de datos georreferenciados
library(readr)
library(dplyr)
library(scales)
df <- read_csv("EMU24gdb.csv", show_col_types = FALSE)
# Top 10 Localidades (ponderado)
top_loc <- df %>%
filter(!is.na(Localidad)) %>%
group_by(Localidad) %>%
summarise(viajes = sum(w, na.rm = TRUE), .groups = "drop") %>%
mutate(pct = viajes / sum(viajes)) %>%
arrange(desc(viajes)) %>%
slice_head(n = 10) %>%
transmute(Localidad,
viajes = round(viajes),
`% del total` = percent(pct, accuracy = 0.1))
# Top 10 UPZ (ponderado) filtrado por estratos altos
top_upz <- df %>%
filter(!is.na(NOMBRE_1) & estrato_g3 == "Bajo (1-2)") %>%
group_by(NOMBRE_1) %>%
summarise(viajes = sum(w, na.rm = TRUE), .groups = "drop") %>%
mutate(pct = viajes / sum(viajes)) %>%
arrange(desc(viajes)) %>%
slice_head(n = 10) %>%
transmute(NOMBRE_1,
viajes = round(viajes),
`% del total` = percent(pct, accuracy = 0.1))
top_loc
## # A tibble: 10 × 3
## Localidad viajes `% del total`
## <chr> <dbl> <chr>
## 1 SUBA 2103 26.0%
## 2 USAQUEN 1979 24.5%
## 3 ENGATIVA 724 9.0%
## 4 CHAPINERO 670 8.3%
## 5 KENNEDY 584 7.2%
## 6 FONTIBON 368 4.6%
## 7 BOSA 355 4.4%
## 8 TEUSAQUILLO 254 3.1%
## 9 BARRIOS UNIDOS 245 3.0%
## 10 CIUDAD BOLIVAR 158 2.0%
top_upz
## # A tibble: 10 × 3
## NOMBRE_1 viajes `% del total`
## <chr> <dbl> <chr>
## 1 BOSA OCCIDENTAL 99 9.2%
## 2 BOSA CENTRAL 94 8.8%
## 3 COMUNEROS 85 8.0%
## 4 CALANDAIMA 57 5.3%
## 5 PATIO BONITO 48 4.5%
## 6 TIBABUYES 48 4.5%
## 7 ZONA FRANCA 40 3.8%
## 8 TIMIZA 32 3.0%
## 9 LUCERO 32 3.0%
## 10 CASTILLA 31 2.9%
emu %>%
filter(!is.na(micro_sub), !is.na(w_exp)) %>%
group_by(micro_sub) %>%
summarise(viajes_w = sum(w_exp), .groups = "drop") %>%
mutate(`% del total micro` = viajes_w / sum(viajes_w)) %>%
arrange(desc(viajes_w)) %>%
mutate(viajes_w = round(viajes_w),
`% del total micro` = scales::percent(`% del total micro`, 0.1)) %>%
knitr::kable(caption = "Composición de micromovilidad (viajes ponderados)")
Composición de micromovilidad (viajes ponderados)
| Bici propia |
398 |
71.2% |
| Patineta |
106 |
19.0% |
| Tembici |
55 |
9.7% |
emu %>%
filter(!is.na(genero_lbl), !is.na(micro_sub), !is.na(w_exp)) %>%
group_by(genero_lbl, micro_sub) %>%
summarise(viajes_w = sum(w_exp), .groups = "drop_last") %>%
mutate(`% dentro del género` = viajes_w / sum(viajes_w)) %>%
ungroup() %>%
arrange(genero_lbl, desc(viajes_w)) %>%
mutate(viajes_w = round(viajes_w),
`% dentro del género` = scales::percent(`% dentro del género`, 0.1)) %>%
knitr::kable(caption = "Micromovilidad por género (ponderado)")
Micromovilidad por género (ponderado)
| Femenino |
Bici propia |
79 |
48.5% |
| Femenino |
Tembici |
42 |
25.8% |
| Femenino |
Patineta |
42 |
25.7% |
| Masculino |
Bici propia |
306 |
79.9% |
| Masculino |
Patineta |
64 |
16.8% |
| Masculino |
Tembici |
13 |
3.3% |
emu %>%
filter(!is.na(estrato_g3), !is.na(micro_sub), !is.na(w_exp)) %>%
group_by(estrato_g3, micro_sub) %>%
summarise(viajes_w = sum(w_exp), .groups = "drop_last") %>%
mutate(`% dentro del estrato` = viajes_w / sum(viajes_w)) %>%
ungroup() %>%
arrange(estrato_g3, desc(viajes_w)) %>%
mutate(viajes_w = round(viajes_w),
`% dentro del estrato` = scales::percent(`% dentro del estrato`, 0.1)) %>%
knitr::kable(caption = "Micromovilidad por grupo de estrato (ponderado)")
Micromovilidad por grupo de estrato (ponderado)
| Bajo (1-2) |
Bici propia |
101 |
100.0% |
| Medio (3-4) |
Bici propia |
156 |
68.7% |
| Medio (3-4) |
Patineta |
64 |
28.2% |
| Medio (3-4) |
Tembici |
7 |
3.1% |
| Alto (5-6) |
Bici propia |
93 |
51.0% |
| Alto (5-6) |
Tembici |
47 |
26.0% |
| Alto (5-6) |
Patineta |
42 |
23.0% |
emu %>%
filter(!is.na(rol3), !is.na(micro_sub), !is.na(w_exp)) %>%
group_by(rol3, micro_sub) %>%
summarise(viajes_w = sum(w_exp), .groups = "drop_last") %>%
mutate(`% dentro del vínculo` = viajes_w / sum(viajes_w)) %>%
ungroup() %>%
arrange(rol3, desc(viajes_w)) %>%
mutate(viajes_w = round(viajes_w),
`% dentro del vínculo` = scales::percent(`% dentro del vínculo`, 0.1)) %>%
knitr::kable(caption = "Micromovilidad por vínculo (ponderado)")
Micromovilidad por vínculo (ponderado)
| Estudiantes |
Bici propia |
188 |
61.1% |
| Estudiantes |
Patineta |
78 |
25.2% |
| Estudiantes |
Tembici |
42 |
13.7% |
| Profesores |
Bici propia |
141 |
83.1% |
| Profesores |
Patineta |
16 |
9.5% |
| Profesores |
Tembici |
13 |
7.4% |
| Administrativos |
Bici propia |
70 |
84.6% |
| Administrativos |
Patineta |
13 |
15.4% |
emu %>%
mutate(dist_bin = cut(
dist_km,
breaks = c(0,2,5,10,15,20,50, Inf),
right = FALSE,
labels = c("[0-2)","[2-5)","[5-10)","[10-15)","[15-20)","[20-50)","50+")
)) %>%
filter(!is.na(dist_bin), !is.na(micro_sub), !is.na(w_exp)) %>%
group_by(dist_bin, micro_sub) %>%
summarise(viajes_w = sum(w_exp), .groups = "drop_last") %>%
mutate(`% dentro del tramo` = viajes_w / sum(viajes_w)) %>%
ungroup() %>%
arrange(dist_bin, desc(viajes_w)) %>%
mutate(viajes_w = round(viajes_w),
`% dentro del tramo` = scales::percent(`% dentro del tramo`, 0.1)) %>%
knitr::kable(caption = "Micromovilidad por tramos de distancia (ponderado)")
Micromovilidad por tramos de distancia (ponderado)
| [0-2) |
Patineta |
36 |
58.5% |
| [0-2) |
Bici propia |
25 |
41.5% |
| [2-5) |
Bici propia |
117 |
90.4% |
| [2-5) |
Tembici |
7 |
5.5% |
| [2-5) |
Patineta |
5 |
4.2% |
| [5-10) |
Bici propia |
114 |
65.8% |
| [5-10) |
Tembici |
47 |
27.4% |
| [5-10) |
Patineta |
12 |
6.8% |
| [10-15) |
Bici propia |
88 |
62.1% |
| [10-15) |
Patineta |
54 |
37.9% |
| [15-20) |
Bici propia |
13 |
100.0% |
# Detalle por submodo
emu %>%
filter(!is.na(rol3), !is.na(micro_sub), !is.na(w_exp),
is.finite(dist_km), dist_km >= 0) %>%
group_by(rol3, micro_sub) %>%
summarise(km_tot_w = sum(w_exp * dist_km), .groups = "drop") %>%
arrange(rol3, desc(km_tot_w)) %>%
mutate(km_tot_w = round(km_tot_w)) %>%
knitr::kable(caption = "Km totales ponderados en micromovilidad por vínculo y submodo")
Km totales ponderados en micromovilidad por vínculo y
submodo
| Estudiantes |
Bici propia |
898 |
| Estudiantes |
Patineta |
592 |
| Estudiantes |
Tembici |
263 |
| Profesores |
Bici propia |
1197 |
| Profesores |
Patineta |
130 |
| Profesores |
Tembici |
62 |
| Administrativos |
Bici propia |
393 |
| Administrativos |
Patineta |
118 |
# Totales por vínculo
emu %>%
filter(!is.na(rol3), !is.na(micro_sub), !is.na(w_exp),
is.finite(dist_km), dist_km >= 0) %>%
group_by(rol3) %>%
summarise(km_tot_w = sum(w_exp * dist_km), .groups = "drop") %>%
arrange(desc(km_tot_w)) %>%
mutate(km_tot_w = round(km_tot_w)) %>%
knitr::kable(caption = "Km totales ponderados en micromovilidad por vínculo (TOTAL)")
Km totales ponderados en micromovilidad por vínculo
(TOTAL)
| Estudiantes |
1753 |
| Profesores |
1388 |
| Administrativos |
511 |
emu %>%
mutate(es_micro = as.integer(!is.na(micro_sub))) %>%
filter(!is.na(rol3), !is.na(w_exp)) %>%
group_by(rol3) %>%
summarise(
viajes_micro_w = sum(w_exp * es_micro, na.rm = TRUE),
viajes_tot_w = sum(w_exp, na.rm = TRUE),
pct_micro_w = viajes_micro_w / viajes_tot_w,
.groups = "drop"
) %>%
arrange(desc(viajes_micro_w)) %>%
mutate(
viajes_micro_w = round(viajes_micro_w),
viajes_tot_w = round(viajes_tot_w),
`% modal micro` = scales::percent(pct_micro_w, 0.1)
) %>%
select(rol3, viajes_micro_w, viajes_tot_w, `% modal micro`) %>%
knitr::kable(caption = "Cantidad de viajes de micro y % modal (ponderado) por vínculo")
Cantidad de viajes de micro y % modal (ponderado) por
vínculo
| Estudiantes |
307 |
19395 |
1.6% |
| Profesores |
170 |
1481 |
11.5% |
| Administrativos |
82 |
2252 |
3.7% |
# Total viajes ponderados (usa todas las filas con peso)
emu %>%
filter(is.finite(w_exp)) %>%
summarise(total_viajes_w = sum(w_exp))
## # A tibble: 1 × 1
## total_viajes_w
## <dbl>
## 1 23128
# Promedio ponderado de duración (min)
emu %>%
filter(is.finite(w_exp),
is.finite(dur_min_hacia), dur_min_hacia > 0, dur_min_hacia <= 300) %>%
summarise(prom_duracion_min_w = sum(w_exp * dur_min_hacia) / sum(w_exp))
## # A tibble: 1 × 1
## prom_duracion_min_w
## <dbl>
## 1 62.0
# Promedio ponderado de distancia (km)
emu %>%
filter(is.finite(w_exp),
is.finite(dist_km), dist_km >= 0) %>%
summarise(prom_distancia_km_w = sum(w_exp * dist_km) / sum(w_exp))
## # A tibble: 1 × 1
## prom_distancia_km_w
## <dbl>
## 1 9.65
6. Proyección de reparto modal por rol hasta 2035 (escenario “hágase
la voluntad”)
## ---- do-nothing-by-role-setup, message=FALSE, warning=FALSE ------------------
library(tidyverse)
library(lubridate)
library(scales)
# === Rutas de entrada (ajústalas según tus archivos) ===
pth_pop_hist <- "uniandes_poblacion_2010_2024_extendida.csv" # columnas: Año + 7 categorías
pth_pop_proj_opt <- NA # opcional: "proyeccion_poblacion_2025_2035.csv"
# Horizonte
horizonte <- 2035
years_proj <- 2025:horizonte
base_year <- 2024
# === Mapeo: categorías poblacionales -> etiquetas EXACTAS de rol7 en tu Rmd ===
map_roles <- tribble(
~cat_pob, ~rol7,
"Estudiantes de pregrado", "Estudiantes - Pregrado",
"Estudiantes de especialización", "Estudiantes - Especialización",
"Estudiantes de maestría", "Estudiantes - Maestría",
"Estudiantes de doctorado", "Estudiantes - Doctorado",
"Profesores de planta (TCE)", "Profesor de planta",
"Profesores de cátedra", "Profesor de cátedra",
"Administrativos (planta sin Doc/Inv)", "Empleado administrativo"
)
# Normalizador composicional
renorm01 <- function(x) { x <- pmax(x, 0); x/sum(x) }
# Vector canónico de modos (orden opcional, pero nombres exactos)
modes <- c("Caminata","Micromovilidad","Transp. público",
"Vehículo particular","Moto","Taxi / App","Carro compartido")
# ------------------------------------------------------------------------------
# 1) MATRIZ BASE S_{rol7, modo_cat} (2024) DESDE TU OBJETO `emu`
# Usa pesos w_exp ya calculados en tu Rmd.
# ------------------------------------------------------------------------------
# (Por si en alguna corrida quedó "Vehículo particular", lo normalizo a "Vehículo particular")
emu <- emu %>%
mutate(modo_cat = if_else(modo_cat == "Vehículo particular", "Vehículo particular", modo_cat),
modo_cat = factor(modo_cat, levels = modes))
S_base <- emu %>%
filter(!is.na(rol7), !is.na(modo_cat), !is.na(w_exp)) %>%
group_by(rol7, modo_cat) %>%
summarise(viajes_w = sum(w_exp, na.rm = TRUE), .groups = "drop") %>%
group_by(rol7) %>%
mutate(share = viajes_w / sum(viajes_w)) %>%
ungroup() %>%
select(rol7, modo_cat, share)
# Completa modos ausentes y renormaliza por rol7
S0 <- S_base %>%
complete(rol7, modo_cat = modes, fill = list(share = 0)) %>%
group_by(rol7) %>% mutate(share = renorm01(share)) %>% ungroup()
# ------------------------------------------------------------------------------
# 2) PROYECCIÓN POBLACIONAL (2010–2024 -> 2025–2035)
# ------------------------------------------------------------------------------
pop_hist <- read_csv(pth_pop_hist, show_col_types = FALSE)
pop_long <- pop_hist %>%
rename(anio = any_of(c("Ano","año","anio","ano_"))) %>%
pivot_longer(-anio, names_to = "cat_pob", values_to = "n") %>%
mutate(n = as.numeric(n)) %>%
inner_join(map_roles, by = "cat_pob") %>% # trae rol7 exacto
select(anio, rol7, n) %>%
arrange(rol7, anio)
# Proyección simple por CAGR (2015–2024) con fallback robusto
proj_cagr <- function(df, y0 = 2015, y1 = 2024){
d <- df %>% filter(anio %in% c(y0, y1))
if(nrow(d)<2 || any(is.na(d$n))){
g <- 0
n1 <- (df %>% filter(anio==y1) %>% pull(n))[1]
if(is.na(n1)) n1 <- (df %>% filter(anio==max(anio)) %>% pull(n))[1]
} else {
n0 <- d$n[d$anio==y0]; n1 <- d$n[d$anio==y1]
g <- (n1/n0)^(1/(y1-y0)) - 1
}
tibble(anio = years_proj, n = n1*((1+g)^(anio - y1)))
}
if(!is.na(pth_pop_proj_opt) && file.exists(pth_pop_proj_opt)){
pop_proj_roles <- read_csv(pth_pop_proj_opt, show_col_types = FALSE) %>%
rename(anio = any_of(c("anio","año","ano"))) %>%
inner_join(map_roles, by = "cat_pob") %>%
transmute(anio, rol7 = rol7, n)
} else {
pop_proj_roles <- pop_long %>%
group_by(rol7) %>% group_modify(~proj_cagr(.x)) %>% ungroup()
}
# Mezcla 2024 para continuidad
pop_base_roles <- pop_long %>% filter(anio == base_year) %>%
group_by(rol7) %>% summarise(n = sum(n, na.rm = TRUE), .groups = "drop")
# ------------------------------------------------------------------------------
# 3) SHOCKS "DO NOTHING" Y SENSIBILIDADES (pivot-point logit por rol)
# ------------------------------------------------------------------------------
beta_time <- -0.1
beta_cost <- -0.08
role_weight <- tibble(
rol7 = c("Estudiantes - Pregrado","Estudiantes - Especialización",
"Estudiantes - Maestría","Estudiantes - Doctorado",
"Profesor de planta","Profesor de cátedra","Empleado administrativo"),
w_time = c(1.00, 0.90, 0.90, 0.90, 0.70, 0.70, 0.85),
w_cost = c(1.00, 1.00, 1.00, 1.00, 0.90, 0.85, 0.95)
)
mk_shocks <- function(anio){
metro <- ifelse(anio >= 2028, -0.15, 0) # mejora TP con Metro en 2028
tembici <- case_when(anio==2025 ~ +0.06,
anio==2026 ~ +0.04,
anio>=2027 ~ +0.02,
TRUE ~ 0) # desaparición bici pública, se diluye
moto_trend <- 0.2 * (anio - 2024) # mejora paulatina de Moto
tribble(
~modo_cat, ~d_time, ~d_cost,
"Transp. público", metro, metro,
"Vehículo particular", 0, 0,
"Taxi / App", 0, +0.05,
"Micromovilidad", tembici, tembici,
"Moto", -moto_trend, -moto_trend,
"Caminata", 0, 0,
"Carro compartido", 0, 0
)
}
softmax_pp <- function(s_base, dU){
w <- s_base * exp(dU)
w / sum(w)
}
# Proyección determinística por rol (sin loess)
proj_S <- map_df(years_proj, function(y){
sh <- mk_shocks(y)
S0 %>%
left_join(role_weight, by = "rol7") %>%
left_join(sh, by = "modo_cat") %>%
mutate(dU = (beta_time*w_time)*coalesce(d_time,0) + (beta_cost*w_cost)*coalesce(d_cost,0)) %>%
group_by(rol7) %>%
summarise(modo_cat = modo_cat,
share = softmax_pp(share, dU),
.groups = "drop") %>%
mutate(anio = y)
})
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
# ------------------------------------------------------------------------------
# 4) AGREGACIÓN UNIVERSITARIA (mixtura por composición poblacional)
# ------------------------------------------------------------------------------
mix_by_year <- pop_proj_roles %>%
group_by(anio) %>% mutate(w = n/sum(n)) %>% ungroup() %>% select(anio, rol7, w)
proj_uni <- proj_S %>%
left_join(mix_by_year, by = c("anio","rol7")) %>%
group_by(anio, modo_cat) %>%
summarise(share = sum(share*w, na.rm = TRUE), .groups = "drop") %>%
group_by(anio) %>% mutate(share = renorm01(share)) %>% ungroup()
# Añade 2024 observado (mezcla S0 con composición 2024)
mix_2024 <- pop_base_roles %>% mutate(w = n/sum(n)) %>% select(rol7, w)
uni_2024 <- S0 %>%
left_join(mix_2024, by = "rol7") %>%
group_by(modo_cat) %>% summarise(share = sum(share*w), .groups="drop") %>%
mutate(anio = base_year)
proj_uni_all <- bind_rows(uni_2024, proj_uni) %>% arrange(anio, modo_cat)
# ------------------------------------------------------------------------------
# 5) ESCENARIOS DETERMINÍSTICOS (amplifican/atenúan shocks)
# ------------------------------------------------------------------------------
scalars <- list(
base = c("Transp. público"=1.0, "Vehículo particular"=1.0, "Micromovilidad"=1.0,
"Taxi / App"=1.0, "Moto"=1.0, "Caminata"=1.0, "Carro compartido"=1.0),
optimista = c("Transp. público"=1.6, "Vehículo particular"=0.8, "Micromovilidad"=1.5,
"Taxi / App"=0.7, "Moto"=0.9, "Caminata"=1.2, "Carro compartido"=1.3),
pesimista = c("Transp. público"=0.5, "Vehículo particular"=1.4, "Micromovilidad"=0.5,
"Taxi / App"=1.3, "Moto"=2, "Caminata"=0.6, "Carro compartido"=0.8)
)
mk_shocks_scaled <- function(anio, sc_vec){
mk_shocks(anio) %>%
mutate(d_time = d_time * sc_vec[modo_cat],
d_cost = d_cost * sc_vec[modo_cat])
}
proj_S_scn <- function(sc_name){
sc <- scalars[[sc_name]]
map_df(years_proj, function(y){
sh <- mk_shocks_scaled(y, sc)
S0 %>%
left_join(role_weight, by = "rol7") %>%
left_join(sh, by = "modo_cat") %>%
mutate(dU = (beta_time*w_time)*coalesce(d_time,0) + (beta_cost*w_cost)*coalesce(d_cost,0)) %>%
group_by(rol7) %>%
summarise(modo_cat = modo_cat, share = softmax_pp(share, dU), .groups="drop") %>%
mutate(anio = y, escenario = sc_name)
})
}
scn_S <- map_df(names(scalars), proj_S_scn)
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
scn_uni <- scn_S %>%
left_join(mix_by_year, by = c("anio","rol7")) %>%
group_by(escenario, anio, modo_cat) %>%
summarise(share = sum(share*w, na.rm = TRUE), .groups="drop") %>%
group_by(escenario, anio) %>% mutate(share = renorm01(share)) %>% ungroup()
# ------------------------------------------------------------------------------
# 6) MONTE CARLO (VERSIÓN VECTORIZADA Y RÁPIDA)
# ------------------------------------------------------------------------------
set.seed(1234)
n_sim <- 800
sd_time <- 0.03; sd_cost <- 0.02
sd_btime <- 0.005; sd_bcost <- 0.005
# PASO 1: Pre-calcular todos los shocks no aleatorios para todos los años.
shocks_base <- map_df(years_proj, mk_shocks, .id = "anio_idx") %>%
mutate(anio = years_proj[as.integer(anio_idx)]) %>%
select(-anio_idx)
# PASO 2: Crear una tabla con los parámetros aleatorios para cada simulación.
sim_params <- tibble(
sim = 1:n_sim,
btime = rnorm(n_sim, beta_time, sd_btime),
bcost = rnorm(n_sim, beta_cost, sd_bcost)
)
# PASO 3: Crear la "parrilla" de cálculo con TODAS las combinaciones posibles.
# Esto reemplaza los bucles.
mc_grid <- expand_grid(
sim = 1:n_sim,
rol7 = unique(S0$rol7),
anio = years_proj
) %>%
left_join(S0, by = "rol7", relationship = "many-to-many") # Trae modo_cat y share base
# PASO 4: Unir todo y calcular dU y las nuevas participaciones en una sola pasada.
mc <- mc_grid %>%
# Unir los parámetros de la simulación (betas aleatorios)
left_join(sim_params, by = "sim") %>%
# Unir los ponderadores por rol y los shocks base
left_join(role_weight, by = "rol7") %>%
left_join(shocks_base, by = c("anio", "modo_cat")) %>%
# Generar el ruido aleatorio para los shocks de forma vectorizada
mutate(
d_time_rand = rnorm(n(), 0, sd_time),
d_cost_rand = rnorm(n(), 0, sd_cost),
d_time_total = coalesce(d_time, 0) + d_time_rand,
d_cost_total = coalesce(d_cost, 0) + d_cost_rand,
dU = (btime * w_time) * d_time_total + (bcost * w_cost) * d_cost_total
) %>%
# Aplicar el softmax_pp por grupo
group_by(sim, anio, rol7) %>%
mutate(share_new = softmax_pp(share, dU)) %>%
ungroup()
# PASO 5: Agregar a nivel de universidad (mezclando con la población proyectada)
mc_uni <- mc %>%
select(sim, anio, rol7, modo_cat, share_new) %>%
left_join(mix_by_year, by = c("anio", "rol7")) %>%
group_by(sim, anio, modo_cat) %>%
summarise(share = sum(share_new * w, na.rm = TRUE), .groups = "drop") %>%
# Renormalizar por si acaso
group_by(sim, anio) %>%
mutate(share = renorm01(share)) %>%
ungroup()
# PASO 6: Calcular los intervalos de confianza (esto es rápido)
mc_ci <- mc_uni %>%
group_by(anio, modo_cat) %>%
summarise(p05 = quantile(share, 0.05),
p50 = median(share),
p95 = quantile(share, 0.95), .groups = "drop") %>%
mutate(across(starts_with("p"), ~.*100))
# ------------------------------------------------------------------------------
# 7) SALIDAS: tablas y gráficos
# ------------------------------------------------------------------------------
scn_uni_w <- scn_uni %>% mutate(share = share*100) %>%
arrange(escenario, anio, modo_cat)
write_csv(scn_uni_w, "do_nothing_reparto_modal_scenarios_deterministic.csv")
write_csv(mc_ci %>% arrange(modo_cat, anio), "do_nothing_reparto_modal_montecarlo_ci.csv")
# Gráfico Determinístico
# Preparamos los datos solo para las etiquetas de inicio y fin
labels_det <- scn_uni %>%
filter(anio %in% c(2025, 2035))
# Gráfico
ggplot(scn_uni, aes(x = anio, y = share, color = modo_cat)) +
geom_line(linewidth = 1.1) +
facet_wrap(~escenario, labeller = labeller(escenario = toupper)) +
# Añadimos las etiquetas de inicio y fin sin que se solapen
geom_text_repel(
data = labels_det,
aes(label = scales::percent(share, accuracy = 1)),
size = 3.5,
nudge_x = ifelse(labels_det$anio == 2025, -0.7, 0.7),
direction = "y",
hjust = 0.5,
min.segment.length = 0,
segment.size = 0.2
) +
labs(
title = "Proyección Modal Determinística hasta 2035",
subtitle = "Reparto modal según tres escenarios de evolución para Bogotá.",
caption = "Fuente: Simulación basada en la Encuesta de Movilidad Uniandina 2024.",
x = "Año",
y = "Participación Modal",
color = "Modo de Transporte"
) +
scale_y_continuous(labels = scales::percent) +
scale_x_continuous(breaks = seq(2025, 2035, by = 1)) +
theme_minimal(base_size = 12) +
theme(
legend.position = "bottom",
plot.title = element_text(face = "bold", size = 16),
plot.caption = element_text(color = "grey50"),
strip.text = element_text(face = "bold", size = 11),
panel.grid.minor = element_blank(),
panel.grid.major = element_line(color = "grey92")
)

# Gráfico Monte Carlo
# Preparamos los datos solo para las etiquetas de inicio y fin
labels_mc <- mc_ci %>%
filter(anio %in% c(2025, 2035))
# Gráfico
ggplot(mc_ci, aes(x = anio, color = modo_cat, fill = modo_cat)) +
geom_ribbon(aes(ymin = p05, ymax = p95), alpha = 0.25, color = NA) +
geom_line(aes(y = p50), linewidth = 1.1) +
# Añadimos las etiquetas de inicio y fin para la mediana (p50)
geom_text_repel(
data = labels_mc,
aes(y = p50, label = scales::percent(p50 / 100, accuracy = 1)),
size = 3.5,
nudge_x = ifelse(labels_mc$anio == 2025, -0.7, 0.7),
direction = "y",
hjust = 0.5,
min.segment.length = 0,
segment.size = 0.2
) +
labs(
title = "Proyección Modal con Incertidumbre (Monte Carlo)",
subtitle = "La línea representa la mediana de las simulaciones; el área sombreada el intervalo de confianza del 90%.",
caption = "Fuente: 100 simulaciones de Monte Carlo basadas en la EMU 2024.",
x = "Año",
y = "Participación Modal (%)",
color = "Modo",
fill = "Modo"
) +
scale_x_continuous(breaks = seq(2025, 2035, by = 1)) +
theme_minimal(base_size = 12) +
theme(
legend.position = "bottom",
plot.title = element_text(face = "bold", size = 16),
plot.caption = element_text(color = "grey50"),
panel.grid.minor = element_blank(),
panel.grid.major = element_line(color = "grey92")
)

# Gráfico de evolución de la repartición modal (2025-2035)
ggplot(mc_ci, aes(x = factor(anio), y = p50 / 100, fill = modo_cat)) +
# 1. Barras apiladas (100%)
geom_col(position = "fill", width = 0.8) +
# 2. Etiquetas de porcentaje dentro de las barras (solo para segmentos >= 7%)
geom_text(
aes(label = ifelse(p50 / 100 >= 0.07, scales::percent(p50 / 100, accuracy = 1), "")),
position = position_stack(vjust = 0.5),
color = "white",
size = 3.0
) +
# 3. Escalas y paleta de colores consistentes
scale_y_continuous(labels = scales::label_percent()) +
# 4. Títulos y etiquetas descriptivas
labs(
title = "Evolución Proyectada del Reparto Modal (2025-2035)",
subtitle = "Mediana del escenario base (simulaciones de Monte Carlo)",
x = "Año de la Simulación",
y = "% del Reparto Modal Total",
fill = "Categoría modal"
) +
# 5. Tema minimalista para consistencia visual
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 16),
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank(), # Quitamos las líneas verticales
panel.grid.major.y = element_line(color = "grey92"),
axis.text = element_text(color = "black")
)

# =========================
# EDA Carpooling + EV (EMU24)
# =========================
# Requiere que en el entorno ya existan:
# - emu : microdato EMU 2024 con *_lbl, rol7, modo_cat, w_exp, dist_km, etc.
# - df : EMUgdb (geocodificada) con columnas de UPZ y Localidad
# y un ID de encuestado para cruzar (p.ej. encuesta_no o id_encuesta)
# - Las funciones/helper que ya definiste en tu Rmd.
# No se recrea 'emu'; solo se enriquece y se explora.
# ---------------------------
# 0) Vincular UPZ / Localidad
# ---------------------------
# Normaliza nombres de ID y selecciona solo lo necesario para no duplicar columnas.
# Intenta cruzar por 'encuesta_no'; si en df el id se llama distinto, agrega aquí el alias.
df_keys <- df %>%
janitor::clean_names() %>%
rename(encuesta_no = any_of(c("encuesta_no","id_encuesta","id"))) %>%
select(encuesta_no,
upz = any_of(c("upz","upz_lbl","nom_upz", "nombre_1")),
localidad = any_of(c("localidad","localidad_lbl","nom_localidad"))) %>%
distinct()
emu <- emu %>%
janitor::clean_names() %>%
rename(encuesta_no = any_of(c("encuesta_no","id_encuesta","id"))) %>%
left_join(df_keys, by = "encuesta_no")
# -------------------------------------
# 1) Indicadores básicos de carpooling
# -------------------------------------
# Variables clave (todas venían en el cuestionario):
# - frecuencia_uso_carro (CB)
# - carpooling1 ... carpooling33 (CC–DQ)
# - estacionamiento_frecuencia (CJ) y afines
# Importante: carpooling3 = número de PASAJEROS (sin contar conductor)
car_flags <- emu %>%
mutate(
usa_carro_conductor = as.integer(frecuencia_uso_carro %in% 1:5), # usa carro alguna vez
hace_carpool = as.integer(carpooling1 == 1),
# Ocupación última vez (conductor + pasajeros reportados)
occ_last = ifelse(is.na(carpooling3), NA_real_, 1 + as.numeric(carpooling3)),
# Disposición a compartir (escala 1–4); alto si >=3
will_share_hi = as.integer(carpooling21 %in% c(3,4)),
# Conocimiento/uso de Wheels (CN): 4-5 = usa a veces/frecuentemente
usa_wheels = as.integer(uso_conoce_aplicaciones_wheels %in% 4:5),
# Pago de pico y placa solidario
paga_pps = as.integer(carpooling5 == 1)
)
# 2) Crear mapas de etiquetas para las variables categóricas que vamos a usar
map_parking <- get_label_map(dicc, "estacionamiento_frecuencia")
map_alt_mode <- get_label_map(dicc, "hacia_modo_ppal") # Usamos el mapa de modos principales
# 1.1) Ocupación promedio actual entre quienes comparten (ponderada)
car_occ <- car_flags %>%
filter(usa_carro_conductor == 1, hace_carpool == 1, !is.na(occ_last), !is.na(w_exp)) %>%
summarise(
occ_prom_w = sum(w_exp * occ_last, na.rm = TRUE) / sum(w_exp, na.rm = TRUE),
n = n()
)
# 1.2) Reparto "Vehículo particular" vs. demás (ponderado) por rol7 y por UPZ
veh_share_by <- function(var){
car_flags %>%
filter(!is.na(modo_cat), !is.na(w_exp), !is.na(.data[[var]])) %>%
mutate(es_auto = modo_cat %in% c("Vehículo particular","Carro compartido", "Taxi / App")) %>%
group_by(.data[[var]], es_auto) %>%
summarise(w = sum(w_exp), .groups="drop_last") %>%
mutate(share = w / sum(w)) %>%
filter(es_auto) %>%
arrange(desc(share)) %>%
rename(!!var := 1) %>%
select(!!sym(var), share)
}
veh_share_rol <- veh_share_by("rol7")
veh_share_upz <- veh_share_by("upz")
veh_share_loc <- veh_share_by("localidad")
# 1.3) Tamaño del "mercado objetivo" de carpool (perfil y localización)
# Definición: usa carro como conductor (al menos ocasional), NO siempre comparte,
# alta disposición a compartir o ya usa Wheels; se reporta por UPZ/rol.
target_carpool <- car_flags %>%
filter(usa_carro_conductor == 1, !is.na(w_exp)) %>%
mutate(
comparte_a_veces = case_when(
carpooling4 %in% c(3,4) ~ 1L, # Algunas veces / Siempre
TRUE ~ 0L
),
target = as.integer((will_share_hi == 1 | usa_wheels == 1) & comparte_a_veces == 0)
) %>%
filter(target == 1) %>%
group_by(upz, rol7) %>%
summarise(
personas_w = sum(w_exp, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(desc(personas_w))
# 1.4) Elasticidad de ocupación “potencial” simple (para insumo de modelación):
# Supón que el 25–40% del target adopta carpool y eleva ocupación media a 2.5–3.0.
# Calcula la ocupación resultante esperada (tabla, no proyección todavía).
occ_base <- car_occ$occ_prom_w %||% 1.95
adopcion_grid <- crossing(tasa_adopcion = c(.25,.30,.35,.40),
occ_meta = c(2.5, 2.7, 3.0)) %>%
mutate(
occ_resultante = occ_base*(1 - tasa_adopcion) + occ_meta*(tasa_adopcion)
)
# -------------------------------------
# 2) Barreras y operación del carpool
# -------------------------------------
# Ejemplos de indicadores de calidad/operación (likert 1–5):
carpool_qos <- car_flags %>%
summarise(
fac_encontrar = weighted.mean(carpooling23, w_exp, na.rm = TRUE), # "Es fácil encontrar wheels"
confort = weighted.mean(carpooling24, w_exp, na.rm = TRUE),
seguridad = weighted.mean(carpooling25, w_exp, na.rm = TRUE),
puntualidad = weighted.mean(carpooling26, w_exp, na.rm = TRUE),
esperas = weighted.mean(carpooling27, w_exp, na.rm = TRUE),
cancelaciones = weighted.mean(carpooling28, w_exp, na.rm = TRUE),
ocup_max = weighted.mean(carpooling31, w_exp, na.rm = TRUE)
)
# Estacionamiento más usado por quienes conducen (ponderado)
park_by_driver <- car_flags %>%
recode_from_dict(estacionamiento_frecuencia, map_parking) %>% # <-- CORRECCIÓN APLICADA
filter(usa_carro_conductor == 1, !is.na(estacionamiento_frecuencia_lbl)) %>%
count(estacionamiento_frecuencia_lbl, wt = w_exp, name = "personas_w", sort = TRUE)
# Modo alterno cuando no hay carpool (CX)
alt_mode_when_no_carpool <- car_flags %>%
filter(!is.na(carpooling14)) %>%
recode_from_dict(carpooling14, map_alt_mode) %>%
filter(!is.na(carpooling14_lbl)) %>%
count(carpooling14_lbl, wt = w_exp, name = "personas_w", sort = TRUE)
# -------------------------------------
# 3) EV / Electromovilidad (situación actual)
# -------------------------------------
# Shares ponderados de tecnología (BU) y detalle híbrido (BW)
ev_now <- emu %>%
filter(!is.na(vehiculo_combustible), !is.na(w_exp)) %>%
mutate(tec = case_when(
vehiculo_combustible == 4 ~ "Eléctrico",
vehiculo_combustible == 5 ~ "Híbrido",
vehiculo_combustible == 1 ~ "Gasolina",
vehiculo_combustible == 2 ~ "Diésel",
vehiculo_combustible == 3 ~ "GNV",
TRUE ~ "Otro"
)) %>%
count(tec, wt = w_exp, name = "viajes_w") %>%
mutate(share = viajes_w/sum(viajes_w)) %>%
arrange(desc(share))
# Híbridos: identificar PHEV (BW==4) dentro de los híbridos
hyb_split <- emu %>%
filter(vehiculo_combustible == 5, !is.na(tipo_veh_hibrido), !is.na(w_exp)) %>%
mutate(tipo_h = case_when(
tipo_veh_hibrido == 4 ~ "PHEV",
TRUE ~ "Híbrido no enchufable"
)) %>%
count(tipo_h, wt = w_exp, name = "viajes_w") %>%
mutate(share = viajes_w/sum(viajes_w))
# EV/Hybrid por rol y por UPZ
ev_by_rol_upz <- emu %>%
filter(!is.na(vehiculo_combustible), !is.na(w_exp), !is.na(rol7), !is.na(upz)) %>%
mutate(es_ev_o_h = vehiculo_combustible %in% c(4,5)) %>%
group_by(upz, rol7) %>%
summarise(pct_ev_h = sum(w_exp*es_ev_o_h, na.rm=TRUE)/sum(w_exp, na.rm=TRUE),
.groups="drop") %>%
arrange(desc(pct_ev_h))
# ---------------------------
# 4) Tablas claras de resultados
# ---------------------------
# a) Ocupación actual promedio (ponderada)
car_occ
## # A tibble: 1 × 2
## occ_prom_w n
## <dbl> <int>
## 1 3.33 187
# b) UPZ y roles con mayor participación de auto (para focalizar carpool)
veh_share_upz %>% mutate(share = percent(share, 1)) %>% head(20)
## # A tibble: 20 × 2
## # Groups: upz [20]
## upz share
## <chr> <chr>
## 1 LAS MARGARITAS 100%
## 2 MARCO FIDEL SUAREZ 100%
## 3 PARQUE SIMON BOLIVAR-CAN 100%
## 4 SAN BLAS 100%
## 5 SAN CRISTOBAL NORTE 88%
## 6 CASA BLANCA SUBA 87%
## 7 EL REFUGIO 79%
## 8 KENNEDY CENTRAL 74%
## 9 LA ESMERALDA 73%
## 10 LAS FERIAS 71%
## 11 SAGRADO CORAZON 66%
## 12 PARDO RUBIO 64%
## 13 MODELIA 63%
## 14 USAQUEN 63%
## 15 SANTA BARBARA 60%
## 16 BOLIVIA 59%
## 17 CIUDAD SALITRE ORIENTAL 58%
## 18 LA ALHAMBRA 58%
## 19 COUNTRY CLUB 55%
## 20 GALERIAS 50%
veh_share_rol %>% mutate(share = percent(share, 1))
## # A tibble: 7 × 2
## # Groups: rol7 [7]
## rol7 share
## <chr> <chr>
## 1 Profesor de planta 51%
## 2 Estudiantes - Especialización 50%
## 3 Profesor de cátedra 47%
## 4 Estudiantes - Pregrado 40%
## 5 Estudiantes - Maestría 33%
## 6 Estudiantes - Doctorado 19%
## 7 Empleado administrativo 18%
# c) “Mercado objetivo” (personas ponderadas) por UPZ y rol
target_carpool %>% mutate(personas_w = round(personas_w)) %>% head(30)
## # A tibble: 30 × 3
## upz rol7 personas_w
## <chr> <chr> <dbl>
## 1 CHICO LAGO Estudiantes - Pregrado 168
## 2 <NA> Estudiantes - Pregrado 168
## 3 <NA> Estudiantes - Maestría 142
## 4 CASA BLANCA SUBA Estudiantes - Pregrado 126
## 5 GALERIAS Estudiantes - Pregrado 84
## 6 LA ESMERALDA Estudiantes - Pregrado 84
## 7 LOS CEDROS Estudiantes - Pregrado 84
## 8 SAGRADO CORAZON Estudiantes - Pregrado 84
## 9 SANTA BARBARA Estudiantes - Pregrado 84
## 10 TEUSAQUILLO Estudiantes - Pregrado 84
## # ℹ 20 more rows
# d) Escenarios simples de ocupación resultante (insumo cualitativo)
adopcion_grid
## # A tibble: 12 × 3
## tasa_adopcion occ_meta occ_resultante
## <dbl> <dbl> <dbl>
## 1 0.25 2.5 3.12
## 2 0.25 2.7 3.17
## 3 0.25 3 3.25
## 4 0.3 2.5 3.08
## 5 0.3 2.7 3.14
## 6 0.3 3 3.23
## 7 0.35 2.5 3.04
## 8 0.35 2.7 3.11
## 9 0.35 3 3.21
## 10 0.4 2.5 3.00
## 11 0.4 2.7 3.08
## 12 0.4 3 3.20
# e) Calidad/operación percibida (1–5)
carpool_qos
## # A tibble: 1 × 7
## fac_encontrar confort seguridad puntualidad esperas cancelaciones ocup_max
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 3.38 4.11 4.52 3.90 2.93 2.61 3.79
# f) Estacionamientos más usados por conductores
park_by_driver %>% head(10)
## # A tibble: 10 × 2
## estacionamiento_frecuencia_lbl personas_w
## <chr> <dbl>
## 1 Un parqueadero de la Universidad (Sd, Ml, Au, etc.) 2590.
## 2 Otro. ¿Cuál? 1223.
## 3 El parqueadero de CityU 1097.
## 4 El parqueadero Aparcar eje ambiental 649.
## 5 Un parqueadero arrendado 416.
## 6 Alguien se lleva el carro después de dejarme 378.
## 7 El parqueadero Tequendama (al frente del SD) 354.
## 8 El parqueadero de la Cinemateca 168.
## 9 El parqueadero de un amigo(a) o familiar que vive cerca 47.4
## 10 En la calle 42.0
# g) Modo alterno cuando no hay carpool
alt_mode_when_no_carpool %>% head(10)
## # A tibble: 10 × 2
## carpooling14_lbl personas_w
## <chr> <dbl>
## 1 Transmilenio (con o sin alimentador) 1431.
## 2 SITP 588.
## 3 Carro compartido (grupo WhatsApp, Wheels Uniandes, vecino(a), etc… 379.
## 4 Pasajero(a) en carro con un(a) amigo(a) o un familiar (o miembro … 210.
## 5 Moto por aplicación (Picap, Didi moto, etc) 132.
## 6 Caminata 126.
## 7 Moto (propia) 42.0
## 8 Bicicleta (propia) 35.5
## 9 Otro. ¿Cuál? 6.33
## 10 Aplicaciones de servicios de transporte (Uber, Cabify, etc.) 5.38
# h) Estado actual de tecnologías vehiculares (ponderado)
ev_now %>% mutate(share = percent(share, 1))
## # A tibble: 4 × 3
## tec viajes_w share
## <chr> <dbl> <chr>
## 1 Gasolina 2698. 63%
## 2 Híbrido 746. 18%
## 3 Eléctrico 565. 13%
## 4 Diésel 243. 6%
# i) Descomposición de híbridos (PHEV vs otros)
hyb_split %>% mutate(share = percent(share, 1))
## # A tibble: 2 × 3
## tipo_h viajes_w share
## <chr> <dbl> <chr>
## 1 Híbrido no enchufable 699. 94%
## 2 PHEV 47.4 6%
# j) Zonas con mayor % de EV/Híbridos (para ubicación de cargadores)
ev_by_rol_upz %>%
mutate(pct_ev_h = percent(pct_ev_h, 1)) %>%
group_by(upz) %>%
slice_max(order_by = pct_ev_h, n = 3, with_ties = FALSE) %>%
ungroup() %>%
arrange(desc(pct_ev_h)) %>%
head(30)
## # A tibble: 30 × 3
## upz rol7 pct_ev_h
## <chr> <chr> <chr>
## 1 SANTA BARBARA Estudiantes - Pregrado 83%
## 2 EL REFUGIO Empleado administrativo 67%
## 3 EL RINCON Estudiantes - Pregrado 67%
## 4 AMERICAS Empleado administrativo 50%
## 5 EL REFUGIO Profesor de cátedra 50%
## 6 LA ALHAMBRA Profesor de planta 50%
## 7 NIZA Profesor de planta 50%
## 8 NIZA Estudiantes - Pregrado 40%
## 9 CASA BLANCA SUBA Estudiantes - Pregrado 29%
## 10 CHICO LAGO Profesor de planta 25%
## # ℹ 20 more rows
# ---------------------------
# 5) Gráficos rápidos (opcional)
# ---------------------------
# TOP-12 UPZ con mayor market share de auto (ponderado)
veh_share_upz %>%
slice_max(order_by = share, n = 12) %>%
ggplot(aes(x = reorder(upz, share), y = share)) +
geom_col(fill = "#8da0cb") +
geom_text(aes(label = percent(share,1)), hjust = -0.1, size = 3.5) +
coord_flip(clip = "off") +
labs(title = "Participación de viajes en automóvil por UPZ (ponderado)",
x = "UPZ", y = "Share") +
scale_y_continuous(labels = percent, limits = c(0, NA),
expand = expansion(mult = c(0, .08))) +
theme_minimal(base_size = 12)

# Distribución de ocupación reportada (último carpool)
car_flags %>%
filter(hace_carpool == 1, !is.na(occ_last)) %>%
ggplot(aes(x = occ_last)) +
geom_histogram(binwidth = 1, boundary = 1, closed = "left", fill = "#66c2a5") +
labs(title = "Ocupación reportada en viajes compartidos (última vez)",
x = "Personas por vehículo", y = "Frecuencia (muestra)") +
scale_x_continuous(breaks = 1:6) +
theme_minimal(base_size = 12)

# Share EV / Híbridos por rol (ponderado)
emu %>%
filter(!is.na(vehiculo_combustible), !is.na(rol7), !is.na(w_exp)) %>%
mutate(es_ev_h = vehiculo_combustible %in% c(4,5)) %>%
group_by(rol7) %>%
summarise(pct_ev_h = sum(w_exp*es_ev_h, na.rm=TRUE)/sum(w_exp, na.rm=TRUE),
.groups="drop") %>%
ggplot(aes(x = reorder(rol7, pct_ev_h), y = pct_ev_h, fill = rol7)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = percent(pct_ev_h,1)), vjust = -0.25, size = 3.5) +
coord_flip(clip = "off") +
scale_y_continuous(labels = percent, limits = c(0, NA),
expand = expansion(mult = c(.02,.08))) +
labs(title = "Porcentaje de EV/Híbridos por rol (ponderado)", x = NULL, y = "% EV/Híbridos")

Modelación del escenario 3
## ---- escenario-vehicular-sostenible-CORREGIDO-Y-FUNCIONAL, message=FALSE, warning=FALSE ----
library(tidyverse)
library(lubridate)
library(scales)
# --------------------------------------------------------------------------
# 0) HELPERS Y PARÁMETROS GENERALES (Aseguramos que existan)
# --------------------------------------------------------------------------
if (!exists("renorm01")) renorm01 <- function(x){ x <- pmax(x,0); x/sum(x) }
if (!exists("softmax_pp")) softmax_pp <- function(s_base, dU){ w <- s_base * exp(dU); w / sum(w) }
if (!exists("S0")) { # Si S0 no existe, lo creamos desde emu
modes <- c("Caminata","Micromovilidad","Transp. público", "Vehículo particular","Moto","Taxi / App","Carro compartido")
emu <- emu %>% mutate(modo_cat = factor(modo_cat, levels = modes))
S_base <- emu %>%
filter(!is.na(rol7), !is.na(modo_cat), !is.na(w_exp)) %>%
group_by(rol7, modo_cat) %>%
summarise(viajes_w = sum(w_exp, na.rm = TRUE), .groups = "drop") %>%
group_by(rol7) %>%
mutate(share = viajes_w / sum(viajes_w)) %>%
ungroup() %>%
select(rol7, modo_cat, share)
S0 <- S_base %>%
complete(rol7, modo_cat = modes, fill = list(share = 0)) %>%
group_by(rol7) %>% mutate(share = renorm01(share)) %>% ungroup()
}
if (!exists("role_weight")) { # Si role_weight no existe, lo creamos
role_weight <- tibble(
rol7 = unique(S0$rol7),
w_time = c(1.00, 0.90, 0.90, 0.90, 0.70, 0.70, 0.85),
w_cost = c(1.00, 1.00, 1.00, 1.00, 0.90, 0.85, 0.95)
)
}
if (!exists("mix_by_year")){ # Si mix_by_year no existe, lo creamos
# Este bloque requiere que pop_proj_roles exista, como en el script do_nothing
mix_by_year <- pop_proj_roles %>%
group_by(anio) %>% mutate(w = n/sum(n)) %>% ungroup() %>% select(anio, rol7, w)
}
# --------------------------------------------------------------------------
# 1) PARÁMETROS DE POLÍTICA (Agresivos para ver el cambio)
# --------------------------------------------------------------------------
policy_start_year <- 2025
policy_ramp_years <- 4 # Años para alcanzar la máxima intensidad
carpool_cost_shock <- -2.0 # Fuerte REDUCCIÓN de costo percibido para Carro Compartido
carpool_time_shock <- -1.0 # Fuerte REDUCCIÓN de tiempo percibido para Carro Compartido
solo_car_cost_shock <- +1.5 # Fuerte AUMENTO de costo percibido para Vehículo Particular
# Hacemos que el carpooling también sea atractivo frente a otros modos
moto_cost_shock <- +0.3 # Ligero desincentivo a la moto
taxi_app_cost_shock <- +0.5 # Desincentivo moderado a Taxi/App
# --------------------------------------------------------------------------
# 2) FUNCIÓN DE SHOCKS PARA EL ESCENARIO 3
# Combina los shocks del "Do Nothing" con los de la nueva política
# --------------------------------------------------------------------------
mk_shocks_escenario3 <- function(anio){
# Traemos los shocks base del Do Nothing (Metro, Tembici, etc.)
shocks_base <- mk_shocks(anio)
ramp_factor <- function(y){
if (y < policy_start_year) return(0)
pmin(1, (y - policy_start_year + 1) / policy_ramp_years)
}
# Calculamos la intensidad de la nueva política para el año actual
ramp <- ramp_factor(anio)
# Aplicamos los shocks adicionales de la política del Escenario 3
shocks_politica <- shocks_base %>%
mutate(
d_cost = d_cost + case_when(
modo_cat == "Carro compartido" ~ carpool_cost_shock * ramp,
modo_cat == "Vehículo particular" ~ solo_car_cost_shock * ramp,
modo_cat == "Moto" ~ moto_cost_shock * ramp,
modo_cat == "Taxi / App" ~ taxi_app_cost_shock * ramp,
TRUE ~ 0
),
d_time = d_time + case_when(
modo_cat == "Carro compartido" ~ carpool_time_shock * ramp,
TRUE ~ 0
)
)
return(shocks_politica)
}
# --------------------------------------------------------------------------
# 3) PROYECCIÓN DETERMINÍSTICA (USANDO LA LÓGICA DEL "DO NOTHING")
# --------------------------------------------------------------------------
scn_S_sostenible <- map_df(years_proj, function(y){
sh <- mk_shocks_escenario3(y) # Usamos la nueva función de shocks
S0 %>%
left_join(role_weight, by = "rol7") %>%
left_join(sh, by = "modo_cat") %>%
mutate(dU = (beta_time * w_time) * coalesce(d_time,0) + (beta_cost * w_cost) * coalesce(d_cost,0)) %>%
group_by(rol7) %>%
# CORRECCIÓN FINAL: Usamos summarise EXACTAMENTE como en el script "Do Nothing"
summarise(
modo_cat = modo_cat,
share = softmax_pp(share, dU),
.groups = "drop"
) %>%
mutate(anio = y, escenario = "Vehicular Sostenible")
})
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
# --------------------------------------------------------------------------
# 4) AGREGACIÓN Y POST-PROCESO (Sin cambios)
# --------------------------------------------------------------------------
proj_uni_sostenible <- scn_S_sostenible %>%
left_join(mix_by_year, by = c("anio", "rol7")) %>%
group_by(escenario, anio, modo_cat) %>%
summarise(share = sum(share * w, na.rm = TRUE), .groups = "drop") %>%
group_by(escenario, anio) %>%
mutate(share = renorm01(share)) %>%
ungroup()
# Trayectoria de Ocupación Vehicular
occ_base <- 1.95
occ_target_2035 <- 2.6
occ_traj <- tibble(
anio = years_proj,
occ_obj = occ_base + (occ_target_2035 - occ_base) * (anio - base_year) / (2035 - base_year)
)
# Trayectoria de Adopción de EV
ev_meta_share_2035 <- 0.35
auto_rows <- emu %>% filter(modo_cat %in% c("Vehículo particular","Carro compartido"), !is.na(w_exp))
prop_ev_base_uniandes <- auto_rows %>%
mutate(es_ev_h = vehiculo_combustible %in% c(4,5)) %>%
summarise(p = sum(w_exp * es_ev_h, na.rm=TRUE) / sum(w_exp, na.rm=TRUE)) %>%
pull(p) %>% coalesce(0)
ev_traj <- tibble(
anio = years_proj,
prop_ev = prop_ev_base_uniandes + (ev_meta_share_2035 - prop_ev_base_uniandes) *
(anio - base_year) / (2035 - base_year)
) %>%
mutate(prop_ev = pmin(pmax(prop_ev, 0), 1))
Resultados y visualizaciones
# ============ 5) SALIDAS RÁPIDAS ============
# a) Barras 100%: reparto modal en el escenario
proj_uni_sostenible %>%
mutate(anio = factor(anio)) %>%
ggplot(aes(x = anio, y = share, fill = modo_cat)) +
geom_col(position = "fill", width = 0.85) +
geom_text(aes(label = ifelse(share >= 0.07, percent(share, 0), "")),
position = position_stack(vjust = 0.5), color = "white", size = 3.2) +
scale_y_continuous(labels = label_percent()) +
labs(
title = "Proyección Modal – Escenario Vehicular Sostenible",
subtitle = "Incentivos al carpooling + desincentivos al conductor solo",
x = "Año", y = "% del total", fill = "Modo"
) +
theme_minimal(base_size = 12) +
theme(panel.grid.major.x = element_blank())
## Warning: Removed 44 rows containing missing values or values outside the scale range
## (`geom_text()`).

# b) Trayectorias de ocupación objetivo y adopción EV (diagnóstico)
occ_traj %>%
ggplot(aes(anio, occ_obj)) +
geom_line(linewidth=1) +
geom_point(size=1.5) +
labs(title="Trayectoria de ocupación objetivo", x=NULL, y="Personas por vehículo") +
scale_x_continuous(breaks = years_proj) +
theme_minimal(base_size = 12)

ev_traj %>%
ggplot(aes(anio, prop_ev)) +
geom_line(linewidth=1) +
geom_point(size=1.5) +
scale_y_continuous(labels = percent) +
labs(title="Adopción EV/PHEV (supuesto)", x=NULL, y="% dentro de viajes en auto") +
scale_x_continuous(breaks = years_proj) +
theme_minimal(base_size = 12)

# d) Exportables
readr::write_csv(proj_uni_sostenible, "escenario3_reparto_modal_universidad.csv")
# ============ 7) NOTAS DE REFERENCIA (texto) ============
# - Cargadores EV 2024 en campus: 16 (Reporte de Sostenibilidad 2024, "Parqueaderos autos eléctricos: 16").
# - Puedes alinear el target de cargadores (ev_chargers_target) con tu plan de inversión.
# - Si el CSV de factores no está o los nombres no coinciden, el módulo de emisiones se omite sin romper el flujo.
Cálculo de emisiones
## ---- emisiones-2035-por-rol-y-escenario-FINAL ----
library(tidyverse)
library(janitor)
# ==========================================================
# 0) PRE-PROCESAMIENTO E INSUMOS CLAVE
# ==========================================================
# -- 0.1 Parámetros de Escenario y Globales (Ajustables) --
semanas_academicas <- 40
roundtrip_factor <- 2
# Ocupaciones por defecto y metas
occ_defaults <- c("Moto" = 1.1, "Taxi / App" = 1.5)
prop_ev_2035_DoNothing <- 0.15
prop_ev_2035_Esc3 <- 0.35
occ_target_carpool_Esc3 <- 2.6
# -- 0.2 Insumos Base 2024 (calculados desde `emu`) --
# Ocupación vehicular observada
occ_observada <- emu %>%
mutate(
modo_priv = case_when(
modo_cat == "Carro compartido" ~ "Carro compartido",
modo_cat == "Vehículo particular" ~ "Vehículo particular",
TRUE ~ NA_character_
),
occ_last = if_else(!is.na(carpooling3), 1 + as.numeric(carpooling3), NA_real_)
) %>%
filter(!is.na(modo_priv), !is.na(w_exp), is.finite(occ_last)) %>%
group_by(modo_priv) %>%
summarise(occ_prom_w = weighted.mean(occ_last, w_exp, na.rm = TRUE), .groups = "drop") %>%
deframe()
occ_2024 <- unlist(purrr::list_modify(as.list(occ_defaults), !!!as.list(occ_observada)))
# Distancia y Frecuencia por rol y modo
dist_rol_modo <- emu %>%
filter(!is.na(rol7), !is.na(modo_cat), is.finite(dist_km), dist_km >= 0) %>%
group_by(rol7, modo_cat) %>%
summarise(dist_km_mean = weighted.mean(dist_km, w_exp, na.rm = TRUE), .groups = "drop")
# Frecuencia semanal por rol (VERSIÓN CORREGIDA Y ROBUSTA)
# Usa la columna numérica directa 'frecuencia_viaje_u' según el diccionario
freq_sem_rol <- emu %>%
filter(!is.na(rol7), !is.na(frecuencia_viaje_u), !is.na(w_exp)) %>%
# La columna ya contiene el número de días, solo hay que asegurar que sea numérica
mutate(freq_sem = as.numeric(frecuencia_viaje_u)) %>%
filter(is.finite(freq_sem)) %>%
group_by(rol7) %>%
summarise(
freq_sem_mean = weighted.mean(freq_sem, w_exp, na.rm = TRUE),
.groups = "drop"
)
# Fallbacks (promedios globales para rellenar NAs)
dist_modo_global <- dist_rol_modo %>% group_by(modo_cat) %>% summarise(dist_km_mean_glob = mean(dist_km_mean, na.rm = TRUE), .groups = "drop")
freq_sem_global <- mean(freq_sem_rol$freq_sem_mean, na.rm = TRUE)
# -- 0.3 Tabla de Insumos Completa (a prueba de NAs) --
insumos_2024 <- crossing(
rol7 = unique(emu$rol7[!is.na(emu$rol7)]),
modo_cat = unique(emu$modo_cat[!is.na(emu$modo_cat)])
) %>%
left_join(dist_rol_modo, by = c("rol7", "modo_cat")) %>%
left_join(freq_sem_rol, by = "rol7") %>%
left_join(dist_modo_global, by = "modo_cat") %>%
mutate(
dist_km_mean = coalesce(dist_km_mean, dist_km_mean_glob, mean(dist_rol_modo$dist_km_mean, na.rm = TRUE)),
freq_sem_mean = coalesce(freq_sem_mean, freq_sem_global)
)
# -- 0.4 Población 2035 --
pop_2035 <- pop_proj_roles %>% filter(anio == 2035) %>% select(rol7, n) %>% rename(N_rol = n)
# ==========================================================
# 1) LECTURA Y ARMONIZACIÓN DE FACTORES DE EMISIÓN
# ==========================================================
pth_factores <- "Factores_de_emision.csv"
fe_raw <- read_csv(pth_factores, show_col_types = FALSE) %>% clean_names()
pax_km_col <- names(fe_raw)[2]; veh_km_col <- names(fe_raw)[3]
fe <- fe_raw %>%
rename(modo_in = 1) %>%
mutate(
modo_cat = case_when(
modo_in == "Caminata" ~ "Caminata",
modo_in == "Bicicleta o Patineta" ~ "Micromovilidad",
modo_in == "Motocicleta" ~ "Moto",
modo_in == "Vehiculo electrico" ~ "Vehículo eléctrico",
str_detect(modo_in, "Bus|TransMilenio|Zonal|Padrones|Interurbano") ~ "Transp. público",
modo_in == "Carro o Taxi (1 pasajero)" ~ "Vehículo particular",
modo_in == "Carro o Taxi (3 pasajeros)" ~ "Carro compartido",
TRUE ~ NA_character_
),
gco2_paxkm = as.numeric(!!sym(pax_km_col)),
gco2_vehkm = as.numeric(!!sym(veh_km_col))
) %>%
filter(!is.na(modo_cat)) %>%
group_by(modo_cat) %>%
summarise(gco2_paxkm = first(na.omit(gco2_paxkm)), gco2_vehkm = first(na.omit(gco2_vehkm)), .groups = "drop")
factor_ev_paxkm <- fe$gco2_paxkm[fe$modo_cat == "Vehículo eléctrico"]; if(length(factor_ev_paxkm)==0 || !is.finite(factor_ev_paxkm)) factor_ev_paxkm <- 12.0
# ====================================================================
# 2) FUNCIÓN PARA CALCULAR FACTOR FINAL (gCO2eq/pax-km)
# ====================================================================
factor_paxkm <- function(modo, occ_vec, prop_ev, fe_tbl) {
lookup_modo <- if (modo == "Taxi / App") "Vehículo particular" else modo
row <- fe_tbl %>% filter(modo_cat == lookup_modo)
if (nrow(row) == 0) return(0)
# --- CAMBIO QUIRÚRGICO AQUÍ ---
# Solo usamos el factor gco2/pax-km directo si el modo NO es uno de los que modelamos con ocupación variable.
if (is.finite(row$gco2_paxkm) && !modo %in% c("Vehículo particular", "Taxi / App")) {
return(row$gco2_paxkm)
}
# Para Carro Particular, Compartido y Taxi/App, FORZAMOS el cálculo usando gco2/veh-km y la ocupación del escenario.
if (is.finite(row$gco2_vehkm)) {
ice_factor_vehkm <- row$gco2_vehkm
# Usamos la ocupación específica del modo que viene en occ_vec (ej. 2.6 para Carro Compartido en Esc3)
ocupacion <- ifelse(modo %in% names(occ_vec), occ_vec[[modo]], 1.0)
ev_factor_vehkm <- factor_ev_paxkm * ocupacion
emision_veh_mixta <- (1 - prop_ev) * ice_factor_vehkm + prop_ev * ev_factor_vehkm
return(emision_veh_mixta / ocupacion)
}
return(0)
}
# ==========================================================
# 3) CÁLCULO DE PASAJEROS-KILÓMETRO (PKM) ANUALES 2035
# ==========================================================
compute_pkm <- function(shares_tbl, insumos_tbl, pop_tbl) {
shares_tbl %>%
left_join(insumos_tbl, by = c("rol7", "modo_cat")) %>%
left_join(pop_tbl, by = "rol7") %>%
mutate(
viajes_persona_anuales = freq_sem_mean * semanas_academicas * roundtrip_factor,
pkm_anuales = N_rol * viajes_persona_anuales * share * dist_km_mean
) %>%
replace_na(list(pkm_anuales = 0)) %>%
select(rol7, modo_cat, pkm_anuales)
}
shares_2035_DoNothing <- scn_S %>% filter(anio == 2035, escenario == "base")
shares_2035_Esc3 <- scn_S_sostenible %>% filter(anio == 2035)
pkm_DoNothing <- compute_pkm(shares_2035_DoNothing, insumos_2024, pop_2035)
pkm_Esc3 <- compute_pkm(shares_2035_Esc3, insumos_2024, pop_2035)
# ==========================================================
# 4) CÁLCULO FINAL DE EMISIONES (tCO2e) POR ESCENARIO
# ==========================================================
calc_emisiones <- function(pkm_tbl, occ_vec, prop_ev, fe_tbl, escenario_name){
factores_escenario <- tibble(modo_cat = unique(pkm_tbl$modo_cat)) %>%
rowwise() %>%
mutate(f_gco2_paxkm = factor_paxkm(modo_cat, occ_vec, prop_ev, fe_tbl)) %>%
ungroup()
pkm_tbl %>%
left_join(factores_escenario, by = "modo_cat") %>%
mutate(
emisiones_gCO2e = pkm_anuales * f_gco2_paxkm,
emisiones_tCO2e = emisiones_gCO2e / 1e6
) %>%
group_by(rol7) %>%
summarise(emisiones_tCO2e = sum(emisiones_tCO2e, na.rm = TRUE), .groups = "drop") %>%
mutate(escenario = escenario_name)
}
# Ocupaciones por escenario
occ_DoNothing <- occ_2024
occ_Esc3 <- occ_2024
occ_Esc3["Carro compartido"] <- occ_target_carpool_Esc3
emis_2035_DoNothing <- calc_emisiones(pkm_DoNothing, occ_DoNothing, prop_ev_2035_DoNothing, fe, "Do-Nothing")
emis_2035_Esc3 <- calc_emisiones(pkm_Esc3, occ_Esc3, prop_ev_2035_Esc3, fe, "Vehicular Sostenible")
# ==========================================================
# 5) PRESENTACIÓN DE RESULTADOS
# ==========================================================
resultados_rol <- bind_rows(emis_2035_DoNothing, emis_2035_Esc3)
totales_esc <- resultados_rol %>%
group_by(escenario) %>%
summarise(emisiones_totales_tCO2e = sum(emisiones_tCO2e, na.rm = TRUE), .groups = "drop")
# --- Imprimir en Consola ---
cat("\n--- EMISIONES TOTALES 2035 (tCO2e) ---\n")
##
## --- EMISIONES TOTALES 2035 (tCO2e) ---
print(knitr::kable(totales_esc, digits = 1))
##
##
## |escenario | emisiones_totales_tCO2e|
## |:--------------------|-----------------------:|
## |Do-Nothing | 4473.1|
## |Vehicular Sostenible | 3881.4|
cat("\n--- EMISIONES 2035 (tCO2e) POR ROL Y ESCENARIO ---\n")
##
## --- EMISIONES 2035 (tCO2e) POR ROL Y ESCENARIO ---
print(knitr::kable(resultados_rol, digits = 1))
##
##
## |rol7 | emisiones_tCO2e|escenario |
## |:-----------------------------|---------------:|:--------------------|
## |Empleado administrativo | 382.1|Do-Nothing |
## |Estudiantes - Doctorado | 51.8|Do-Nothing |
## |Estudiantes - Especialización | 42.4|Do-Nothing |
## |Estudiantes - Maestría | 674.9|Do-Nothing |
## |Estudiantes - Pregrado | 2989.3|Do-Nothing |
## |Profesor de cátedra | 214.5|Do-Nothing |
## |Profesor de planta | 118.1|Do-Nothing |
## |Empleado administrativo | 359.9|Vehicular Sostenible |
## |Estudiantes - Doctorado | 41.2|Vehicular Sostenible |
## |Estudiantes - Especialización | 35.1|Vehicular Sostenible |
## |Estudiantes - Maestría | 590.9|Vehicular Sostenible |
## |Estudiantes - Pregrado | 2584.2|Vehicular Sostenible |
## |Profesor de cátedra | 178.5|Vehicular Sostenible |
## |Profesor de planta | 91.7|Vehicular Sostenible |
Búsqueda de errores
## ---- chunk-diagnostico-pipeline, echo=FALSE, message=FALSE, warning=FALSE ----
cat("\n\n==================================================")
##
##
## ==================================================
cat("\n== REPORTE DE DIAGNÓSTICO DEL PIPELINE DE EMISIONES ==")
##
## == REPORTE DE DIAGNÓSTICO DEL PIPELINE DE EMISIONES ==
cat("\n==================================================\n\n")
##
## ==================================================
# --- 1. VERIFICACIÓN DE DATOS INICIALES Y TRANSFORMACIONES ---
cat("\n--- 1. Datos Iniciales y Transformaciones Clave ---\n\n")
##
## --- 1. Datos Iniciales y Transformaciones Clave ---
cat("Dimensiones del dataframe 'emu' después de la limpieza inicial:\n")
## Dimensiones del dataframe 'emu' después de la limpieza inicial:
print(dim(emu))
## [1] 1278 182
cat("\nCategorías Modales ('modo_cat') generadas:\n")
##
## Categorías Modales ('modo_cat') generadas:
print(knitr::kable(count(emu, modo_cat), col.names = c("Modo", "Conteo")))
##
##
## |Modo | Conteo|
## |:-------------------|------:|
## |Caminata | 76|
## |Micromovilidad | 50|
## |Transp. público | 470|
## |Vehículo particular | 233|
## |Moto | 68|
## |Taxi / App | 75|
## |Carro compartido | 41|
## |NA | 265|
cat("\nRoles ('rol7') identificados:\n")
##
## Roles ('rol7') identificados:
print(knitr::kable(count(emu, rol7), col.names = c("Rol", "Conteo")))
##
##
## |Rol | Conteo|
## |:-----------------------------|------:|
## |Empleado administrativo | 356|
## |Estudiantes - Doctorado | 16|
## |Estudiantes - Especialización | 8|
## |Estudiantes - Maestría | 113|
## |Estudiantes - Pregrado | 345|
## |Profesor de cátedra | 106|
## |Profesor de planta | 135|
## |NA | 199|
# --- 2. VERIFICACIÓN DE INSUMOS CALCULADOS (AÑO BASE 2024) ---
cat("\n\n--- 2. Insumos Clave Calculados para 2024 ---\n\n")
##
##
## --- 2. Insumos Clave Calculados para 2024 ---
cat("Ocupación vehicular promedio (ponderada) observada en 2024:\n")
## Ocupación vehicular promedio (ponderada) observada en 2024:
print(knitr::kable(as.data.frame(occ_2024), col.names = c("Modo", "Ocupación Promedio")))
##
##
## |Modo | Ocupación Promedio|
## |:-------------------|------------------:|
## |Moto | 1.100000|
## |Taxi / App | 1.500000|
## |Carro compartido | 4.147798|
## |Vehículo particular | 3.219251|
cat("\nResumen de la tabla 'insumos_2024' (debe estar completa, sin NAs):\n")
##
## Resumen de la tabla 'insumos_2024' (debe estar completa, sin NAs):
print(summary(insumos_2024))
## rol7 modo_cat dist_km_mean freq_sem_mean
## Length:49 Caminata :7 Min. : 0.2859 Min. :2.613
## Class :character Micromovilidad :7 1st Qu.: 6.5295 1st Qu.:2.750
## Mode :character Transp. público :7 Median : 9.1776 Median :3.625
## Vehículo particular:7 Mean : 8.2261 Mean :3.586
## Moto :7 3rd Qu.:10.4257 3rd Qu.:4.300
## Taxi / App :7 Max. :21.0502 Max. :4.771
## Carro compartido :7
## dist_km_mean_glob
## Min. : 1.325
## 1st Qu.: 7.167
## Median : 9.418
## Mean : 8.226
## 3rd Qu.:10.352
## Max. :11.543
##
cat(paste("\n¿Hay algún NA en dist_km_mean?", any(is.na(insumos_2024$dist_km_mean))))
##
## ¿Hay algún NA en dist_km_mean? FALSE
cat(paste("\n¿Hay algún NA en freq_sem_mean?", any(is.na(insumos_2024$freq_sem_mean)), "\n"))
##
## ¿Hay algún NA en freq_sem_mean? FALSE
# --- 3. VERIFICACIÓN DE FACTORES DE EMISIÓN ---
cat("\n\n--- 3. Factores de Emisión Armonizados ---\n\n")
##
##
## --- 3. Factores de Emisión Armonizados ---
cat("Tabla de factores de emisión ('fe') leída y estandarizada desde el CSV:\n")
## Tabla de factores de emisión ('fe') leída y estandarizada desde el CSV:
print(knitr::kable(fe, digits = 2))
##
##
## |modo_cat | gco2_paxkm| gco2_vehkm|
## |:-------------------|----------:|----------:|
## |Caminata | 0| NA|
## |Carro compartido | 65| NA|
## |Micromovilidad | 0| NA|
## |Moto | 195| 258.0|
## |Transp. público | 18| 487.7|
## |Vehículo eléctrico | 0| NA|
## |Vehículo particular | 190| 312.0|
cat(paste("\nFactor de emisión para Vehículo Eléctrico (g/pax-km):", round(factor_ev_paxkm, 2), "\n"))
##
## Factor de emisión para Vehículo Eléctrico (g/pax-km): 0
# --- 4. VERIFICACIÓN DEL CÁLCULO DE PASAJEROS-KILÓMETRO (PKM) ---
cat("\n\n--- 4. Verificación del Cálculo de PKM para 2035 ---\n\n")
##
##
## --- 4. Verificación del Cálculo de PKM para 2035 ---
cat("Resumen de pkm_anuales para el escenario 'Do-Nothing':\n")
## Resumen de pkm_anuales para el escenario 'Do-Nothing':
print(summary(pkm_DoNothing$pkm_anuales))
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0 47653 238827 1633875 739023 31999288
if(any(is.na(pkm_DoNothing$pkm_anuales))) {
cat("\n¡ALERTA! Se encontraron NAs en pkm_DoNothing.\n")
}
cat("\nPrimeras filas de la tabla 'pkm_DoNothing':\n")
##
## Primeras filas de la tabla 'pkm_DoNothing':
print(knitr::kable(head(pkm_DoNothing), digits = 0))
##
##
## |rol7 |modo_cat | pkm_anuales|
## |:-----------------------|:----------------|-----------:|
## |Empleado administrativo |Caminata | 35261|
## |Empleado administrativo |Carro compartido | 70007|
## |Empleado administrativo |Micromovilidad | 157564|
## |Empleado administrativo |Moto | 1188179|
## |Empleado administrativo |Taxi / App | 132267|
## |Empleado administrativo |Transp. público | 3656557|
cat("\nResumen de pkm_anuales para el escenario 'Vehicular Sostenible':\n")
##
## Resumen de pkm_anuales para el escenario 'Vehicular Sostenible':
print(summary(pkm_Esc3$pkm_anuales))
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0 57424 239210 1635671 699798 32146292
if(any(is.na(pkm_Esc3$pkm_anuales))) {
cat("\n¡ALERTA! Se encontraron NAs en pkm_Esc3.\n")
}
cat("\nPrimeras filas de la tabla 'pkm_Esc3':\n")
##
## Primeras filas de la tabla 'pkm_Esc3':
print(knitr::kable(head(pkm_Esc3), digits = 0))
##
##
## |rol7 |modo_cat | pkm_anuales|
## |:-----------------------|:----------------|-----------:|
## |Empleado administrativo |Caminata | 35855|
## |Empleado administrativo |Carro compartido | 90225|
## |Empleado administrativo |Micromovilidad | 160219|
## |Empleado administrativo |Moto | 1180962|
## |Empleado administrativo |Taxi / App | 129481|
## |Empleado administrativo |Transp. público | 3718163|
# --- 5. VERIFICACIÓN DE RESULTADOS FINALES DE EMISIONES ---
cat("\n\n--- 5. Resultados Finales de Emisiones (tCO2e) para 2035 ---\n\n")
##
##
## --- 5. Resultados Finales de Emisiones (tCO2e) para 2035 ---
cat("Emisiones Totales por Escenario:\n")
## Emisiones Totales por Escenario:
print(knitr::kable(totales_esc, digits = 1))
##
##
## |escenario | emisiones_totales_tCO2e|
## |:--------------------|-----------------------:|
## |Do-Nothing | 4473.1|
## |Vehicular Sostenible | 3881.4|
cat("\nEmisiones por Rol y Escenario:\n")
##
## Emisiones por Rol y Escenario:
print(knitr::kable(resultados_rol, digits = 1))
##
##
## |rol7 | emisiones_tCO2e|escenario |
## |:-----------------------------|---------------:|:--------------------|
## |Empleado administrativo | 382.1|Do-Nothing |
## |Estudiantes - Doctorado | 51.8|Do-Nothing |
## |Estudiantes - Especialización | 42.4|Do-Nothing |
## |Estudiantes - Maestría | 674.9|Do-Nothing |
## |Estudiantes - Pregrado | 2989.3|Do-Nothing |
## |Profesor de cátedra | 214.5|Do-Nothing |
## |Profesor de planta | 118.1|Do-Nothing |
## |Empleado administrativo | 359.9|Vehicular Sostenible |
## |Estudiantes - Doctorado | 41.2|Vehicular Sostenible |
## |Estudiantes - Especialización | 35.1|Vehicular Sostenible |
## |Estudiantes - Maestría | 590.9|Vehicular Sostenible |
## |Estudiantes - Pregrado | 2584.2|Vehicular Sostenible |
## |Profesor de cátedra | 178.5|Vehicular Sostenible |
## |Profesor de planta | 91.7|Vehicular Sostenible |
cat("\n\n==================================================")
##
##
## ==================================================
cat("\n== FIN DEL REPORTE DE DIAGNÓSTICO ==")
##
## == FIN DEL REPORTE DE DIAGNÓSTICO ==
cat("\n==================================================\n")
##
## ==================================================
# ==========================================================
# EMU24 - DIAGNÓSTICO INTEGRAL (para correr al final del Rmd)
# ==========================================================
suppressPackageStartupMessages({
library(tidyverse); library(janitor); library(knitr); library(scales)
})
cat("\n==================== DIAGNÓSTICO EMU24 ====================\n")
##
## ==================== DIAGNÓSTICO EMU24 ====================
# ---------- Helpers ----------
ok <- function(x) if (isTRUE(x)) "OK" else "REVISAR"
exists_safe <- function(x) exists(x, inherits = TRUE)
get_tbl <- function(x) { obj <- get0(x, ifnotfound = NULL, inherits = TRUE); if (is.data.frame(obj)) obj else NULL }
num <- function(x) suppressWarnings(as.numeric(x))
section <- function(t) cat("\n\n---------------- ", t, " ----------------\n", sep = "")
bullet <- function(...){ cat(" • ", sprintf(...), "\n") }
expect_modes <- c("Caminata","Micromovilidad","Transp. público",
"Vehículo particular","Moto","Taxi / App","Carro compartido")
expect_roles <- c("Estudiantes - Pregrado","Estudiantes - Especialización",
"Estudiantes - Maestría","Estudiantes - Doctorado",
"Profesor de planta","Profesor de cátedra","Empleado administrativo")
# ---------- 0) Existencia de objetos clave ----------
section("Existencia de objetos")
##
##
## ---------------- Existencia de objetos ----------------
objs <- c("dicc","bd","emu","w_tbl","S_base","S0","scn_S","scn_uni",
"pop_proj_roles","pop_base_roles","mix_by_year",
"dist_rol_modo","freq_sem_rol","insumos_2024",
"fe","factor_paxkm",
"pkm_DoNothing","pkm_Esc3",
"emis_2035_DoNothing","emis_2035_Esc3",
"resultados_rol","totales_esc")
chk <- tibble(obj = objs, existe = map_lgl(objs, exists_safe))
print(knitr::kable(chk, caption = "Objetos encontrados en el environment"))
##
##
## Table: Objetos encontrados en el environment
##
## |obj |existe |
## |:-------------------|:------|
## |dicc |TRUE |
## |bd |TRUE |
## |emu |TRUE |
## |w_tbl |TRUE |
## |S_base |TRUE |
## |S0 |TRUE |
## |scn_S |TRUE |
## |scn_uni |TRUE |
## |pop_proj_roles |TRUE |
## |pop_base_roles |TRUE |
## |mix_by_year |TRUE |
## |dist_rol_modo |TRUE |
## |freq_sem_rol |TRUE |
## |insumos_2024 |TRUE |
## |fe |TRUE |
## |factor_paxkm |TRUE |
## |pkm_DoNothing |TRUE |
## |pkm_Esc3 |TRUE |
## |emis_2035_DoNothing |TRUE |
## |emis_2035_Esc3 |TRUE |
## |resultados_rol |TRUE |
## |totales_esc |TRUE |
emu <- get_tbl("emu")
if (!is.null(emu)) {
section("EMU base: estructura y cobertura")
# --- CORRECCIÓN AQUÍ ---
# Se formatea el número a texto con comas ANTES de pasarlo al string.
# La función bullet ahora solo recibe strings (%s).
bullet("Filas: %s | Columnas: %s", scales::comma(nrow(emu)), scales::comma(ncol(emu)))
req_cols <- c("rol7","hacia_modo_ppal","hacia_modo_ppal_lbl","modo_cat","w_exp","dist_km","dur_min_hacia","frecuencia_viaje_u")
miss <- setdiff(req_cols, names(emu))
bullet("Columnas requeridas presentes: %s", ok(length(miss)==0))
if (length(miss)) bullet("Faltan: %s", paste(miss, collapse = ", "))
if (all(c("rol7","modo_cat","w_exp") %in% names(emu))) {
na_tab <- tibble(
var = c("rol7","modo_cat","w_exp","dist_km","dur_min_hacia","frecuencia_viaje_u"),
na_rate = sapply(var, function(v) mean(is.na(emu[[v]])))
)
print(knitr::kable(na_tab, digits = 3, caption = "Porcentaje de NA por variable clave"))
# --- CORRECCIÓN PREVENTIVA AQUÍ ---
# Usamos el mismo método para asegurar que la suma de pesos se imprima correctamente.
bullet("Suma de pesos (w_exp) sobre filas con rol7 y modo_cat no nulos: %s",
scales::comma(sum(emu$w_exp[!is.na(emu$rol7) & !is.na(emu$modo_cat)], na.rm = TRUE)))
# Valores atípicos básicos
if ("dist_km" %in% names(emu)) {
d99 <- suppressWarnings(quantile(emu$dist_km, 0.99, na.rm = TRUE))
bullet("Distancia p99: %.2f km (p>p99: %d casos)", d99, sum(emu$dist_km > d99, na.rm = TRUE))
}
if ("dur_min_hacia" %in% names(emu)) {
t99 <- suppressWarnings(quantile(emu$dur_min_hacia, 0.99, na.rm = TRUE))
bullet("Duración p99: %.1f min (p>p99: %d casos)", t99, sum(emu$dur_min_hacia > t99, na.rm = TRUE))
}
# Niveles esperados de modo y roles
bullet("Modos esperados presentes: %s", ok(all(expect_modes %in% unique(na.omit(emu$modo_cat)))))
bullet("Roles esperados presentes: %s", ok(all(expect_roles %in% unique(na.omit(emu$rol7)))))
}
}
##
##
## ---------------- EMU base: estructura y cobertura ----------------
## • Filas: 1,278 | Columnas: 182
## • Columnas requeridas presentes: OK
##
##
## Table: Porcentaje de NA por variable clave
##
## |var | na_rate|
## |:------------------|-------:|
## |rol7 | 0.156|
## |modo_cat | 0.207|
## |w_exp | 0.156|
## |dist_km | 0.308|
## |dur_min_hacia | 0.219|
## |frecuencia_viaje_u | 0.194|
## • Suma de pesos (w_exp) sobre filas con rol7 y modo_cat no nulos: 22,258
## • Distancia p99: 17.86 km (p>p99: 9 casos)
## • Duración p99: 165.0 min (p>p99: 10 casos)
## • Modos esperados presentes: OK
## • Roles esperados presentes: OK
# ---------- 2) Matriz S0 (2024) y escenarios scn_S ----------
S0 <- get_tbl("S0"); scn_S <- get_tbl("scn_S")
if (!is.null(S0)) {
section("S0 (2024): sanity checks")
miss <- setdiff(c("rol7","modo_cat","share"), names(S0))
bullet("Columnas requeridas en S0: %s", ok(length(miss)==0))
if (length(miss)) bullet("Faltan en S0: %s", paste(miss, collapse = ", "))
if (all(c("rol7","modo_cat","share") %in% names(S0))) {
s_sum <- S0 %>% group_by(rol7) %>% summarise(sum_share = sum(share, na.rm = TRUE), n = n(), .groups = "drop")
print(knitr::kable(s_sum, digits = 4, caption = "Suma de shares por rol en S0 (debe ser 1.0)"))
bullet("Shares S0 dentro de [0,1]: %s", ok(all(S0$share >= -1e-6 & S0$share <= 1+1e-6, na.rm = TRUE)))
bullet("Sum(share)≈1 por rol (tol=1e-4): %s", ok(all(abs(s_sum$sum_share - 1) < 1e-4)))
}
}
##
##
## ---------------- S0 (2024): sanity checks ----------------
## • Columnas requeridas en S0: OK
##
##
## Table: Suma de shares por rol en S0 (debe ser 1.0)
##
## |rol7 | sum_share| n|
## |:-----------------------------|---------:|--:|
## |Empleado administrativo | 1| 7|
## |Estudiantes - Doctorado | 1| 7|
## |Estudiantes - Especialización | 1| 7|
## |Estudiantes - Maestría | 1| 7|
## |Estudiantes - Pregrado | 1| 7|
## |Profesor de cátedra | 1| 7|
## |Profesor de planta | 1| 7|
## • Shares S0 dentro de [0,1]: OK
## • Sum(share)≈1 por rol (tol=1e-4): OK
if (!is.null(scn_S)) {
section("Escenarios determinísticos (scn_S): coherencia")
miss <- setdiff(c("rol7","modo_cat","share","anio","escenario"), names(scn_S))
bullet("Columnas requeridas en scn_S: %s", ok(length(miss)==0))
if (length(miss)) bullet("Faltan en scn_S: %s", paste(miss, collapse = ", "))
if (all(c("rol7","modo_cat","share","anio","escenario") %in% names(scn_S))) {
s_chk <- scn_S %>% group_by(escenario, anio, rol7) %>%
summarise(sum_share = sum(share, na.rm = TRUE), .groups = "drop")
viol <- s_chk %>% filter(abs(sum_share - 1) > 1e-4)
bullet("Sum(share)≈1 por (escenario, año, rol): %s", ok(nrow(viol)==0))
if (nrow(viol)) {
bullet("Primeras filas con violación de cierre composicional:")
print(head(viol, 10))
}
}
}
##
##
## ---------------- Escenarios determinísticos (scn_S): coherencia ----------------
## • Columnas requeridas en scn_S: OK
## • Sum(share)≈1 por (escenario, año, rol): OK
# ---------- 3) Insumos 2024 (distancias/frecuencias) ----------
insumos_2024 <- get_tbl("insumos_2024")
if (!is.null(insumos_2024)) {
section("Insumos 2024: distancias y frecuencias")
miss <- setdiff(c("rol7","modo_cat","dist_km_mean","freq_sem_mean"), names(insumos_2024))
bullet("Columnas requeridas en insumos_2024: %s", ok(length(miss)==0))
if (length(miss)) bullet("Faltan: %s", paste(miss, collapse = ", "))
if (all(c("dist_km_mean","freq_sem_mean") %in% names(insumos_2024))) {
na_ins <- summarise(insumos_2024,
pct_na_dist = mean(is.na(dist_km_mean)),
pct_na_freq = mean(is.na(freq_sem_mean)))
print(knitr::kable(na_ins, digits = 3, caption = "NA rates en insumos_2024"))
rng <- insumos_2024 %>% summarise(
dist_min = min(dist_km_mean, na.rm = TRUE),
dist_max = max(dist_km_mean, na.rm = TRUE),
freq_min = min(freq_sem_mean, na.rm = TRUE),
freq_max = max(freq_sem_mean, na.rm = TRUE)
)
print(knitr::kable(rng, digits = 3, caption = "Rangos de dist_km_mean y freq_sem_mean"))
}
}
##
##
## ---------------- Insumos 2024: distancias y frecuencias ----------------
## • Columnas requeridas en insumos_2024: OK
##
##
## Table: NA rates en insumos_2024
##
## | pct_na_dist| pct_na_freq|
## |-----------:|-----------:|
## | 0| 0|
##
##
## Table: Rangos de dist_km_mean y freq_sem_mean
##
## | dist_min| dist_max| freq_min| freq_max|
## |--------:|--------:|--------:|--------:|
## | 0.286| 21.05| 2.613| 4.771|
# ---------- 4) Factores de emisión ----------
fe <- get_tbl("fe")
if (!is.null(fe)) {
section("Factores de emisión: cobertura vs categorías EMU")
miss <- setdiff(c("modo_cat","gco2_paxkm","gco2_vehkm"), names(fe))
bullet("Columnas requeridas en fe: %s", ok(length(miss)==0))
if (length(miss)) bullet("Faltan: %s", paste(miss, collapse = ", "))
# ¿Hay duplicados por modo_cat?
if ("modo_cat" %in% names(fe)) {
dups <- fe %>% count(modo_cat) %>% filter(n>1)
bullet("Duplicados de modo en fe: %s", ok(nrow(dups)==0))
if (nrow(dups)) print(dups)
}
# Cobertura de modos esperados
bullet("Todos los modos esperados tienen FE (directo o veh-km): %s",
ok(all(expect_modes %in% fe$modo_cat)))
# Vista rápida
print(knitr::kable(fe %>% arrange(modo_cat), digits = 2, caption = "Resumen de factores (gCO2/pax-km o gCO2/veh-km)"))
}
##
##
## ---------------- Factores de emisión: cobertura vs categorías EMU ----------------
## • Columnas requeridas en fe: OK
## • Duplicados de modo en fe: OK
## • Todos los modos esperados tienen FE (directo o veh-km): REVISAR
##
##
## Table: Resumen de factores (gCO2/pax-km o gCO2/veh-km)
##
## |modo_cat | gco2_paxkm| gco2_vehkm|
## |:-------------------|----------:|----------:|
## |Caminata | 0| NA|
## |Carro compartido | 65| NA|
## |Micromovilidad | 0| NA|
## |Moto | 195| 258.0|
## |Transp. público | 18| 487.7|
## |Vehículo eléctrico | 0| NA|
## |Vehículo particular | 190| 312.0|
# ---------- 5) PKM 2035 por escenario ----------
pkm_DoNothing <- get_tbl("pkm_DoNothing")
pkm_Esc3 <- get_tbl("pkm_Esc3")
summ_pkm <- function(pkm_tbl, name){
if (is.null(pkm_tbl)) { bullet("%s: %s", name, "NO ENCONTRADO"); return(invisible()) }
section(sprintf("PKM 2035 – %s", name))
miss <- setdiff(c("rol7","modo_cat","pkm_anuales"), names(pkm_tbl))
bullet("Columnas requeridas: %s", ok(length(miss)==0))
if (length(miss)) bullet("Faltan: %s", paste(miss, collapse = ", "))
if (all(c("rol7","modo_cat","pkm_anuales") %in% names(pkm_tbl))) {
neg <- sum(pkm_tbl$pkm_anuales < 0, na.rm = TRUE)
na <- mean(is.na(pkm_tbl$pkm_anuales))
bullet("PKM >= 0: %s (negativos: %d)", ok(neg==0), neg)
bullet("NA rate en PKM: %.3f", na)
top <- pkm_tbl %>% group_by(modo_cat) %>% summarise(pkm = sum(pkm_anuales, na.rm=TRUE)) %>%
arrange(desc(pkm))
print(knitr::kable(top, digits = 1, caption = sprintf("PKM por modo – %s", name)))
}
}
summ_pkm(pkm_DoNothing, "Do-Nothing")
##
##
## ---------------- PKM 2035 – Do-Nothing ----------------
## • Columnas requeridas: OK
## • PKM >= 0: OK (negativos: 0)
## • NA rate en PKM: 0.000
##
##
## Table: PKM por modo – Do-Nothing
##
## |modo_cat | pkm|
## |:-------------------|--------:|
## |Transp. público | 42203907|
## |Vehículo particular | 20864267|
## |Carro compartido | 6517880|
## |Moto | 4437040|
## |Taxi / App | 3992001|
## |Micromovilidad | 1428992|
## |Caminata | 615792|
summ_pkm(pkm_Esc3, "Vehicular Sostenible")
##
##
## ---------------- PKM 2035 – Vehicular Sostenible ----------------
## • Columnas requeridas: OK
## • PKM >= 0: OK (negativos: 0)
## • NA rate en PKM: 0.000
##
##
## Table: PKM por modo – Vehicular Sostenible
##
## |modo_cat | pkm|
## |:-------------------|----------:|
## |Transp. público | 42574739.9|
## |Vehículo particular | 18702638.0|
## |Carro compartido | 8492188.4|
## |Moto | 4399502.2|
## |Taxi / App | 3897994.8|
## |Micromovilidad | 1454820.9|
## |Caminata | 626011.6|
# ---------- 6) Emisiones 2035 ----------
emis_2035_DoNothing <- get_tbl("emis_2035_DoNothing")
emis_2035_Esc3 <- get_tbl("emis_2035_Esc3")
resultados_rol <- get_tbl("resultados_rol")
totales_esc <- get_tbl("totales_esc")
if (!is.null(totales_esc)) {
section("Emisiones totales 2035 por escenario (tCO2e)")
print(knitr::kable(totales_esc, digits = 1))
}
##
##
## ---------------- Emisiones totales 2035 por escenario (tCO2e) ----------------
##
##
## |escenario | emisiones_totales_tCO2e|
## |:--------------------|-----------------------:|
## |Do-Nothing | 4473.1|
## |Vehicular Sostenible | 3881.4|
if (!is.null(resultados_rol)) {
section("Emisiones 2035 por rol y escenario (tCO2e)")
print(knitr::kable(resultados_rol %>% arrange(escenario, rol7), digits = 1))
}
##
##
## ---------------- Emisiones 2035 por rol y escenario (tCO2e) ----------------
##
##
## |rol7 | emisiones_tCO2e|escenario |
## |:-----------------------------|---------------:|:--------------------|
## |Empleado administrativo | 382.1|Do-Nothing |
## |Estudiantes - Doctorado | 51.8|Do-Nothing |
## |Estudiantes - Especialización | 42.4|Do-Nothing |
## |Estudiantes - Maestría | 674.9|Do-Nothing |
## |Estudiantes - Pregrado | 2989.3|Do-Nothing |
## |Profesor de cátedra | 214.5|Do-Nothing |
## |Profesor de planta | 118.1|Do-Nothing |
## |Empleado administrativo | 359.9|Vehicular Sostenible |
## |Estudiantes - Doctorado | 41.2|Vehicular Sostenible |
## |Estudiantes - Especialización | 35.1|Vehicular Sostenible |
## |Estudiantes - Maestría | 590.9|Vehicular Sostenible |
## |Estudiantes - Pregrado | 2584.2|Vehicular Sostenible |
## |Profesor de cátedra | 178.5|Vehicular Sostenible |
## |Profesor de planta | 91.7|Vehicular Sostenible |
# ---------- 7) Consistencia de inputs de emisiones ----------
# Prop EV y ocupaciones (si existen en el entorno)
section("Parámetros de emisiones: rangos")
##
##
## ---------------- Parámetros de emisiones: rangos ----------------
prop_ev_2035_DoNothing <- get0("prop_ev_2035_DoNothing", ifnotfound = NA_real_)
prop_ev_2035_Esc3 <- get0("prop_ev_2035_Esc3", ifnotfound = NA_real_)
occ_target_carpool_Esc3 <- get0("occ_target_carpool_Esc3", ifnotfound = NA_real_)
if (is.finite(prop_ev_2035_DoNothing)) bullet("prop_ev_2035_DoNothing en [0,1]: %s (valor=%.3f)",
ok(prop_ev_2035_DoNothing>=0 && prop_ev_2035_DoNothing<=1),
prop_ev_2035_DoNothing)
## • prop_ev_2035_DoNothing en [0,1]: OK (valor=0.150)
if (is.finite(prop_ev_2035_Esc3)) bullet("prop_ev_2035_Esc3 en [0,1]: %s (valor=%.3f)",
ok(prop_ev_2035_Esc3>=0 && prop_ev_2035_Esc3<=1),
prop_ev_2035_Esc3)
## • prop_ev_2035_Esc3 en [0,1]: OK (valor=0.350)
if (is.finite(occ_target_carpool_Esc3)) bullet("occ_target_carpool_Esc3 >= 1: %s (valor=%.2f)",
ok(occ_target_carpool_Esc3>=1), occ_target_carpool_Esc3)
## • occ_target_carpool_Esc3 >= 1: OK (valor=2.60)
# ¿Se pueden evaluar factores por modo?
if (!is.null(fe) && exists_safe("factor_paxkm")) {
section("Factor final gCO2/pax-km por modo (prueba con Do-Nothing)")
occ_vec <- get0("occ_2024", ifnotfound = list())
prop_ev <- if (is.finite(prop_ev_2035_DoNothing)) prop_ev_2035_DoNothing else 0
facs <- tibble(modo_cat = expect_modes) %>%
rowwise() %>%
mutate(f_gco2 = tryCatch(factor_paxkm(modo_cat, occ_vec, prop_ev, fe), error = function(e) NA_real_)) %>%
ungroup()
print(knitr::kable(facs, digits = 2, caption = "Factor final estimado por modo (gCO2e/pax-km)"))
}
##
##
## ---------------- Factor final gCO2/pax-km por modo (prueba con Do-Nothing) ----------------
##
##
## Table: Factor final estimado por modo (gCO2e/pax-km)
##
## |modo_cat | f_gco2|
## |:-------------------|------:|
## |Caminata | 0.00|
## |Micromovilidad | 0.00|
## |Transp. público | 18.00|
## |Vehículo particular | 82.38|
## |Moto | 195.00|
## |Taxi / App | 176.80|
## |Carro compartido | 65.00|
# ---------- 8) Población proyectada ----------
pop_proj_roles <- get_tbl("pop_proj_roles")
if (!is.null(pop_proj_roles)) {
section("Población proyectada por rol (2035)")
if (all(c("anio","rol7","n") %in% names(pop_proj_roles))) {
p2035 <- pop_proj_roles %>% filter(anio == 2035) %>%
group_by(rol7) %>% summarise(N_rol = sum(n, na.rm = TRUE), .groups = "drop")
print(knitr::kable(p2035, caption = "N proyectado 2035 por rol"))
bullet("Total 2035: %s", format(sum(p2035$N_rol, na.rm = TRUE), big.mark = ","))
} else {
bullet("Columnas esperadas en pop_proj_roles (anio, rol7, n): %s", "REVISAR")
}
}
##
##
## ---------------- Población proyectada por rol (2035) ----------------
##
##
## Table: N proyectado 2035 por rol
##
## |rol7 | N_rol|
## |:-----------------------------|----------:|
## |Empleado administrativo | 1872.2448|
## |Estudiantes - Doctorado | 431.5549|
## |Estudiantes - Especialización | 199.6338|
## |Estudiantes - Maestría | 4730.6501|
## |Estudiantes - Pregrado | 15147.9558|
## |Profesor de cátedra | 1415.2281|
## |Profesor de planta | 733.8883|
## • Total 2035: 24,531.16
# ---------- 9) Resumen final ----------
section("Resumen final")
##
##
## ---------------- Resumen final ----------------
bullet("Objetos faltantes: %d", sum(!chk$existe))
## • Objetos faltantes: 0
if (sum(!chk$existe)) {
print(chk %>% filter(!existe))
}
bullet("Diagnóstico completado. Revisa tablas y banderas 'REVISAR' arriba.")
## • Diagnóstico completado. Revisa tablas y banderas 'REVISAR' arriba.
# ---------- 10) (Opcional) sessionInfo corto ----------
section("Session info (paquetes núcleo)")
##
##
## ---------------- Session info (paquetes núcleo) ----------------
si <- sessionInfo()
cat(paste0("R ", si$R.version$version.string, "\n"))
## R R version 4.4.2 (2024-10-31 ucrt)
cat("Packages loaded:\n")
## Packages loaded:
base_pkgs <- c("tidyverse","dplyr","tidyr","readr","ggplot2","janitor","knitr","scales","lubridate")
loaded <- base_pkgs[base_pkgs %in% (.packages())]
cat(paste(" -", loaded), sep = "\n")
## - tidyverse
## - dplyr
## - tidyr
## - readr
## - ggplot2
## - janitor
## - knitr
## - scales
## - lubridate
cat("\n===========================================================\n")
##
## ===========================================================
## ---- graficos-presentacion-final, echo=FALSE, message=FALSE, warning=FALSE, fig.width=10, fig.height=6 ----
# ==========================================================
# 1. ESTÉTICA VISUAL (PALETAS Y TEMA)
# ==========================================================
paleta_modos_agregados <- c(
"Caminata" = "#339933", "Micromovilidad" = "#33cc33", "Transp. público" = "#2266bf",
"Vehículo particular" = "#505050", "Carro compartido" = "#994499",
"Moto" = "#aa2211", "Taxi / App" = "#cc6600"
)
theme_uniandes <- function(base_size = 12, base_family = "Roboto") {
theme_minimal(base_size = base_size, base_family = base_family) +
theme(
plot.title = element_text(face = "bold", size = rel(1.6), hjust = 0.5, margin = margin(b=10)),
plot.subtitle = element_text(size = rel(1.2), hjust = 0.5, color = "gray40", margin = margin(b=15)),
axis.title = element_text(face = "bold", size = rel(1.1)),
axis.text = element_text(color = "gray30", size = rel(1.0)),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "bottom",
legend.title = element_text(face = "bold"),
panel.grid.major = element_line(color = "grey92", linewidth = 0.5),
panel.grid.minor = element_blank(),
strip.text = element_text(face = "bold", size = rel(1.1), hjust = 0)
)
}
# ==========================================================
# GRÁFICO 1: Evolución de la Población Uniandina (2010-2024) - CORREGIDO
# ==========================================================
# Cargar y procesar los datos históricos
pop_historica_raw <- read_csv("uniandes_poblacion_2010_2024_extendida.csv") %>%
clean_names()
## Rows: 15 Columns: 9
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (3): Ano, Profesores de planta (TCE), Estudiantes de doctorado
## num (6): Profesores de cátedra, Administrativos (planta sin Doc/Inv), Estudi...
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
pop_historica_long <- pop_historica_raw %>%
pivot_longer(
cols = -ano,
names_to = "rol7",
values_to = "n"
) %>%
mutate(n = as.numeric(gsub(",", "", n))) %>%
filter(!is.na(n), rol7 != "total_comunidad_aprox")
# CORRECCIÓN: Se crea un subconjunto de datos para etiquetar solo 2010 y 2024
# 1. Preparar datos para las etiquetas de inicio y fin
label_data_g1 <- pop_historica_long %>% filter(ano %in% c(2010, 2024))
# 2. Crear el gráfico
g1 <- pop_historica_long %>%
ggplot(aes(x = ano, y = n, color = rol7)) +
geom_line(linewidth = 1.2) +
geom_point(data = label_data_g1, size = 2.5) +
# 3. Añadir etiquetas con ggrepel y posicionamiento izquierda/derecha
ggrepel::geom_text_repel(
data = label_data_g1,
aes(label = scales::comma(n, accuracy = 1)), # Etiqueta numérica
fontface = "bold", size = 3.5,
nudge_x = ifelse(label_data_g1$ano == 2010, -0.7, 0.7), # Izquierda/Derecha
hjust = ifelse(label_data_g1$ano == 2010, 1, 0), # Alineación
direction = "y", segment.size = 0.2, min.segment.length = 0
) +
scale_y_continuous(labels = scales::comma, expand = expansion(mult = c(0.05, 0.15))) +
scale_x_continuous(breaks = seq(2010, 2024, 2)) +
labs(
title = "Evolución de la comunidad uniandina (2010-2024)",
subtitle = "Datos históricos muestran cambios en la composición de la comunidad",
x = "Año", y = "Número de personas"
) +
theme_uniandes() +
theme(legend.position = "none", axis.text.x = element_text(angle = 0, hjust = 0.5))
print(g1)

# ==========================================================
# GRÁFICO 2: Repartición Modal 2024 (Diagnóstico Actual)
# ==========================================================
g2 <- uni_2024 %>%
ggplot(aes(x = reorder(modo_cat, -share), y = share, fill = modo_cat)) +
geom_col(width = 0.7) +
geom_text(aes(label = scales::percent(share, accuracy = 0.1)), vjust = -0.5,
family = "Roboto", fontface = "bold", color = "gray30", size = 4) +
scale_y_continuous(labels = scales::percent, limits = c(0, 0.55), expand = expansion(mult = c(0, .05))) +
scale_fill_manual(values = paleta_modos_agregados) +
labs(
title = "Repartición Modal de la Comunidad Uniandina (2024)",
subtitle = "El vehículo particular y el transporte público dominan los viajes",
x = "Modo de Transporte", y = "% de Viajes" # Título de eje corregido
) +
theme_uniandes() +
theme(legend.position = "none")
print(g2)

# ==========================================================
# GRÁFICO 3: Proyección Poblacional (2025-2035)
# ==========================================================
# 1. Preparar datos para etiquetas
label_data_g3 <- pop_proj_roles %>% filter(anio %in% c(2025, 2035))
# 2. Crear el gráfico
g3 <- pop_proj_roles %>%
filter(anio >= 2024) %>%
ggplot(aes(x = anio, y = n, color = rol7)) +
geom_line(linewidth = 1.2, linetype = "dashed") +
geom_point(data = label_data_g3, size = 2.5) +
# 3. Añadir etiquetas con ggrepel y posicionamiento izquierda/derecha
ggrepel::geom_text_repel(
data = label_data_g3,
aes(label = scales::comma(n, accuracy = 1)), # Etiqueta numérica
fontface = "bold", size = 3.5,
nudge_x = ifelse(label_data_g3$anio == 2025, -0.9, 0.9), # Izquierda/Derecha
hjust = ifelse(label_data_g3$anio == 2025, 1, 0), # Alineación
direction = "y", segment.size = 0.2, min.segment.length = 0
) +
scale_y_continuous(labels = scales::comma) +
scale_x_continuous(breaks = seq(2025, 2035, 2)) +
labs(
title = "Proyección de la comunidad uniandina (2025-2035)",
subtitle = "La composición interna de la comunidad continúa su transformación",
x = "Año", y = "Población proyectada"
) +
theme_uniandes() +
theme(legend.position = "none", axis.text.x = element_text(angle = 0, hjust = 0.5))
print(g3)

# ==========================================================
# GRÁFICO 4: Factores de Emisión por Pasajero-km
# ==========================================================
g4 <- fe %>%
filter(is.finite(gco2_paxkm)) %>%
ggplot(aes(x = reorder(modo_cat, gco2_paxkm), y = gco2_paxkm, fill = modo_cat)) +
geom_col(width = 0.7) +
geom_text(aes(label = round(gco2_paxkm, 1)), vjust = 1.5, color = "white", fontface = "bold", size = 4) +
scale_fill_manual(values = paleta_modos_agregados) +
labs(
title = "Factor de Emisión por Modo de Transporte",
subtitle = "Gramos de CO2 equivalente por pasajero-kilómetro",
x = "Modo de Transporte", y = "Factor de Emisión\n(gCO2eq/pax-km)" # Título de eje corregido
) +
theme_uniandes() +
theme(legend.position = "none")
print(g4)

# ==========================================================
# GRÁFICOS 5 y 6: Evolución Modal - Escenario "Do Nothing"
# ==========================================================
# ---------- A) LÍNEAS con banda 5–95% y etiquetas de Escenario 3 ----------
# 1. Preparar datos para etiquetas
labels_mc <- mc_ci %>% filter(anio %in% c(2025, 2035))
# 2. Crear el gráfico
p_mc_line <- ggplot(mc_ci, aes(x = anio, color = modo_cat, fill = modo_cat)) +
geom_ribbon(aes(ymin = p05/100, ymax = p95/100), alpha = .18, color = NA) +
geom_line(aes(y = p50/100), linewidth = 1.2) +
geom_point(data = labels_mc, aes(y = p50/100), size = 2.5) +
# 3. Añadir etiquetas con ggrepel y posicionamiento izquierda/derecha
ggrepel::geom_text_repel(
data = labels_mc,
aes(y = p50/100, label = scales::percent(p50/100, accuracy = 0.1)), # Etiqueta numérica
fontface = "bold", size = 3.5,
nudge_x = ifelse(labels_mc$anio == 2025, -0.9, 0.9), # Izquierda/Derecha
hjust = ifelse(labels_mc$anio == 2025, 1, 0), # Alineación
direction = "y", segment.size = 0.2, min.segment.length = 0
) +
scale_y_continuous(labels = scales::label_percent()) +
scale_x_continuous(breaks = seq(2025, 2035, 2)) +
scale_color_manual(values = paleta_modos_agregados) +
scale_fill_manual(values = paleta_modos_agregados) +
labs(
title = "Evolución proyectada del reparto modal (2025–2035)",
subtitle = "Escenario base (Do Nothing): mediana (línea) y banda de confianza 90%",
x = "Año", y = "% del reparto modal"
) +
theme_uniandes() +
theme(legend.position = "none", axis.text.x = element_text(angle = 0, hjust = 0.5))
print(p_mc_line)

p_mc_stack <- ggplot(mc_ci, aes(x = factor(anio), y = p50 / 100, fill = modo_cat)) +
geom_col(position = "fill", width = 0.8) +
geom_text(
aes(label = ifelse(p50 / 100 >= 0.07, scales::percent(p50 / 100, accuracy = 0.1), "")),
position = position_stack(vjust = 0.5), color = "white", size = 3.0
) +
scale_y_continuous(labels = scales::label_percent()) +
scale_fill_manual(values = paleta_modos_agregados) +
labs(
title = "Composición Modal Proyectada (2025-2035)", # Título corregido
subtitle = "Mediana del Escenario Base (Simulaciones de Monte Carlo)",
x = "Año", y = "% del Reparto Modal Total", # Título de eje corregido
fill = "Categoría modal"
) +
theme_uniandes() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
print(p_mc_stack)

# ==========================================================
# GRÁFICOS 7 y 8: Evolución Modal - Escenario 3 (Sostenible)
# ==========================================================
# 1. Preparar datos para las etiquetas (LA CORRECCIÓN CLAVE ESTÁ AQUÍ)
# Creamos una columna 'share_label' que contiene el texto ya formateado.
labels_g7 <- proj_uni_sostenible %>%
filter(anio %in% c(2025, 2035)) %>%
mutate(
share_label = scales::percent(share, accuracy = 0.1)
)
# CORRECCIÓN: Se aplica el estilo de etiquetado del "Do Nothing" a las barras del Escenario 3.
# Paso 1: Crear una columna nueva para las etiquetas en el dataframe
proj_uni_sostenible_con_etiquetas <- proj_uni_sostenible %>%
mutate(
etiqueta_share = ifelse(
# La condición sigue siendo la misma:
modo_cat %in% c("Carro compartido", "Vehículo particular") | share >= 0.08,
# Formateamos la etiqueta aquí directamente
scales::percent(share, accuracy = 0.1),
# Si no, la dejamos en blanco
""
)
)
# Paso 2: Usar el nuevo dataframe en ggplot y simplificar el geom_text
g8 <- proj_uni_sostenible_con_etiquetas %>%
ggplot(aes(x = factor(anio), y = share, fill = modo_cat)) +
# Capa 1: Barras
geom_col(position = "fill", width = 0.85) +
# Capa 2: Etiquetas (ahora mucho más simple)
geom_text(
# El aes ahora solo necesita llamar a la columna que ya creamos
aes(label = etiqueta_share),
position = position_stack(vjust = 0.5),
color = "white",
size = 3.2,
) +
# Escalas, etiquetas y tema (sin cambios)
scale_y_continuous(labels = scales::label_percent()) +
scale_fill_manual(values = paleta_modos_agregados, name = "Modo") +
labs(
title = "Evolución Modal (Escenario Sostenible)",
subtitle = "El Carro Compartido crece al absorber viajes del Vehículo Particular y otros modos",
x = "Año de la Simulación",
y = "% del Reparto Modal Total"
) +
theme_uniandes()
print(g8)

# ==========================================================
# GRÁFICO 9: ANÁLISIS DE EMISIONES 2035 POR ESCENARIO
# ==========================================================
emisiones_final_con_poblacion <- resultados_rol %>%
left_join(pop_2035, by = "rol7") %>%
mutate(
emisiones_per_capita_kg = (emisiones_tCO2e * 1000) / N_rol
) %>%
mutate(escenario = factor(escenario, levels = c("Do-Nothing", "Vehicular Sostenible")))
# -- Gráfico 9a: Emisiones TOTALES (Títulos corregidos) --
g9a <- ggplot(emisiones_final_con_poblacion,
aes(x = reorder(rol7, -emisiones_tCO2e), y = emisiones_tCO2e, fill = escenario)) +
geom_col(position = "dodge", width = 0.7) +
geom_text(
aes(label = scales::comma(emisiones_tCO2e, accuracy = 1)),
position = position_dodge(width = 0.7), vjust = -0.5, size = 3.5,
family = "Roboto", fontface = "bold"
) +
scale_y_continuous(labels = scales::comma, expand = expansion(mult = c(0, .1))) +
scale_fill_manual(values = c("Do-Nothing" = "#cc6600", "Vehicular Sostenible" = "#339933"), name = "Escenario") +
labs(
title = "Emisiones Totales Proyectadas por Rol (2035)",
subtitle = "Comparación entre el escenario tendencial y el de políticas activas",
x = "Rol en la Comunidad", # Título de eje corregido
y = "Emisiones Totales\n(tCO2e/año)" # Título de eje corregido
) +
theme_uniandes()
print(g9a)

# -- Gráfico 9b: Emisiones PER CÁPITA (Títulos corregidos) --
g9b <- ggplot(emisiones_final_con_poblacion,
aes(x = reorder(rol7, -emisiones_per_capita_kg), y = emisiones_per_capita_kg, fill = escenario)) +
geom_col(position = "dodge", width = 0.7) +
geom_text(
aes(label = round(emisiones_per_capita_kg, 0)),
position = position_dodge(width = 0.7), vjust = -0.5, size = 3.5,
family = "Roboto", fontface = "bold"
) +
scale_y_continuous(labels = scales::comma, expand = expansion(mult = c(0, .1))) +
scale_fill_manual(values = c("Do-Nothing" = "#cc6600", "Vehicular Sostenible" = "#339933"), name = "Escenario") +
labs(
title = "Emisiones Per Cápita Proyectadas por Rol (2035)",
subtitle = "Impacto de los escenarios en la huella de carbono individual",
x = "Rol en la Comunidad", # Título de eje corregido
y = "Emisiones Per Cápita\n(kg CO2e/persona/año)" # Título de eje corregido
) +
theme_uniandes()
print(g9b)

# ==========================================================
# GRÁFICO 10: Evolución de Tecnología Vehicular (Escenario 3)
# ==========================================================
g10 <- ev_traj %>%
mutate(prop_ice = 1 - prop_ev) %>%
pivot_longer(cols = c(prop_ev, prop_ice), names_to = "tecnologia", values_to = "share") %>%
mutate(tecnologia = if_else(tecnologia == "prop_ev", "Eléctrico (EV)", "Combustión (ICE)")) %>%
ggplot(aes(x = anio, y = share, fill = tecnologia)) +
geom_area(position = "fill") +
geom_text(
data = . %>% filter(anio %% 2 != 0), # Etiquetar años impares para no saturar
aes(label = scales::percent(share, accuracy = 1)),
position = position_stack(vjust = 0.5),
color = "white", fontface = "bold", size = 3
) +
scale_y_continuous(labels = scales::percent) +
scale_x_continuous(breaks = seq(2025, 2035, 2)) +
scale_fill_manual(values = c("Eléctrico (EV)" = "#00b4d8", "Combustión (ICE)" = "#505050"), name = "Tecnología") +
labs(
title = "Proyección de Adopción EV (Escenario Sostenible)", # Título corregido
subtitle = "Meta de 35% de viajes en auto en vehículos eléctricos para 2035",
x = "Año", y = "% del Total de Viajes en Auto" # Título de eje corregido
) +
theme_uniandes() +
theme(axis.text.x = element_text(angle = 0, hjust = 0.5))
print(g10)

# ==========================================================
# GRÁFICO 11: Evolución de Tasa de Ocupación (Escenario 3)
# ==========================================================
g11 <- occ_traj %>%
ggplot(aes(x = anio, y = occ_obj)) +
geom_line(linewidth = 1.5, color = "#994499") +
geom_point(size = 3, color = "#994499") +
geom_text(aes(label = round(occ_obj, 2)), vjust = -1.2, fontface = "bold", color = "#994499") +
scale_y_continuous(limits = c(1.8, 2.8)) +
scale_x_continuous(breaks = 2025:2035) +
labs(
title = "Proyección de Ocupación Vehicular (Esc. Sostenible)", # Título corregido
subtitle = "Meta de 2.6 personas por vehículo en viajes de carro compartido para 2035",
x = "Año", y = "Personas por Vehículo"
) +
theme_uniandes() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
print(g11)

# =================================================================
# BLOQUE FINAL, COMPLETO Y CORREGIDO: COMPARACIÓN DE ESCENARIOS
# =================================================================
# Cargamos las librerías necesarias para este bloque
library(tidyverse)
library(scales)
library(forcats)
# ---
# PARTE 1: GRÁFICOS DE COMPARACIÓN DE REPARTO MODAL (2035)
# ---
# 1.1 Unificar los datos de reparto modal de los 3 escenarios para 2035
# -----------------------------------------------------------------
# (Este código asume que los objetos scn_uni y proj_uni_sostenible ya existen)
data_micromovilidad_rm <- read_csv("rm_caso_micromovilidad2035.csv") %>%
mutate(
modo_cat = fct_collapse(modo,
"Transp. público" = c("TransMilenio", "SITP", "Bus intermunicipal", "Metro"),
"Vehículo particular" = c("Carro (conductor)", "Carro (pasajero)"),
"Taxi / App" = c("Taxi", "App de transporte"),
"Carro compartido" = "Carro compartido",
"Micromovilidad" = c("Bicicleta", "Patineta eléctrica"),
"Moto" = "Moto",
other_level = "Caminata"
)
) %>%
group_by(modo_cat) %>%
summarise(total_viajes = sum(viajes_imp_2035, na.rm = TRUE), .groups = "drop") %>%
mutate(share = total_viajes / sum(total_viajes), escenario = "Micromovilidad", anio = 2035)
## Rows: 13 Columns: 6
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): modo, categoria
## dbl (4): viajes_imp_2035, rm_imp_2035, viajes_2024, rm_2024
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
comparativa_rm_2035 <- bind_rows(
scn_uni %>% filter(escenario == "base", anio == 2035) %>% mutate(escenario = "Do Nothing"),
proj_uni_sostenible %>% filter(anio == 2035),
data_micromovilidad_rm
) %>%
select(escenario, anio, modo_cat, share) %>%
mutate(escenario = factor(escenario, levels = c("Do Nothing", "Vehicular Sostenible", "Micromovilidad")))
# 1.2 Generar los gráficos de reparto modal
# -----------------------------------------------------------------
# GRÁFICO 1: Barras apiladas por escenario
g_comparativo_apilado <- comparativa_rm_2035 %>%
ggplot(aes(x = escenario, y = share, fill = modo_cat)) +
geom_col(position = "fill", width = 0.7) +
geom_text(
aes(label = ifelse(share >= 0.04, scales::percent(share, accuracy = 1), "")),
position = position_stack(vjust = 0.5), color = "white", size = 3.5, fontface = "bold"
) +
scale_y_continuous(labels = scales::percent_format()) +
scale_fill_manual(values = paleta_modos_agregados, name = "Modo de transporte") +
labs(
title = "Comparación del reparto modal en 2035 por escenario",
subtitle = "El escenario de micromovilidad logra la transformación más significativa",
x = "Escenario Proyectado", y = "Distribución modal"
) +
theme_uniandes() +
theme(axis.text.x = element_text(angle = 0, hjust = 0.5))
print(g_comparativo_apilado)

# GRÁFICO 2: Barras agrupadas por modo
orden_modos <- comparativa_rm_2035 %>% filter(escenario == "Do Nothing") %>% arrange(desc(share)) %>% pull(modo_cat)
paleta_escenarios <- c("Do Nothing" = "#a9a9a9", "Vehicular Sostenible" = "#4682b4", "Micromovilidad" = "#3cb371")
g_comparativo_agrupado <- comparativa_rm_2035 %>%
mutate(modo_cat = factor(modo_cat, levels = orden_modos)) %>%
ggplot(aes(x = modo_cat, y = share, fill = escenario)) +
geom_col(position = position_dodge(width = 0.9), width = 0.8) +
geom_text(
aes(label = scales::percent(share, accuracy = 0.1)),
position = position_dodge(width = 0.9), vjust = -0.5, size = 3.0, fontface = "bold", color = "gray30"
) +
scale_y_continuous(labels = scales::percent_format(), expand = expansion(mult = c(0, 0.1))) +
scale_fill_manual(values = paleta_escenarios, name = "Escenario") +
labs(
title = "Impacto de cada escenario por modo de transporte (2035)",
subtitle = "Comparación directa del reparto modal para cada alternativa",
x = NULL, y = "Participación modal"
) +
theme_uniandes() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
print(g_comparativo_agrupado)

# ---
# PARTE 2: PREPARACIÓN DE DATOS Y GRÁFICOS DE EMISIONES
# ---
calc_emisiones_por_modo <- function(pkm_tbl, occ_vec, prop_ev, fe_tbl) {
factores <- tibble(modo_cat = unique(pkm_tbl$modo_cat)) %>%
rowwise() %>%
mutate(f_gco2_paxkm = factor_paxkm(modo_cat, occ_vec, prop_ev, fe_tbl)) %>%
ungroup()
pkm_tbl %>%
left_join(factores, by = "modo_cat") %>%
mutate(emisiones_tCO2e = (pkm_anuales * f_gco2_paxkm) / 1e6) %>%
group_by(modo_cat) %>%
summarise(emisiones_tCO2e = sum(emisiones_tCO2e, na.rm = TRUE), .groups = "drop")
}
# 2.1 Unificar los datos de EMISIONES de los 3 escenarios (2024 y 2035)
# -----------------------------------------------------------------
# Emisiones 2024 (Línea Base para Do Nothing y Sostenible)
shares_2024_base <- uni_2024 %>% left_join(tibble(rol7 = unique(insumos_2024$rol7)), by = character())
## Warning: Using `by = character()` to perform a cross join was deprecated in dplyr 1.1.0.
## ℹ Please use `cross_join()` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
pkm_2024 <- compute_pkm(shares_2024_base, insumos_2024, pop_base_roles %>% rename(N_rol = n))
emisiones_2024_base <- calc_emisiones_por_modo(pkm_2024, occ_2024, 0.067, fe)
# Emisiones 2035 para los escenarios modelados en R
emisiones_donothing_2035 <- calc_emisiones_por_modo(pkm_DoNothing, occ_2024, prop_ev_2035_DoNothing, fe)
emisiones_sostenible_2035 <- calc_emisiones_por_modo(pkm_Esc3, c(occ_2024, "Carro compartido" = occ_target_carpool_Esc3), prop_ev_2035_Esc3, fe)
# CORRECCIÓN: Importar y procesar los nuevos datos desagregados de micromovilidad
emisiones_micromovilidad_full <- read_csv("emisiones_por_modo_caso_micro.csv") %>%
# Paso 1: Homologar los modos desagregados a las categorías estándar
mutate(
modo_cat = case_when(
modo %in% c("TransMilenio", "SITP", "Bus intermunicipal") ~ "Transp. público",
modo %in% c("Carro (conductor)", "Carro (pasajero)") ~ "Vehículo particular",
modo %in% c("Taxi", "App de transporte") ~ "Taxi / App",
modo == "Carro compartido" ~ "Carro compartido",
modo == "Moto" ~ "Moto",
# Agrupamos los modos activos, cuyas emisiones son 0
categoria_modo == "Modos activos" ~ "Micromovilidad"
)
) %>%
# Paso 2: Agrupar por la nueva categoría y sumar las emisiones
group_by(modo_cat) %>%
summarise(
tCO2_2024 = sum(tCO2_2024, na.rm = TRUE),
tCO2_2035 = sum(tCO2_2035, na.rm = TRUE),
.groups = "drop"
) %>%
# Paso 3: Pivotar los datos a formato largo (tidy)
pivot_longer(
cols = c(tCO2_2024, tCO2_2035),
names_to = "anio",
values_to = "emisiones_tCO2e",
names_pattern = "tCO2_(\\d{4})"
) %>%
mutate(
anio = as.integer(anio),
escenario = "Micromovilidad"
)
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): modo, categoria_modo
## dbl (2): tCO2_2024, tCO2_2035
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# Combinamos todos los datos de emisiones (2024 y 2035) para los 3 escenarios
emisiones_full <- bind_rows(
emisiones_2024_base %>% mutate(anio = 2024, escenario = "Do Nothing"),
emisiones_donothing_2035 %>% mutate(anio = 2035, escenario = "Do Nothing"),
emisiones_2024_base %>% mutate(anio = 2024, escenario = "Vehicular Sostenible"),
emisiones_sostenible_2035 %>% mutate(anio = 2035, escenario = "Vehicular Sostenible"),
emisiones_micromovilidad_full
) %>%
mutate(escenario = factor(escenario, levels = c("Do Nothing", "Vehicular Sostenible", "Micromovilidad")))
# 2.2 Generar los gráficos de comparación de emisiones
# -----------------------------------------------------------------
# GRÁFICO 3: Barras agrupadas de emisiones absolutas en 2035
g_emisiones_comparativo <- emisiones_full %>%
filter(anio == 2035, emisiones_tCO2e > 0) %>%
ggplot(aes(x = fct_reorder(modo_cat, -emisiones_tCO2e), y = emisiones_tCO2e, fill = escenario)) +
geom_col(position = position_dodge(width = 0.9), width = 0.8) +
geom_text(
aes(label = scales::comma(emisiones_tCO2e, accuracy = 1)),
position = position_dodge(width = 0.9), vjust = -0.5, size = 3.0, fontface = "bold", color = "gray30"
) +
scale_y_continuous(labels = scales::comma_format(), expand = expansion(mult = c(0, 0.15))) +
scale_fill_manual(values = paleta_escenarios, name = "Escenario") +
labs(
title = "Comparación de emisiones por modo en cada escenario (2035)",
subtitle = "Ambos escenarios de intervención logran reducir la huella de carbono vehicular",
x = NULL, y = "Emisiones totales (toneladas de CO2e/año)"
) +
theme_uniandes() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
print(g_emisiones_comparativo)

# GRÁFICO 4: Cambio porcentual DENTRO de cada escenario
cambio_emisiones_interno <- emisiones_full %>%
pivot_wider(names_from = anio, values_from = emisiones_tCO2e, names_prefix = "emisiones_") %>%
mutate(cambio_pct = (`emisiones_2035` - `emisiones_2024`) / `emisiones_2024`) %>%
filter(is.finite(cambio_pct)) %>%
mutate(
tipo_cambio = ifelse(cambio_pct < 0, "Reducción", "Aumento"),
modo_cat = fct_reorder(modo_cat, cambio_pct)
)
g_cambio_emisiones <- ggplot(cambio_emisiones_interno, aes(x = modo_cat, y = cambio_pct, color = tipo_cambio)) +
geom_segment(aes(xend = modo_cat, yend = 0), linewidth = 1) +
geom_point(size = 4) +
geom_text(aes(label = scales::percent(cambio_pct, accuracy = 1)),
hjust = ifelse(cambio_emisiones_interno$cambio_pct > 0, -0.3, 1.3),
size = 3.5, fontface = "bold") +
scale_y_continuous(labels = scales::percent_format()) +
scale_color_manual(values = c("Reducción" = "#3cb371", "Aumento" = "#cc6600")) +
coord_flip() +
facet_wrap(~escenario, ncol = 1) +
labs(
title = "Cambio porcentual en emisiones por modo (2024 vs. 2035)",
subtitle = "Comparación de la evolución de emisiones dentro de cada escenario",
x = NULL, y = "Cambio porcentual en emisiones de CO2e"
) +
theme_uniandes() +
theme(legend.position = "none",
panel.grid.major.y = element_blank(),
axis.text.x = element_text(angle = 0, hjust = 0.5))
print(g_cambio_emisiones)
