Análisis de costos de contratos públicos en base a factores asociados al valor de contratación por partido político

Proyecto integrador con datos de Mexicanos Contra la Corrupción y la Impunidad (MCCI)

Autores/as

Alejandro Flores Hernandez

Emilio Lopez Juarez

Ruben Arturo Valerio Garcia

Salvador Gutierrez

Profesores: Jorge Juvenal Campos y Jose Manuel Toral Cruz.

Fecha de publicación

12 de junio de 2026

1 Resumen ejecutivo

Este proyecto analiza informacion de contratos publicos provenientes de MCCI con el objetivo de identificar variables asociadas al monto de contratacion. El análisis se enfoca en contratos vinculados con partidos politicos, proveedores, tipos de contrato, duracion y años de firma. La pregunta central es si estas variables ayudan a explicar o predecir el valor esperado de un contrato.

El flujo de trabajo incluyo carga de datos, limpieza, creacion de variables, analisis exploratorio, visualizaciones y comparacion de cuatro enfoques de modelado, regresion lineal OLS, Ridge Regression, Lasso Regression y XGBoost. Los resultados disponibles muestran que los modelos regularizados Ridge y Lasso mejoran frente a una regresion lineal tradicional, aunque su capacidad explicativa en pesos sigue siendo limitada. Esto sugiere que el monto de los contratos depende de factores adicionales que no estan completamente capturados en la base.

El modelo Lasso obtuvo el mejor resultado disponible entre los modelos ejecutados, con un RMSE en pesos de $2,964,510, MAE de $633,907 y R2 en pesos de 0.0517. El modelo Ridge obtuvo resultados muy similares. XGBoost aparece como una alternativa mas flexible para relaciones no lineales, pero sus metricas quedan como pendiente porque el script disponible requiere corregir un error de sintaxis antes de ejecutarse.

La conclusion principal es que el analisis permite detectar patrones relevantes y construir una base tecnica para un sistema de alertas, pero no permite afirmar irregularidades ni causalidad. Los hallazgos deben interpretarse como evidencia exploratoria para priorizar revisiones, no como acusaciones.

2 Contexto del problema

La contratacion publica es un espacio central para estudiar transparencia, rendicion de cuentas y uso de recursos publicos. A traves de los contratos es posible observar que proveedores concentran mayor participacion, que partidos aparecen con determinados patrones de gasto, que tipos de contrato se repiten y como evolucionan los montos a lo largo del tiempo.

Sin embargo, el analisis requiere cautela. Un contrato de monto alto, una concentracion de proveedores o una diferencia entre partidos no implica por si misma una irregularidad. Los datos permiten identificar asociaciones y puntos que merecen revision, pero no sustituyen auditorias, expedientes administrativos ni evidencia legal.

El problema especifico de este proyecto consiste en analizar si variables como tipo de contrato, proveedor, duracion y anio ayudan a explicar el monto de un contrato. Este enfoque se alinea con el Canvas LM del proyecto, donde la tarea de prediccion se define como estimar el costo esperado de un contrato individual emitido por un partido politico.

3 Propuesta de valor

La propuesta de valor consiste en transformar una base de contratos en informacion util para analistas, periodistas, organizaciones civiles y ciudadania interesada en transparencia. El proyecto busca apoyar la deteccion temprana de contratos cuyo costo real se aleje de un valor esperado estimado por modelos estadisticos.

Con base en el Canvas LM, el modelo podría integrarse en un tablero de auditoria ciudadana. En ese escenario, el usuario cargaría datos de contratos, observaría estimaciones del modelo y compararía los monto reales contra valores esperados. Los casos con desviaciones mayores se priorizarían para investigación documental o auditoria.

El valor no esta en afirmar automáticamente que existe corrupción, sino en ordenar información dispersa y producir alertas razonables para enfocar mejor los recursos de revisión.

4 Pregunta de investigación

Pregunta principal:
Que variables de los contratos públicos se asocian con un mayor valor de contratación y que modelo permite explicar o predecir mejor esta diferencia?

Preguntas secundarias:

  • Que proveedores concentran mayor numero de contratos?
  • Como se distribuyen los montos de contratación?
  • Que diferencias se observan por tipo de contrato?
  • Que papel tienen la duración del contrato y el año?
  • Ridge y Lasso mejoran frente a una regresión lineal tradicional?
  • XGBoost podría capturar patrones no lineales que los modelos lineales no capturan?

5 Datos

La fuente principal de datos es Mexicanos Contra la Corrupción y la Impunidad (MCCI). El proyecto contiene dos archivos principales:

  • datos/contratos.csv
  • datos/contratos_montos.csv

Para el modelado se utilizo principalmente contratos_montos.csv, porque contiene la variable partido y la columna costo, usada como monto del contrato.

Código
datos_raw <- readr::read_csv(
  datos_path,
  show_col_types = FALSE,
  locale = readr::locale(encoding = "UTF-8")
)

tibble(
  archivo = "contratos_montos.csv",
  filas = nrow(datos_raw),
  columnas = ncol(datos_raw),
  partidos = n_distinct(datos_raw$partido, na.rm = TRUE),
  tipos_contrato = n_distinct(datos_raw$tipo_contrato, na.rm = TRUE),
  anio_min = min(datos_raw$ano, na.rm = TRUE),
  anio_max = max(datos_raw$ano, na.rm = TRUE),
  costo_min = min(datos_raw$costo, na.rm = TRUE),
  costo_max = max(datos_raw$costo, na.rm = TRUE)
) |>
  kable(caption = "Resumen general de la base de datos utilizada")
Resumen general de la base de datos utilizada
archivo filas columnas partidos tipos_contrato anio_min anio_max costo_min costo_max
contratos_montos.csv 15118 30 7 3 2019 2025 1 262275000

Los datos disponibles en el archivo contratos_montos.csv contienen 15,118 filas y 30 columnas. En la preparación usada por los scripts del proyecto quedaron 15,114 contratos limpios, después de filtrar registros sin monto, año, tipo de contrato, proveedor o duración.

Variables principales:

  • costo: monto del contrato.
  • partido: partido político asociado al archivo.
  • tipo_contrato: clasificación del contrato.
  • tipo_persona: persona física o moral.
  • moral_razon, moral_razon_homo, nom_fisica, nom_fisica_homo: campos usados para construir el proveedor.
  • fecha_firma, fecha_inicio_vigencia, fecha_fin_vigencia: fechas usadas para crear duración.
  • ano: año del contrato.

6 Limpieza y preparación de datos

La limpieza se realizo en los scripts del proyecto sin modificar los archivos originales dentro de datos/. El flujo seguido por los modelos fue:

  1. Cargar contratos_montos.csv.
  2. Convertir fechas con lubridate::ymd().
  3. Crear monto_contrato = costo.
  4. Calcular duracion_dias como diferencia entre fecha de fin e inicio de vigencia.
  5. Completar ano con el año de fecha_firma cuando fuera necesario.
  6. Construir una variable de proveedor unificada usando razón social homologada o nombre de persona física.
  7. Limpiar tipo_contrato y reemplazar vacíos por “Sin especificar”.
  8. Crear log_monto = log1p(monto_contrato) para reducir simetría.
  9. Filtrar registros incompletos para modelado.
Código
datos <- datos_raw |>
  mutate(
    monto_contrato = costo,
    fecha_firma = lubridate::ymd(fecha_firma),
    fecha_inicio_vigencia = lubridate::ymd(fecha_inicio_vigencia),
    fecha_fin_vigencia = lubridate::ymd(fecha_fin_vigencia),
    duracion_dias = as.numeric(fecha_fin_vigencia - fecha_inicio_vigencia),
    ano = coalesce(as.integer(ano), lubridate::year(fecha_firma)),
    proveedor = case_when(
      tipo_persona == "Moral" & !is.na(moral_razon_homo) ~ moral_razon_homo,
      tipo_persona == "Moral" & !is.na(moral_razon) ~ moral_razon,
      str_detect(tipo_persona, "F.sica") & !is.na(nom_fisica_homo) ~ nom_fisica_homo,
      str_detect(tipo_persona, "F.sica") & !is.na(nom_fisica) ~ nom_fisica,
      TRUE ~ "Proveedor no identificado"
    ),
    tipo_contrato = str_trim(tipo_contrato),
    tipo_contrato = if_else(tipo_contrato == "" | is.na(tipo_contrato), "Sin especificar",
    tipo_contrato),
    duracion_dias = pmax(duracion_dias, 0),
    log_monto = log1p(monto_contrato)
  ) |>
  filter(
    !is.na(monto_contrato),
    monto_contrato >= 0,
    !is.na(ano),
    !is.na(tipo_contrato),
    !is.na(proveedor),
    !is.na(duracion_dias)
  )

datos |>
  summarise(
    contratos_limpios = n(),
    monto_min = min(monto_contrato),
    monto_mediana = median(monto_contrato),
    monto_promedio = mean(monto_contrato),
    monto_max = max(monto_contrato),
    duracion_mediana = median(duracion_dias),
    periodo = paste(min(ano), max(ano), sep = " - ")
  ) |>
  kable(digits = 2, caption = "Resumen de datos despues de limpieza")
Resumen de datos despues de limpieza
contratos_limpios monto_min monto_mediana monto_promedio monto_max duracion_mediana periodo
15114 1 114358.6 730383.4 262275000 59 2019 - 2025

7 Análisis exploratorio

El análisis exploratorio muestra una distribución de montos altamente asimétrica. La mediana del monto es mucho menor que el promedio, lo que indica presencia de contratos con valores extremadamente altos. Por esta razón, los modelos utilizan log_monto como variable objetivo.

Código
datos |>
  count(tipo_contrato, sort = TRUE) |>
  kable(caption = "Numero de contratos por tipo de contrato")
Numero de contratos por tipo de contrato
tipo_contrato n
Prestación 10543
Adquisición 3798
Arrendamiento 749
Sin especificar 24
Código
proveedores_resumen <- datos |>
  group_by(proveedor) |>
  summarise(
    n_contratos = n(),
    monto_total = sum(monto_contrato, na.rm = TRUE),
    .groups = "drop"
  ) |>
  arrange(desc(n_contratos))

proveedores_resumen |>
  slice_head(n = 10) |>
  mutate(monto_total = dollar(monto_total, prefix = "$", big.mark = ",")) |>
  kable(caption = "Top 10 proveedores por numero de contratos")
Top 10 proveedores por numero de contratos
proveedor n_contratos monto_total
MANUEL EDUARDO AVILA VEGA 468 $11,929,104
LA COVACHA GABINETE DE COMUNICACION SA DE CV 254 $75,796,804
JCDECAUX OUT OF HOME MEXICO SA DE CV 219 $12,085,272
ERIKA SANTILLAN SANCHEZ 210 $23,220,297
INDATCOM SA DE CV 210 $157,871,329
ELEMENT MEDIA SA DE CV 169 $92,604,449
FUNDACION RAFAEL PRECIADO HERNANDEZ 169 $101,015,039
IMPACTOS FRECUENCIA Y COBERTURA EN MEDIOS SA DE CV 156 $41,700,693
GUILLERMO DIAZ HERNANDEZ 151 $2,350,157
1984 COMUNICACION ESTRATEGICA SA DE CV 149 $21,295,880

7.1 Visualizaciones exploratorias

Código
ggplot(datos, aes(x = log_monto)) +
  # Barras continuas sin bordes blancos pesados y un tono sutil
  geom_histogram(bins = 50, fill = "#4A6FA5", color = NA, alpha = 0.85) +
  labs(
    title = "Distribución del logaritmo del monto de contratos",
    subtitle = "Transformación log1p para reducir el efecto de valores extremos",
    x = "log1p(monto)",
    y = "Frecuencia",
    caption = "Fuente: elaboración propia con datos de MCCI"
  ) +
  # Estilo minimalista puro
  theme_minimal(base_size = 11) +
  theme(
    panel.grid.minor = element_blank(),
    panel.grid.major.x = element_blank(), # Tufte: No se necesitan cuadrículas verticales en histogramas
    panel.grid.major.y = element_line(color = "#eaeaea", linewidth = 0.5),
    plot.title = element_text(face = "bold", size = 13, hjust = 0),
    plot.subtitle = element_text(face = "plain", size = 10, color = "gray30"),
    axis.title = element_text(face = "italic"),
    plot.caption = element_text(hjust = 0, face = "italic", size = 8, color = "gray40"),
    axis.line.x = element_line(color = "black", linewidth = 0.5)
  )

La distribución confirma que la mayoría de contratos se concentra en montos relativamente bajos, mientras que existen contratos atípicos de valor muy alto. Esto justifica usar transformaciones logarítmicas y comparar modelos con distinto nivel de flexibilidad.

Código
ggplot(datos, aes(x = log_monto)) +
  # Barras continuas sin bordes blancos pesados y un tono sutil
  geom_histogram(bins = 50, fill = "#4A6FA5", color = NA, alpha = 0.85) +
  labs(
    title = "Distribución del logaritmo del monto de contratos",
    subtitle = "Transformación log1p para reducir el efecto de valores extremos",
    x = "log1p(monto)",
    y = "Frecuencia",
    caption = "Fuente: elaboración propia con datos de MCCI"
  ) +
  # Estilo minimalista puro
  theme_minimal(base_size = 11) +
  theme(
    panel.grid.minor = element_blank(),
    panel.grid.major.x = element_blank(), # Tufte: No se necesitan cuadrículas verticales en histogramas
    panel.grid.major.y = element_line(color = "#eaeaea", linewidth = 0.5),
    plot.title = element_text(face = "bold", size = 13, hjust = 0),
    plot.subtitle = element_text(face = "plain", size = 10, color = "gray30"),
    axis.title = element_text(face = "italic"),
    plot.caption = element_text(hjust = 0, face = "italic", size = 8, color = "gray40"),
    axis.line.x = element_line(color = "black", linewidth = 0.5)
  )

7.2 Gráficas disponibles en el proyecto

Ademas de las visualizaciones generadas en este reporte, la carpeta del proyecto contiene gráficas exportadas por modelo. A continuación se incluyen algunas figuras representativas cuando los archivos existen en la ruta local.

Placeholder: no se encontraron graficas exportadas en las rutas esperadas.
Código
monto_anio <- datos |>
  group_by(ano) |>
  summarise(monto_total = sum(monto_contrato, na.rm = TRUE), .groups = "drop")

ggplot(monto_anio, aes(x = ano, y = monto_total)) +
  # Línea y puntos finos en un tono café elegante
  geom_line(color = "#8b4513", linewidth = 0.8) +
  geom_point(color = "#8b4513", size = 1.8) +
  scale_y_continuous(
    labels = scales::label_dollar(scale = 1e-9, suffix = " MMM"),
    breaks = seq(0, max(monto_anio$monto_total), by = 0.5e9)
  ) +
  scale_x_continuous(breaks = seq(min(monto_anio$ano), max(monto_anio$ano), by = 1)) +
  labs(
    title = "Monto total contratado por año",
    x = "Año",
    y = "Monto total",
    caption = "Fuente: elaboración propia con datos de MCCI"
  ) +

  theme_minimal(base_size = 11) +
  theme(
    panel.grid.minor = element_blank(),
    panel.grid.major.x = element_blank(), # Remueve líneas verticales
    panel.grid.major.y = element_line(color = "#f0f0f0", linewidth = 0.5), # Líneas horizontales muy tenues
    plot.title = element_text(face = "bold", size = 13, hjust = 0),
    axis.title = element_text(face = "italic"),
    plot.caption = element_text(hjust = 0, face = "italic", size = 8, color = "gray30"),
    axis.line = element_line(color = "black", linewidth = 0.5)
  )

8 Modelos utilizados

La variable objetivo de los modelos de regresión fue log_monto = log1p(monto_contrato). Esta transformación ayuda a reducir el efecto de valores extremos y permite comparar modelos de manera mas estable.

Las variables predicadoras principales fueron:

  • tipo_contrato
  • tamano o clasificación del proveedor por monto total contratado
  • duracion_dias
  • ano
  • proveedor en el caso de la regresión lineal OLS

Los datos se dividieron en entrenamiento y prueba con una proporción de 75% y 25%, respectivamente. En los rescripto ejecutados quedaron 11,334 contratos para entrenamiento y 3,780 para prueba.

8.1 Modelo 1: Regresión lineal OLS

La regresión lineal OLS busca explicar el monto del contrato mediante una relación lineal entre variables predicadoras y log_monto. Su principal ventaja es la interpretable, pero es sensible a valores extremos, alta cardinal de proveedores y relaciones no lineales.

Rescripto fuente: regresion_lineal_contratos.R

Fragmento representativo del código usado:

receta_ols <- recipe(
  log_monto ~ tipo_contrato + proveedor + duracion_dias + ano,
  data = train_datos
) |>
  step_impute_median(duracion_dias) |>
  step_impute_mode(tipo_contrato, proveedor) |>
  step_other(proveedor, threshold = 0.01, other = "Otro proveedor") |>
  step_other(tipo_contrato, threshold = 0.01, other = "Otro tipo") |>
  step_dummy(all_nominal_predictors(), one_hot = TRUE) |>
  step_normalize(all_numeric_predictors())

modelo_ols <- linear_reg() |>
  set_engine("lm") |>
  set_mode("regression")

flujo_ols <- workflow() |>
  add_recipe(receta_ols) |>
  add_model(modelo_ols)

Resultados disponibles:

  • RMSE en pesos: $3,001,916
  • MAE en pesos: $651,347
  • R2 en pesos: 0.00795

Interpretación: el modelo lineal tradicional tiene baja capacidad explicativa en pesos. Esto indica que las variables incluidas explican solo una parte pequen de la variación real del monto contractual. Aun así, sirve como linea base para comparar contra modelos regularizados.

8.2 Modelo 2: Ridge Regression

Ridge Regression usa nacionalización L2 para reducir la magnitud de los coeficientes y estabilizar el modelo cuando existen predicadores cor relacionados o categorías múltiples. En este proyecto se utilizo glmnet con mixture = 0.

Script fuente: regresion_ridge_proveedores.R

Fragmento representativo del código usado:

receta_ridge <- recipe(
  log_monto ~ tipo_contrato + tamano + duracion_dias + ano,
  data = train_datos
) |>
  step_impute_median(duracion_dias) |>
  step_impute_mode(tipo_contrato, tamano) |>
  step_zv(all_predictors()) |>
  step_dummy(all_nominal_predictors(), one_hot = TRUE) |>
  step_normalize(all_numeric_predictors())

modelo_ridge <- linear_reg(
  penalty = tune(),
  mixture = 0
) |>
  set_engine("glmnet") |>
  set_mode("regression")

flujo_ridge <- workflow() |>
  add_recipe(receta_ridge) |>
  add_model(modelo_ridge)

Resultados disponibles:

  • Mejor nacionalización: 0.0001
  • RMSE log: 1.81
  • MAE log: 1.41
  • R2 log: 0.234
  • RMSE en pesos: $2,966,351
  • MAE en pesos: $634,035
  • R2 en pesos: 0.0514

Interpretación: Ridge mejora frente al predictor simple por tamaño de proveedor y frente al modelo OLS en RMSE y R2 en pesos. La variable tamano_Grande aparece asociada con aumentos en el monto esperado, mientras que tamano_Pequeno se asocia con reducciones.

8.3 Modelo 3: Lasso Regression

Lasso Regression usa penalización L1 y puede eliminar predictores al llevar algunos coeficientes exactamente a cero. En este proyecto se utilizo glmnet con mixture = 1.

Script fuente: regresion_lasso_proveedores.R

Fragmento representativo del código usado:

receta_lasso <- recipe(
  log_monto ~ tipo_contrato + tamano + duracion_dias + ano,
  data = train_datos
) |>
  step_impute_median(duracion_dias) |>
  step_impute_mode(tipo_contrato, tamano) |>
  step_zv(all_predictors()) |>
  step_dummy(all_nominal_predictors(), one_hot = TRUE) |>
  step_normalize(all_numeric_predictors())

modelo_lasso <- linear_reg(
  penalty = tune(),
  mixture = 1
) |>
  set_engine("glmnet") |>
  set_mode("regression")

flujo_lasso <- workflow() |>
  add_recipe(receta_lasso) |>
  add_model(modelo_lasso)

Resultados disponibles:

  • Mejor penalización: 0.0001
  • RMSE log: 1.81
  • MAE log: 1.41
  • R2 log: 0.234
  • RMSE en pesos: $2,964,510
  • MAE en pesos: $633,907
  • R2 en pesos: 0.0517
  • Predictores eliminados por Lasso: 2

Interpretación: Lasso obtuvo el mejor desempeño disponible por una diferencia pequeña frente a Ridge. Su ventaja adicional es la seleccion de variables, ya que elimino dos predictores con aporte bajo. Esto facilita una lectura mas compacta del modelo.

8.4 Modelo 4: XGBoost

XGBoost es un modelo de boosting basado en arboles que puede capturar relaciones no lineales e interacciones entre variables. En el proyecto existe el script xgboost_contratos1.R, que define tuning de hiperparámetros, validación cruzada, predicciones, importancia de variables y análisis de residuos.

Fragmento representativo del código usado:

receta_xgb <- recipe(
  log_monto ~ tipo_contrato + tamano + duracion_dias + ano,
  data = train_datos
) |>
  step_impute_median(duracion_dias) |>
  step_impute_mode(tipo_contrato, tamano) |>
  step_zv(all_predictors()) |>
  step_dummy(all_nominal_predictors(), one_hot = TRUE) |>
  step_normalize(all_numeric_predictors())

modelo_xgb <- boost_tree(
  trees = tune(),
  tree_depth = tune(),
  learn_rate = tune(),
  min_n = tune(),
  loss_reduction = tune(),
  sample_size = tune(),
  mtry = tune()
) |>
  set_engine("xgboost") |>
  set_mode("regression")

flujo_xgb <- workflow() |>
  add_recipe(receta_xgb) |>
  add_model(modelo_xgb)

Estado de resultados: pendiente de ejecucion final. El script disponible contiene un error de sintaxis en la creacion de ano:

ano = coalesce(as.integer(ano), lubridate::year(fecha_firma)),,

Debe corregirse a:

ano = coalesce(as.integer(ano), lubridate::year(fecha_firma)),

Por esta razon, no se reportan metricas numericas de XGBoost en este documento. Placeholder claro:

  • RMSE log: PENDIENTE
  • MAE log: PENDIENTE
  • R2 log: PENDIENTE
  • RMSE en pesos: PENDIENTE
  • MAE en pesos: PENDIENTE
  • R2 en pesos: PENDIENTE

9 Explicacion del codigo

9.1 Bloque 1: Carga de librerias

Los scripts cargan tidyverse para manipulacion y visualizacion de datos, tidymodels para el flujo de modelado, glmnet para Ridge y Lasso, y en el caso de XGBoost se prepara un modelo con boost_tree().

library(tidyverse)
library(tidymodels)
library(scales)

tidymodels_prefer()
set.seed(20260528)
theme_set(theme_minimal(base_size = 12))

9.2 Bloque 2: Carga de datos

Los datos se cargan desde datos/contratos_montos.csv usando readr::read_csv() con codificacion UTF-8. Despues se revisan filas, columnas y tipos de variables.

datos_raw <- readr::read_csv(
  "datos/contratos_montos.csv",
  show_col_types = FALSE,
  locale = readr::locale(encoding = "UTF-8")
)

cat("Filas originales:", nrow(datos_raw), "\n")
cat("Columnas originales:", ncol(datos_raw), "\n")

9.3 Bloque 3: Limpieza

La limpieza convierte fechas, calcula duracion, homologa proveedor, limpia tipo de contrato y filtra registros incompletos. Esta etapa es clave porque los modelos requieren variables numericas limpias y categorias consistentes.

datos <- datos_raw |>
  mutate(
    monto_contrato = costo,
    fecha_firma = lubridate::ymd(fecha_firma),
    fecha_inicio_vigencia = lubridate::ymd(fecha_inicio_vigencia),
    fecha_fin_vigencia = lubridate::ymd(fecha_fin_vigencia),
    duracion_dias = as.numeric(fecha_fin_vigencia - fecha_inicio_vigencia),
    ano = coalesce(as.integer(ano), lubridate::year(fecha_firma)),
    tipo_contrato = str_trim(tipo_contrato),
    tipo_contrato = if_else(tipo_contrato == "" | is.na(tipo_contrato),
                            "Sin especificar", tipo_contrato)
  ) |>
  filter(!is.na(monto_contrato), monto_contrato >= 0)

9.4 Bloque 4: Creacion de variables

Se crean monto_contrato, duracion_dias, proveedor, log_monto y tamano. La variable tamano clasifica proveedores en pequeno, mediano y grande segun monto total contratado.

datos <- datos |>
  mutate(
    proveedor = case_when(
      tipo_persona == "Moral" & !is.na(moral_razon_homo) ~ moral_razon_homo,
      tipo_persona == "Moral" & !is.na(moral_razon) ~ moral_razon,
      str_detect(tipo_persona, "F.sica") & !is.na(nom_fisica_homo) ~ nom_fisica_homo,
      str_detect(tipo_persona, "F.sica") & !is.na(nom_fisica) ~ nom_fisica,
      TRUE ~ "Proveedor no identificado"
    ),
    log_monto = log1p(monto_contrato)
  )

proveedores_tamano <- datos |>
  group_by(proveedor) |>
  summarise(monto_total = sum(monto_contrato, na.rm = TRUE), .groups = "drop") |>
  mutate(
    tamano = case_when(
      monto_total >= quantile(monto_total, 0.90, na.rm = TRUE) ~ "Grande",
      monto_total >= quantile(monto_total, 0.50, na.rm = TRUE) ~ "Mediano",
      TRUE ~ "Pequeno"
    )
  )

9.5 Bloque 5: Visualizaciones

Las visualizaciones exploran distribucion de montos, proveedores con mas contratos, contratos por tipo, monto por anio, metricas de validacion y predicciones frente a valores reales.

ggplot(datos, aes(x = log_monto)) +
  geom_histogram(bins = 50, fill = "steelblue", color = "white") +
  labs(
    title = "Distribucion del logaritmo del monto de contratos",
    x = "log1p(monto)",
    y = "Frecuencia"
  )

ggplot(predicciones, aes(x = monto_real, y = monto_predicho)) +
  geom_point(alpha = 0.3, color = "steelblue") +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "red") +
  scale_x_log10(labels = scales::label_dollar(prefix = "$", big.mark = ",")) +
  scale_y_log10(labels = scales::label_dollar(prefix = "$", big.mark = ","))

9.6 Bloque 6: Preparacion para modelos

Los scripts dividen la base en entrenamiento y prueba, aplican imputacion, codificacion dummy de variables categoricas, normalizacion y filtros de varianza cero.

split_datos <- initial_split(
  datos,
  prop = 0.75,
  strata = log_monto
)

train_datos <- training(split_datos)
test_datos <- testing(split_datos)

receta_modelo <- recipe(
  log_monto ~ tipo_contrato + tamano + duracion_dias + ano,
  data = train_datos
) |>
  step_impute_median(duracion_dias) |>
  step_impute_mode(tipo_contrato, tamano) |>
  step_zv(all_predictors()) |>
  step_dummy(all_nominal_predictors(), one_hot = TRUE) |>
  step_normalize(all_numeric_predictors())

9.7 Bloque 7: Entrenamiento

OLS usa linear_reg() con motor lm. Ridge y Lasso usan linear_reg() con motor glmnet, ajustando la penalizacion mediante validacion cruzada. XGBoost usa boost_tree() con grilla de hiperparametros.

modelo_ols <- linear_reg() |>
  set_engine("lm") |>
  set_mode("regression")

modelo_ridge <- linear_reg(penalty = tune(), mixture = 0) |>
  set_engine("glmnet") |>
  set_mode("regression")

modelo_lasso <- linear_reg(penalty = tune(), mixture = 1) |>
  set_engine("glmnet") |>
  set_mode("regression")

modelo_xgb <- boost_tree(
  trees = tune(),
  tree_depth = tune(),
  learn_rate = tune(),
  min_n = tune(),
  loss_reduction = tune(),
  sample_size = tune(),
  mtry = tune()
) |>
  set_engine("xgboost") |>
  set_mode("regression")

9.8 Bloque 8: Evaluación

Las métricas usadas son RMSE, MAE y R2. RMSE y MAE se interpretan como errores de predicción; R2 mide proporción de variación explicada. Se reportan métricas tanto en escala log como en pesos cuando estuvieron disponibles.

ajuste_final <- last_fit(
  flujo_lasso,
  split_datos,
  metrics = metric_set(rmse, mae, rsq)
)

metricas_test <- collect_metrics(ajuste_final)

predicciones <- collect_predictions(ajuste_final) |>
  mutate(
    monto_predicho = pmax(expm1(.pred), 0),
    monto_real = pmax(expm1(log_monto), 0)
  )

predicciones |>
  summarise(
    rmse_pesos = rmse_vec(truth = monto_real, estimate = monto_predicho),
    mae_pesos = mae_vec(truth = monto_real, estimate = monto_predicho),
    r2_pesos = rsq_vec(truth = monto_real, estimate = monto_predicho)
  )

10 Tabla comparativa de modelos

Comparacion de modelos con resultados disponibles
Modelo Objetivo Variable_objetivo RMSE_pesos MAE_pesos R2_pesos Ventaja Limitacion
Regresion lineal OLS Linea base interpretable log_monto 3,001,916 pesos 651,347 pesos 0.00795 Facil de explicar Baja capacidad explicativa y sensibilidad a valores extremos
Ridge Regression Regularizar coeficientes log_monto 2,966,351 pesos 634,035 pesos 0.0514 Estable ante predictores correlacionados No elimina variables y sigue con R2 bajo en pesos
Lasso Regression Regularizar y seleccionar variables log_monto 2,964,510 pesos 633,907 pesos 0.0517 Mejor resultado disponible y seleccion de variables Puede eliminar variables utiles si la penalizacion no se ajusta bien
XGBoost Capturar relaciones no lineales log_monto PENDIENTE PENDIENTE PENDIENTE Flexible y adecuado para interacciones Script requiere correccion antes de ejecutar

11 Interpretacion de resultados

Los resultados muestran que los modelos regularizados tienen mejor desempeno que la regresion lineal OLS. Lasso y Ridge reducen el RMSE en pesos y elevan el R2 frente a OLS. Sin embargo, el R2 en pesos se mantiene bajo, lo que indica que todavia existe mucha variacion no explicada por las variables disponibles.

La clasificacion del proveedor por tamano aporta informacion relevante. En Ridge y Lasso, tamano_Grande aparece asociado con montos esperados mas altos, mientras que tamano_Pequeno aparece asociado con montos menores. La duracion del contrato tambien aporta senal positiva: contratos mas largos tienden a asociarse con montos mayores.

La diferencia entre Ridge y Lasso es pequena. En este caso, Lasso tiene una ligera ventaja en metricas y ademas elimina dos predictores, por lo que puede considerarse el mejor modelo disponible hasta corregir y ejecutar XGBoost.

12 Conclusiones de los modelos

  1. La regresion lineal OLS funciona como referencia inicial, pero su desempeno es limitado.
  2. Ridge mejora la estabilidad y reduce el error frente a OLS.
  3. Lasso ofrece el mejor resultado disponible y simplifica el modelo mediante seleccion de variables.
  4. XGBoost es prometedor para capturar relaciones no lineales, pero sus metricas no deben reportarse hasta corregir el script.
  5. Ningun modelo disponible permite hacer afirmaciones causales o acusaciones de irregularidad.

13 Discusion etica

Este proyecto analiza datos relacionados con partidos politicos, proveedores y contratos publicos. Por ello, la interpretacion debe ser cuidadosa. Una asociacion estadistica entre una variable y un monto alto no demuestra corrupcion, sobreprecio ni conducta indebida.

El uso responsable del modelo implica:

  • No confundir correlacion con causalidad.
  • No senalar culpables sin evidencia documental o legal.
  • Reconocer que la base puede contener valores faltantes, errores de captura o falta de contexto.
  • Usar las predicciones como alertas para priorizar revision, no como veredictos.
  • Evitar que el modelo reproduzca sesgos contra proveedores o partidos por diferencias historicas en la informacion disponible.

Desde el Canvas LM, el costo de un falso positivo es revisar un contrato que podria estar justificado. El costo de un falso negativo es mayor, porque una posible irregularidad podria pasar desapercibida. Por ello, el sistema debe privilegiar transparencia, trazabilidad y revision humana.

14 Areas de oportunidad

  • Corregir y ejecutar completamente el script de XGBoost.
  • Guardar metricas finales en archivos .csv para evitar depender solo de salida en consola.
  • Incorporar variables contextuales como entidad federativa, objeto del contrato, etapa electoral o inflacion.
  • Mejorar la homologacion de proveedores.
  • Analizar contratos atipicos por partido y proveedor.
  • Desarrollar un tablero interactivo de auditoria ciudadana.
  • Validar hallazgos con fuentes oficiales y expedientes documentales.
  • Explorar modelos de texto si las descripciones de contratos se usan como variables predictoras.

15 Conclusion general

La investigacion muestra que los datos de MCCI permiten construir un analisis tecnico sobre montos de contratos publicos y factores asociados. Los hallazgos principales son:

  1. La base contiene 15,118 registros en contratos_montos.csv, con contratos entre 2019 y 2025.
  2. Despues de limpieza, los scripts modelan 15,114 contratos.
  3. Los montos presentan fuerte asimetria, con mediana de $114,359 y maximo de $262,275,000.
  4. Ridge y Lasso mejoran frente a OLS, pero el poder explicativo en pesos sigue siendo limitado.
  5. Lasso es el mejor modelo disponible hasta corregir y ejecutar XGBoost.

En respuesta a la pregunta de investigacion, variables como tamano de proveedor, tipo de contrato, duracion y anio si se asocian con el valor esperado de contratacion, pero no explican por completo la variacion de los montos. El proyecto aporta una base util para generar alertas y preguntas de investigacion, siempre bajo una interpretacion etica y no acusatoria.

15.1 Fuente de datos

Mexicanos Contra la Corrupcion y la Impunidad (MCCI).

15.2 Archivos principales del proyecto

contratos.csv, contratos_montos.csv, regresion_lineal_contratos.R, regresion_ridge_proveedores.R, regresion_lasso_proveedores.R, xgboost_contratos1.R y CANVAS LM.pdf.

15.3 Disclaimer de uso de IA

Para la elaboracion de este reporte se utilizo inteligencia artificial como apoyo en la organizacion de ideas, redaccion academica, estructuracion del documento Quarto e integracion de resultados disponibles en los scripts del proyecto.