El presente proyecto tiene como objetivo estimar la evolución del tamaño de mercado de combustibles (en galones americanos de combustible vehícular: gasolinas y diésel) para vehículos en un país, en el contexto de la transición hacia la electromovilidad. La creciente adopción de vehículos eléctricos (VE) representa un cambio estructural que invalida las proyecciones tradicionales basadas únicamente en tendencias históricas. Por lo tanto, el propósito de este modelo es simular dinámicamente el tamaño del mercado de combustibles a lo largo del tiempo, considerando los factores clave que impulsan esta transformación.
Para abordar este problema, se ha empleado un enfoque de Dinámica de Sistemas. Esta metodología es ideal para capturar la complejidad del sistema, incluyendo las acumulaciones (como el Parque vehicular total y eléctrico), los flujos (ventas y retiros de vehículos) y los mecanismos de ajuste que determinan el comportamiento del mercado a largo plazo.
El racional del modelo se basa en la interacción de tres dinámicas principales. Primero, la demanda total de vehículos se proyecta a partir de factores macroeconómicos y demográficos, como el crecimiento de la Población y el PIB per cápita, los cuales impulsan una Tasa de motorización que está limitada por un Techo o nivel de saturación del mercado.
Segundo, se modela la transición de la flota vehicular. Las ventas totales de vehículos nuevos se dividen entre los de combustión y los eléctricos según una curva de adopción tecnológica en forma de “S” (Penetración eléctricos), la cual se calibra con datos del mercado y con la opinión que las compañías manufactureras comunican como año meta para alcanzar el 50% de las ventas en una región. Finalmente, el consumo de combustible por vehículo no es estático; el modelo incorpora una Tasa de mejora en la eficiencia que disminuye el Consumo promedio por vehículo a lo largo del tiempo, hasta alcanzar un Consumo promedio mínimo dictado por los límites tecnológicos.
Al integrar estas dinámicas, el modelo calcula el Parque vehicular de combustión interna remanente en cada año y lo multiplica por el consumo promedio para obtener la variable de resultado: el Consumo de combustibles total. El resultado del modelo nos permite medir la sensilidad de esta variable de resultado a cambios en otras variables como el año en el que las ventas de vehículos eléctricos serán igual a las mitad de las ventas de todos los vehículos.
El modelo puede ser usado tanto por empresas que promueven la electrificación como para empresas que comercializan combustibles. El autor trabaja en una de estas comercializadoras, en donde el modelo se utilizará tanto para establecer estrategias de largo plazo, como insumo para valoración de empresas en procesos de fusiones y adquisiciones en América Latina.
#-----------------------------------------------------------------------
# PASO 1: CONFIGURACIÓN INICIAL Y PARÁMETROS
#-----------------------------------------------------------------------
# Cargar la librería necesaria para la simulación
library(deSolve)
# Definir los parámetros (variables exógenas) del modelo
parameters <- c(
# Parámetros demográficos y económicos
poblacion_inicial = 33731000, # Habitantes en el año inicial
crecimiento_poblacion = 0.01, # Tasa de crecimiento anual de la población (0.01 = 1.0%)
crecimiento_pib_per_capita = 0.035, # Tasa de crecimiento anual del PIB per cápita (0.035 = 3.5%)
# Parámetros de la Tasa de Motorización
tasa_motorizacion_inicial = 248, # Vehículos por mil habitantes en el año inicial
techo_motorizacion = 650, # Límite máximo de vehículos por mil habitantes
elasticidad_pib_motorizacion = 0.997, # Elasticidad del PIB vs. la tasa de motorización
# Parámetros del Parque Vehicular Total
tiempo_ajuste = 1, # Años que tarda el mercado en cerrar la brecha del parque vehicular
# Parámetros de Vida Útil de Vehículos
vida_promedio_combustion = 16, # Años de vida útil de un vehículo de combustión
vida_promedio_electrico = 12, # Años de vida útil de un vehículo eléctrico
# Parámetros de Consumo de Combustible
consumo_promedio_inicial = 248, # Galones por vehículo al año, en el año inicial
consumo_promedio_minimo = 135, # Límite tecnológico inferior de consumo (Galones/vehículo/año)
tasa_mejora = 0.013, # Tasa anual de mejora en la eficiencia del consumo (0.013 = 1.3%)
# Parámetros de Penetración de Vehículos Eléctricos
ano_inicial_simulacion = 2025, # Año en que inicia la simulación
penetracion_ventas_inicial = 0.00027, # Fracción de VEs en las ventas del año inicial (0.03 = 3%)
ano_ventas_inicial = 2023, # Año correspondiente al dato de penetración inicial
ano_cincuenta_penetracion = 2035 # Año objetivo para que el 50% de las ventas sean VEs
)
#-----------------------------------------------------------------------
# PASO 2: CONDICIONES INICIALES (Valores iniciales de los Stocks)
#-----------------------------------------------------------------------
# Definir los valores iniciales para las variables de estado (stocks)
InitialConditions <- c(
parque_vehicular_total = 8372589,
parque_vehicular_electrico = 435
)
#-----------------------------------------------------------------------
# PASO 3: TIEMPO DE SIMULACIÓN
#-----------------------------------------------------------------------
# Definir la secuencia de tiempo para la simulación
times <- seq(0, 40, by = 1) # Simular por 40 años, con pasos de 1 año
#-----------------------------------------------------------------------
# PASO 4: FUNCIÓN PRINCIPAL DEL MODELO
#-----------------------------------------------------------------------
fuel_model <- function(t, state, parameters) {
with(as.list(c(state, parameters)), {
# --- CÁLCULO DE VARIABLES AUXILIARES ---
# 1. Variables dependientes del tiempo
ano_proyeccion <- ano_inicial_simulacion + t
poblacion <- poblacion_inicial * (1 + crecimiento_poblacion)^t
tasa_de_motorizacion <- min(techo_motorizacion, tasa_motorizacion_inicial * (1 + crecimiento_pib_per_capita * elasticidad_pib_motorizacion)^t)
consumo_promedio_por_vehiculo <- consumo_promedio_minimo + (consumo_promedio_inicial - consumo_promedio_minimo) * exp(-tasa_mejora * t)
# 2. Lógica de Penetración de VEs
factor_crecimiento_penetracion <- log((1 - penetracion_ventas_inicial) / penetracion_ventas_inicial) / (ano_cincuenta_penetracion - ano_ventas_inicial)
penetracion_electricos <- 1 / (1 + exp(-factor_crecimiento_penetracion * (ano_proyeccion - ano_cincuenta_penetracion)))
# 3. Lógica del Parque Vehicular
parque_vehicular_objetivo <- poblacion * (tasa_de_motorizacion / 1000)
brecha <- parque_vehicular_objetivo - parque_vehicular_total
# Cálculo para evitar división por cero
if (parque_vehicular_total > 0) {
vida_promedio_parque <- (vida_promedio_electrico * (parque_vehicular_electrico / parque_vehicular_total)) + (vida_promedio_combustion * (1 - (parque_vehicular_electrico / parque_vehicular_total)))
} else {
vida_promedio_parque <- vida_promedio_combustion
}
parque_vehicular_combustion <- parque_vehicular_total - parque_vehicular_electrico
# 4. Variable de resultado final
consumo_de_combustibles <- parque_vehicular_combustion * consumo_promedio_por_vehiculo
# --- CÁLCULO DE VARIABLES DE FLUJO ---
retiros_total <- parque_vehicular_total / vida_promedio_parque
ingresos_total <- max(0, (brecha / tiempo_ajuste) + retiros_total)
retiros_electricos <- parque_vehicular_electrico / vida_promedio_electrico
ingresos_electricos <- ingresos_total * penetracion_electricos
# --- ECUACIONES DIFERENCIALES PARA LOS STOCKS ---
dParque_total <- ingresos_total - retiros_total
dParque_electrico <- ingresos_electricos - retiros_electricos
# --- RETORNO DE VALORES ---
return(list(c(dParque_total, dParque_electrico),
parque_combustion = parque_vehicular_combustion,
consumo_promedio_vehiculo = consumo_promedio_por_vehiculo,
consumo_combustibles = consumo_de_combustibles,
penetracion_ventas_VE = penetracion_electricos,
ingresos_totales_anuales = ingresos_total,
tasa_motorizacion = tasa_de_motorizacion
))
})
}
#-----------------------------------------------------------------------
# PASO 5: EJECUCIÓN Y VISUALIZACIÓN
#-----------------------------------------------------------------------
# Ejecutar la simulación
out <- ode(y = InitialConditions,
times = times,
func = fuel_model,
parms = parameters,
method = "rk4")
# Visualizar los resultados en un gráfico
plot(out)
# Ver los primeros registros de la tabla de resultados
head(out)
## time parque_vehicular_total parque_vehicular_electrico parque_combustion
## [1,] 0 8372589 435.000 8372154
## [2,] 1 8509083 1400.883 8507682
## [3,] 2 8803200 3741.118 8799458
## [4,] 3 9167416 8792.565 9158623
## [5,] 4 9569407 19323.694 9550084
## [6,] 5 9997573 40947.466 9956626
## consumo_promedio_vehiculo consumo_combustibles penetracion_ventas_VE
## [1,] 248.0000 2076294192 0.001061141
## [2,] 246.5405 2097488297 0.002102310
## [3,] 245.0999 2156746084 0.004160797
## [4,] 243.6778 2231753384 0.008218263
## [5,] 242.2742 2313738515 0.016168189
## [6,] 240.8886 2398437883 0.031563718
## ingresos_totales_anuales tasa_motorizacion
## [1,] 515992.6 248.0000
## [2,] 766523.1 256.6540
## [3,] 886428.1 265.6099
## [4,] 958555.7 274.8784
## [5,] 1014062.7 284.4702
## [6,] 1064759.9 294.3968
#-----------------------------------------------------------------------
# PASO 6: VISUALIZACIÓN DE RESULTADOs
#-----------------------------------------------------------------------
# Cargar las librerías
library(ggplot2)
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
# 1. Preparar los datos para la gráfica
df_resultados <- as.data.frame(out)
df_resultados <- df_resultados %>%
mutate(
ano_proyeccion = parameters["ano_inicial_simulacion"] + time,
consumo_millones_galones = consumo_combustibles / 1000000
)
# 2. Encontrar el punto máximo (pico) de consumo
pico_consumo <- df_resultados %>%
filter(consumo_millones_galones == max(consumo_millones_galones))
# 3. Crear la gráfica con ggplot2
ggplot(df_resultados, aes(x = ano_proyeccion, y = consumo_millones_galones)) +
geom_line(color = "#0072B2", size = 1.2) +
annotate("segment",
x = pico_consumo$ano_proyeccion, y = 0,
xend = pico_consumo$ano_proyeccion, yend = pico_consumo$consumo_millones_galones,
linetype = "dashed", color = "grey50") +
annotate("segment",
x = min(df_resultados$ano_proyeccion), y = pico_consumo$consumo_millones_galones,
xend = pico_consumo$ano_proyeccion, yend = pico_consumo$consumo_millones_galones,
linetype = "dashed", color = "grey50") +
# Añadir un punto para resaltar el máximo
geom_point(data = pico_consumo, aes(x = ano_proyeccion, y = consumo_millones_galones),
color = "#D55E00", size = 4, shape = 18) +
# Añadir una etiqueta de texto para el máximo
geom_text(data = pico_consumo,
aes(label = paste0("Pico de Consumo:\n", round(consumo_millones_galones, 1), "M Galones en ", ano_proyeccion)),
hjust = -0.1, vjust = 1.2, color = "#D55E00", fontface = "bold") +
# Definir los títulos y nombres de los ejes
labs(
title = "Proyección del Consumo de Combustible Vehicular",
subtitle = "",
x = "Año",
y = "Consumo (Millones de Galones)"
) +
# Ajustar las escalas y el formato para un look más profesional
scale_y_continuous(labels = function(x) paste0(x, "M")) +
scale_x_continuous(breaks = seq(floor(min(df_resultados$ano_proyeccion)/5)*5, max(df_resultados$ano_proyeccion), by = 5)) +
theme_minimal(base_size = 14) +
theme(
plot.title = element_text(face = "bold", size = 18),
axis.title = element_text(face = "bold"),
panel.grid.minor = element_blank()
)
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
#-----------------------------------------------------------------------
# PASO 7: ANÁLISIS DE ESCENARIOS
#-----------------------------------------------------------------------
# 1. Definir las variables de incertidumbre y sus rangos
rango_ano_penetracion <- seq(from = 2035, to = 2045, by = 1)
rango_consumo_minimo <- seq(from = 120, to = 200, by = 10)
# 2. Crear un data frame con todos los escenarios posibles
# expand.grid() crea una combinación de cada elemento de las listas
escenarios_df <- expand.grid(
# Usamos nombres distintos para las variables de escenario para evitar confusiones
escenario_ano_penetracion = rango_ano_penetracion,
escenario_consumo_minimo = rango_consumo_minimo
)
# Añadir un identificador único para cada corrida (escenario)
escenarios_df$Run.ID <- 1:nrow(escenarios_df)
# 3. Preparar y ejecutar el bucle de simulación
# Crear una lista vacía para almacenar los resultados de cada escenario
resultados_todos_escenarios <- list()
# Bucle 'for' que itera sobre cada fila del data frame de escenarios
for (i in 1:nrow(escenarios_df)) {
# Crear una copia de los parámetros base para esta corrida específica
params_escenario <- parameters
# Sobrescribir los valores de los parámetros con los del escenario actual
params_escenario["ano_cincuenta_penetracion"] <- escenarios_df$escenario_ano_penetracion[i]
params_escenario["consumo_promedio_minimo"] <- escenarios_df$escenario_consumo_minimo[i]
# Ejecutar la simulación con los parámetros de este escenario
out_escenario <- ode(y = InitialConditions,
times = times,
func = fuel_model,
parms = params_escenario,
method = "rk4")
# Convertir el resultado a un data frame y añadir el Run.ID
df_escenario <- as.data.frame(out_escenario)
df_escenario$Run.ID <- escenarios_df$Run.ID[i]
# Guardar el data frame de resultados en la lista
resultados_todos_escenarios <- append(resultados_todos_escenarios, list(df_escenario))
# Imprimir el progreso en la consola (opcional, pero útil)
print(paste("Corriendo escenario:", escenarios_df$Run.ID[i], "de", nrow(escenarios_df)))
}
## [1] "Corriendo escenario: 1 de 99"
## [1] "Corriendo escenario: 2 de 99"
## [1] "Corriendo escenario: 3 de 99"
## [1] "Corriendo escenario: 4 de 99"
## [1] "Corriendo escenario: 5 de 99"
## [1] "Corriendo escenario: 6 de 99"
## [1] "Corriendo escenario: 7 de 99"
## [1] "Corriendo escenario: 8 de 99"
## [1] "Corriendo escenario: 9 de 99"
## [1] "Corriendo escenario: 10 de 99"
## [1] "Corriendo escenario: 11 de 99"
## [1] "Corriendo escenario: 12 de 99"
## [1] "Corriendo escenario: 13 de 99"
## [1] "Corriendo escenario: 14 de 99"
## [1] "Corriendo escenario: 15 de 99"
## [1] "Corriendo escenario: 16 de 99"
## [1] "Corriendo escenario: 17 de 99"
## [1] "Corriendo escenario: 18 de 99"
## [1] "Corriendo escenario: 19 de 99"
## [1] "Corriendo escenario: 20 de 99"
## [1] "Corriendo escenario: 21 de 99"
## [1] "Corriendo escenario: 22 de 99"
## [1] "Corriendo escenario: 23 de 99"
## [1] "Corriendo escenario: 24 de 99"
## [1] "Corriendo escenario: 25 de 99"
## [1] "Corriendo escenario: 26 de 99"
## [1] "Corriendo escenario: 27 de 99"
## [1] "Corriendo escenario: 28 de 99"
## [1] "Corriendo escenario: 29 de 99"
## [1] "Corriendo escenario: 30 de 99"
## [1] "Corriendo escenario: 31 de 99"
## [1] "Corriendo escenario: 32 de 99"
## [1] "Corriendo escenario: 33 de 99"
## [1] "Corriendo escenario: 34 de 99"
## [1] "Corriendo escenario: 35 de 99"
## [1] "Corriendo escenario: 36 de 99"
## [1] "Corriendo escenario: 37 de 99"
## [1] "Corriendo escenario: 38 de 99"
## [1] "Corriendo escenario: 39 de 99"
## [1] "Corriendo escenario: 40 de 99"
## [1] "Corriendo escenario: 41 de 99"
## [1] "Corriendo escenario: 42 de 99"
## [1] "Corriendo escenario: 43 de 99"
## [1] "Corriendo escenario: 44 de 99"
## [1] "Corriendo escenario: 45 de 99"
## [1] "Corriendo escenario: 46 de 99"
## [1] "Corriendo escenario: 47 de 99"
## [1] "Corriendo escenario: 48 de 99"
## [1] "Corriendo escenario: 49 de 99"
## [1] "Corriendo escenario: 50 de 99"
## [1] "Corriendo escenario: 51 de 99"
## [1] "Corriendo escenario: 52 de 99"
## [1] "Corriendo escenario: 53 de 99"
## [1] "Corriendo escenario: 54 de 99"
## [1] "Corriendo escenario: 55 de 99"
## [1] "Corriendo escenario: 56 de 99"
## [1] "Corriendo escenario: 57 de 99"
## [1] "Corriendo escenario: 58 de 99"
## [1] "Corriendo escenario: 59 de 99"
## [1] "Corriendo escenario: 60 de 99"
## [1] "Corriendo escenario: 61 de 99"
## [1] "Corriendo escenario: 62 de 99"
## [1] "Corriendo escenario: 63 de 99"
## [1] "Corriendo escenario: 64 de 99"
## [1] "Corriendo escenario: 65 de 99"
## [1] "Corriendo escenario: 66 de 99"
## [1] "Corriendo escenario: 67 de 99"
## [1] "Corriendo escenario: 68 de 99"
## [1] "Corriendo escenario: 69 de 99"
## [1] "Corriendo escenario: 70 de 99"
## [1] "Corriendo escenario: 71 de 99"
## [1] "Corriendo escenario: 72 de 99"
## [1] "Corriendo escenario: 73 de 99"
## [1] "Corriendo escenario: 74 de 99"
## [1] "Corriendo escenario: 75 de 99"
## [1] "Corriendo escenario: 76 de 99"
## [1] "Corriendo escenario: 77 de 99"
## [1] "Corriendo escenario: 78 de 99"
## [1] "Corriendo escenario: 79 de 99"
## [1] "Corriendo escenario: 80 de 99"
## [1] "Corriendo escenario: 81 de 99"
## [1] "Corriendo escenario: 82 de 99"
## [1] "Corriendo escenario: 83 de 99"
## [1] "Corriendo escenario: 84 de 99"
## [1] "Corriendo escenario: 85 de 99"
## [1] "Corriendo escenario: 86 de 99"
## [1] "Corriendo escenario: 87 de 99"
## [1] "Corriendo escenario: 88 de 99"
## [1] "Corriendo escenario: 89 de 99"
## [1] "Corriendo escenario: 90 de 99"
## [1] "Corriendo escenario: 91 de 99"
## [1] "Corriendo escenario: 92 de 99"
## [1] "Corriendo escenario: 93 de 99"
## [1] "Corriendo escenario: 94 de 99"
## [1] "Corriendo escenario: 95 de 99"
## [1] "Corriendo escenario: 96 de 99"
## [1] "Corriendo escenario: 97 de 99"
## [1] "Corriendo escenario: 98 de 99"
## [1] "Corriendo escenario: 99 de 99"
# 4. Consolidar y graficar los resultados
# Unir todos los data frames de la lista en uno solo
df_final_resultados <- do.call("rbind", resultados_todos_escenarios)
# Unir la tabla de resultados con la tabla de escenarios usando el Run.ID
# Esto añade las columnas con los valores de los parámetros a cada resultado
df_final_resultados <- merge(df_final_resultados, escenarios_df, by = "Run.ID")
# 5. Graficar los resultados de todos los escenarios
ggplot(df_final_resultados, aes(x = time, y = consumo_combustibles / 1000000, group = Run.ID, color = escenario_ano_penetracion)) +
geom_line(alpha = 0.5) + # Usamos alpha para la transparencia si hay muchas líneas
scale_color_gradient(low = "blue", high = "red") +
labs(
title = "Análisis de Escenarios del Consumo de Combustibles",
subtitle = "Impacto del año de penetración del 50% de VEs y del consumo mínimo",
x = "Años desde el inicio de la simulación",
y = "Consumo (Millones de Galones)",
color = "Año 50% Penetración"
) +
theme_minimal()
Con las variables definidas inicialmente podemos esperar un pico de consumo de combustibles en el año 2034. En ese año se estima un tamaño de mercado de 2.6 mil millones de galones de combustible vehícular al año. A partir de ese momento se observa una reduccion en el modelo influenciada tanto por una mayor penetración de vehículos eléctricos como por menor consumo promedio por vehículo de combustión interna. Podemos observar que la relación entre el año
En la gráfica de escenarios podemos observar que si el año en donde se alcanza el 50% de las ventas de eléctricos se retrasa en un año, el pico en la demanda se retrasa también durante un año. Esta es una variable a analizar por las partes interesadas, ya sean estos participantes en la comercialización de combustibles y vehículos de combustión o participantes en el ecosistema de vehículos eléctricos e infraestructura de recarga.
Para el análisis fueron utilizadas variables disponibles ya sea en cifras públicas (gobiernos), información privada (Statista) y algunas entrevistas a personas en la industria. El análisis podría mejorarse si se incluyeran algunos impactos de otras variables como la infraestructura de recarga en el país, antigüedad del parque vehícular por cohorte, tipo de vehículo (pesados, livianos, motocicletas) y mejor calidad de información en variables como el consumo promedio mínimo de combustible por vehículo a largo plazo (variable sensibilizada)
Políticas que tienen impacto: 1.- Incentivos al ecosistema de vehículos eléctricos. 2.- Incentivos a vehículos de bajo consumo de combustible 3.- Incentivos a medidas de “fuel efficiency” como mejores aditivos en combustibles