Este documento presenta un flujo de trabajo completo en R para clasificar municipios como fiscalmente estresados o saludables a partir de razones contables derivadas de estados financieros auditados.

Las razones utilizadas como predictores se agrupan en tres dimensiones:

  • Desempeño Fiscal: Esta dimensión evalúa la disciplina y los resultados financieros del municipio durante el año evaluado. Se enfoca en la capacidad del municipio para sustentar sus operaciones con ingresos propios y recurrentes, midiendo si gasta más de lo que recibe. El índice otorga un mayor peso a estos indicadores de desempeño anual en comparación con la posición acumulada.
  • Posición Financiera: Esta categoría examina la condición del municipio a largo plazo.Refleja el estado financiero resultante de muchos años acumulados, incluyendo activos, pasivos y deuda.
  • Demográfico: Provee una visión estructural y dinámica de la población. Incluye métricas de escala (población total, área), densidad, y salud económica (tasa de desempleo). Además, monitorea tendencias críticas como el crecimiento o decrecimiento poblacional para predecir la resiliencia de la base contributiva a largo plazo.

Se comparan tres clasificadores supervisados:

Clasificador Paquete R
K-Vecinos más Cercanos (KNN) class / caret
Árbol de Decisión (CART) rpart
Máquina de Soporte Vectorial (SVM) e1071

1 Carga de Paquetes

pacman::p_load( knitr,kableExtra,readr,naniar,caret,readxl,tidyr,ranger,ggplot2,plotly,dplyr,writexl, plyr, scales,kknn, class, mice,C50)

2 NOTA METODOLÓGICA:

Para optimizar el rendimiento de los modelos de clasificación, el dataset final fue sometido a un proceso de reducción de dimensionalidad :

  1. EXCLUSIÓN DE VARIABLES DE AUDITORÍA: Se removieron los datos crudos de los estados financieros, ya que estos fueron utilizados previamente para el cálculo de los indicadores financieros estandarizados incluidos en el modelo.

  2. EXCLUSIÓN DE IDENTIFICADORES: Se excluyeron del entrenamiento variables de identificación que no aportan poder predictivo al modelo numérico (e.g., government_id, fips_place, mayor_name, mayor_s_annual_salary, number_of_years_in_office, number_of_public_employees). Nota: Las variables ‘government_id’ y ‘year’ se retienen únicamente para la partición, pero se omiten en la data de entrenamiento.

  3. VARIABLES INGENIERADAS :

    • ‘population_growth_or_decline’: Generada para capturar el cambio porcentual anual en la población.
    • ‘densidad_poblacional’: Generada para capturar la concentración demográfica. Dado que el área geográfica de cada municipio permanece constante a lo largo del tiempo, las fluctuaciones en esta variable reflejan estrictamente el cambio poblacional interanual.

3 Datos: Carga y Exploración Inicial

data_org <- read_excel("data_para_mod.xlsx") 
data_org$letter_grade= as.factor(data_org$letter_grade)
data <- data_org %>% 
  select(-financial_health_ranking)
data$unemployment_rate = as.numeric(as.character(data$unemployment_rate))

kable(head(data, 6),
      caption = "Vista previa de los datos (primeras 6 observaciones)") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = FALSE)
Vista previa de los datos (primeras 6 observaciones)
government_id year population population_growth_or_decline area_square_miles Densidad_poblacional unemployment_rate total_unrestricted_net_position net_change_in_fund_balance_total_revenue_gf end_of_year_fund_balance_gf_total_expenditures_gf current_assets_per_capita current_assets_current_liabilities balance_long_term_debt_per_capita debt_service_expenditures_annual_income excess_deficiency_general_fund_revenues change_in_net_assets_revenues_general_fund intergovermental_funds_gf_revenues_gf total_unrestricted_net_position_total_assets letter_grade
91931 2010 45653 NA 55.9402 816.1036 0.200 5675543 0.0422474 0.892855 931.923 9.78056 479.013 0.0519857 0.0610072 0.3463690 0.370149 0.0626635 A
91931 2011 45321 -0.0072722 55.9402 810.1687 0.196 13148839 0.3377420 1.235480 939.732 8.31163 497.833 0.0543683 0.0745081 0.1600940 0.354332 0.1387120 A
91931 2012 44911 -0.0090466 55.9402 802.8395 0.182 12286372 0.0168216 1.176570 1029.590 9.13323 692.402 0.0830993 0.0350066 0.1366800 0.378255 0.1161870 A
91931 2013 44581 -0.0073479 55.9402 796.9403 0.173 11442023 -0.0462591 1.001590 994.507 7.44498 694.830 0.0953267 -0.0230167 0.1094900 0.242542 0.1048250 A
91931 2014 44149 -0.0096902 55.9402 789.2178 0.151 14470207 0.1096830 1.112970 987.333 10.77710 694.449 0.0935573 0.0814349 -0.1140600 0.217737 0.1379800 A
91931 2015 43398 -0.0170106 55.9402 775.7927 0.127 17964371 0.1425570 0.958699 1005.780 20.00470 717.640 0.0724415 -0.0404275 -0.0657328 0.268701 0.1697690 A
# ============================================================
# EXPLORACIÓN INICIAL DE LA ESTRUCTURA
# ============================================================

cat("Dimensiones del dataset:", nrow(data), "filas x", ncol(data), "columnas\n")
## Dimensiones del dataset: 811 filas x 19 columnas
print(table(data_org$letter_grade))
## 
##   A   B   C   D   F 
##  87 153 286 188  97
cat("\nPorcentaje por categoría:\n")
## 
## Porcentaje por categoría:
print(round(prop.table(table(data_org$letter_grade)) * 100, 1))
## 
##    A    B    C    D    F 
## 10.7 18.9 35.3 23.2 12.0

4 Preprocesamiento: Manejo de Valores Faltantes

4.1 Introducción de valores faltantes (demostración)

# Resumen de valores faltantes
# Las observaciones correspondientes al año fiscal 2011 fueron removidas del dataset #Al ser el año base de nuestro horizonte, la variable 'population_growth_or_decline'
#resulta en un valor nulo  para este periodo debido a la falta de datos del 2009 

data <- data %>% filter(!is.na(population_growth_or_decline))

data$unemployment_rate[data$unemployment_rate == "NULL"] <- NA
cat("Conteo de valores faltantes por variable:\n")
## Conteo de valores faltantes por variable:
print(colSums(is.na(data)))
##                                     government_id 
##                                                 0 
##                                              year 
##                                                 0 
##                                        population 
##                                                 0 
##                      population_growth_or_decline 
##                                                 0 
##                                 area_square_miles 
##                                                 0 
##                              Densidad_poblacional 
##                                                 0 
##                                 unemployment_rate 
##                                                72 
##                   total_unrestricted_net_position 
##                                                 0 
##       net_change_in_fund_balance_total_revenue_gf 
##                                                 0 
## end_of_year_fund_balance_gf_total_expenditures_gf 
##                                                 0 
##                         current_assets_per_capita 
##                                                 0 
##                current_assets_current_liabilities 
##                                                 0 
##                 balance_long_term_debt_per_capita 
##                                                 0 
##           debt_service_expenditures_annual_income 
##                                                 0 
##           excess_deficiency_general_fund_revenues 
##                                                 0 
##        change_in_net_assets_revenues_general_fund 
##                                                 0 
##             intergovermental_funds_gf_revenues_gf 
##                                                 0 
##      total_unrestricted_net_position_total_assets 
##                                                 0 
##                                      letter_grade 
##                                                 0
data_for_mice <- data %>%
  select(
    government_id,
    year,
    net_change_in_fund_balance_total_revenue_gf,
    end_of_year_fund_balance_gf_total_expenditures_gf,
    current_assets_current_liabilities,
    debt_service_expenditures_annual_income,
    excess_deficiency_general_fund_revenues,
    change_in_net_assets_revenues_general_fund,
    intergovermental_funds_gf_revenues_gf,
    total_unrestricted_net_position_total_assets,
    
    
    population_growth_or_decline,
    area_square_miles,
    unemployment_rate,
    
    
    balance_long_term_debt_per_capita,
    
  )
# Imputación con Regresión Múltiple
Imput_reg2 <- mice(data_for_mice, method = "norm.boot", print = FALSE)
Complete_reg2 <- mice::complete(Imput_reg2)
# Imputación con MICE
Imput_mice <- mice(data_for_mice, method = "pmm", m = 10, print = FALSE)  
Complete_mice <- mice::complete(Imput_mice)
# Imputación con Random Forest
Imput_rf <- mice(data_for_mice, method = "rf", m = 10, print = FALSE)
Complete_rf <- mice::complete(Imput_rf)
# Verificación:gráfico de densidad

data_long <- bind_rows(
  data %>% select(unemployment_rate) %>% mutate(Origen = "Original"),
  Complete_reg2 %>% select(unemployment_rate) %>% mutate(Origen = "Regresión Múltiple"),
  Complete_mice %>% select(unemployment_rate) %>% mutate(Origen = "MICE"),
  Complete_rf %>% select(unemployment_rate) %>% mutate(Origen = "Random Forest")
) %>%
  pivot_longer(cols = c(unemployment_rate), names_to = "Variable", values_to = "Valor")

p <- ggplot(data_long, aes(x = Valor, color = Origen, fill = Origen, text = paste("Método:", Origen))) +
  geom_density(alpha = 0.2) +
  facet_wrap(~Variable, scales = "free") + 
  labs(title = "Comparación de Imputación Multivariada", x = "Variables", y = "Densidad") +
  theme_minimal()

ggplotly(p, tooltip = c("text", "x", "y"))
# Reincorporar unemployment rate  
data$unemployment_rate= Complete_rf$unemployment_rate

cat("Valores faltantes después de imputación:", sum(is.na(data)), "\n")
## Valores faltantes después de imputación: 0

4.1.1 Justificación de la Selección del Método de Imputación

Todos los métodos de imputación evaluados introducen cierta desviación respecto a la data original. Sin embargo, el método de Random Forest fue el único cuya desviación principal se debió a la sobrestimación de los valores imputados.

Para garantizar una clasificación conservadora y mitigar el riesgo, se seleccionó este método para la variable ‘unemployment_rate’ porque asegura que las tasas de desempleo en los municipios de Puerto Rico no sean subestimadas dado a que es un riesgo mayor para la planificación y la política pública.

5 Preparación para Modelado

5.1 División entrenamiento / prueba

# ============================================================
# PARTICIÓN POR SERIE DE TIEMPO
# ============================================================
complete_dat = read_excel("Municipal_Fiscal_Health_Clean.xlsx")

# 1. Ejecutar la partición utilizando la variable 'year' como criterio de bloque
entrenamiento_0 <- complete_dat %>% filter(year < 2019)
prueba_0        <- complete_dat %>% filter(year == 2020)

# Una vez asegurada la separación estricta de los conjuntos de datos, procedemos
# a remover 'id' y 'year' de las datos finales de entrenamiento 

entrenamiento <- entrenamiento_0 %>% select(-government_id, -year)

# Conjunto de Prueba / Validación (Test)
prueba  <- prueba_0 %>% select(-government_id, -year)


cat("Tamaño del conjunto de entrenamiento:", nrow(entrenamiento), "\n")
## Tamaño del conjunto de entrenamiento: 590
cat("Tamaño del conjunto de prueba:       ", nrow(prueba), "\n\n")
## Tamaño del conjunto de prueba:        72
cat("Distribución en entrenamiento:\n")
## Distribución en entrenamiento:
print(prop.table(table(entrenamiento$letter_grade)))
## 
##         A         B         C         D         F 
## 0.1067797 0.2000000 0.3542373 0.2220339 0.1169492
cat("\nDistribución en prueba:\n")
## 
## Distribución en prueba:
print(prop.table(table(prueba$letter_grade)))
## 
##         A         B         C         D         F 
## 0.1111111 0.1111111 0.2777778 0.3611111 0.1388889

5.2 Estandarización y Normalización

# ============================================================
# ESTANDARIZACIÓN: media 0, desviación estándar 1
# Necesario para KNN y SVM
# Los algoritmos basados en distancias y márgenes (KNN y SVM) son altamente 
# sensibles a las diferencias de escala entre variables. Se aplica la función 
# 'rescale' para normalizar todas las variables numéricas entre 0 y 1.
# Se utiliza la sintaxis '-letter_grade' dentro de 'across()' para EXCLUIR 
# deliberadamente la columna categórica de etiquetas del proceso de escalamiento
# ============================================================

Min_max <- complete_dat %>%
  mutate(across(-letter_grade, rescale))


entrenamiento_min= Min_max %>% filter(year < 2019) 
entrenamiento_min = entrenamiento_min%>% select(-government_id, -year)
prueba_min = Min_max %>% filter(year == 2020) 
prueba_min = prueba_min%>% select(-government_id, -year)


#labels
train_labels = entrenamiento$letter_grade%>% as.factor()
test_labels = prueba$letter_grade%>% as.factor()


data_est <- complete_dat %>%
  mutate(across(-c(letter_grade, year), scale))

data_est <- as.data.frame(lapply(data_est, as.vector))
entrenamiento_z <- data_est %>% filter(year < 2019)
entrenamiento_z= entrenamiento_z  %>% select(-government_id, -year)
prueba_z        <- data_est %>% filter(year == 2020)
prueba_z = prueba_z %>% select(-government_id, -year)

5.3 Control de validación cruzada

# ============================================================
# CONFIGURACIÓN DE VALIDACIÓN CRUZADA
# ============================================================
set.seed(2025)
train_control <- trainControl(method="cv",number=5,savePredictions = TRUE, classProbs = TRUE)

6 Clasificador 1: K-Vecinos más Cercanos (KNN)

# ============================================================
# K-VECINOS MÁS CERCANOS por Estandarización (KNN)
# El parámetro k (número de vecinos) se optimiza via CV
# Se prueban valores de k del 1-50

# ============================================================
train.kknn(train_labels ~ ., data =  entrenamiento_min, kmax = 50) 
## 
## Call:
## train.kknn(formula = train_labels ~ ., data = entrenamiento_min,     kmax = 50)
## 
## Type of response variable: nominal
## Minimal misclassification: 0.06779661
## Best kernel: optimal
## Best k: 1
# Basado en el resultado del tuning anterior, el K óptimo seleccionado fue K = 1.
# Extraemos las matrices de predictores removiendo la última columna (la variable objetivo)
set.seed(123)
pred <- knn(
  train = entrenamiento_min[, -ncol(entrenamiento_min)], 
  test  = prueba_min[, -ncol(prueba_min)], 
  cl    = train_labels, 
  k     = 1
)

# Evaluación del Rendimiento del Clasificador
# Generamos la matriz de confusión para calcular la precisión (Accuracy), 
# Sensibilidad y Especificidad del modelo comparando las predicciones contra el año real (2020).
confusionMatrix(data = pred, reference = test_labels)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction A B C D F
##          A 0 3 3 3 1
##          B 5 2 5 4 0
##          C 2 1 5 7 5
##          D 1 2 4 7 2
##          F 0 0 3 5 2
## 
## Overall Statistics
##                                           
##                Accuracy : 0.2222          
##                  95% CI : (0.1327, 0.3356)
##     No Information Rate : 0.3611          
##     P-Value [Acc > NIR] : 0.9962          
##                                           
##                   Kappa : 0.0069          
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: A Class: B Class: C Class: D Class: F
## Sensitivity            0.0000  0.25000  0.25000  0.26923  0.20000
## Specificity            0.8438  0.78125  0.71154  0.80435  0.87097
## Pos Pred Value         0.0000  0.12500  0.25000  0.43750  0.20000
## Neg Pred Value         0.8710  0.89286  0.71154  0.66071  0.87097
## Prevalence             0.1111  0.11111  0.27778  0.36111  0.13889
## Detection Rate         0.0000  0.02778  0.06944  0.09722  0.02778
## Detection Prevalence   0.1389  0.22222  0.27778  0.22222  0.13889
## Balanced Accuracy      0.4219  0.51562  0.48077  0.53679  0.53548
# ============================================================
# PREDICCIÓN EN CONJUNTO DE PRUEBA
# ============================================================
set.seed(123)
knn_cv <- train(letter_grade ~ ., data=prueba_min, 
                method = "knn", trControl = train_control, tuneGrid = data.frame(k=1))

# Resultados de validación cruzada
cv1_knn_con= confusionMatrix(knn_cv$pred$pred, knn_cv$pred$obs)
cv1_knn_con
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction A B C D F
##          A 3 2 2 1 0
##          B 1 3 1 1 0
##          C 2 1 7 9 1
##          D 2 2 8 6 8
##          F 0 0 2 9 1
## 
## Overall Statistics
##                                           
##                Accuracy : 0.2778          
##                  95% CI : (0.1786, 0.3959)
##     No Information Rate : 0.3611          
##     P-Value [Acc > NIR] : 0.947           
##                                           
##                   Kappa : 0.0341          
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: A Class: B Class: C Class: D Class: F
## Sensitivity           0.37500  0.37500  0.35000  0.23077  0.10000
## Specificity           0.92188  0.95312  0.75000  0.56522  0.82258
## Pos Pred Value        0.37500  0.50000  0.35000  0.23077  0.08333
## Neg Pred Value        0.92188  0.92424  0.75000  0.56522  0.85000
## Prevalence            0.11111  0.11111  0.27778  0.36111  0.13889
## Detection Rate        0.04167  0.04167  0.09722  0.08333  0.01389
## Detection Prevalence  0.11111  0.08333  0.27778  0.36111  0.16667
## Balanced Accuracy     0.64844  0.66406  0.55000  0.39799  0.46129

6.0.1 Evaluación del Modelo de KNN con escalamiento Min-Max

Para intentar mejorar el rendimiento del clasificador, se empleó el uso de la función rescale(), que se utiliza para el escalamiento Min-Max. Este modelo ha obtenido una exactitud del 22.23% a la hora de realizar predicciones. El modelo también fue probado con una validación cruzada con los datos de prueba, este obtuvo una exactitud de 27.78%, probando ser ineficiente para la clasificación de los municipios.

# ============================================================
# K-VECINOS MÁS CERCANOS por Estandarización (Z-Score)
# ============================================================
set.seed(123)
pred_z           <- knn(entrenamiento_z[, -ncol(entrenamiento_z)], prueba_z[, -ncol(prueba_z)], cl = train_labels, k = 1)
confusionMatrix(data = pred_z, reference = test_labels)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction A B C D F
##          A 0 3 3 3 1
##          B 5 2 5 4 0
##          C 2 1 5 7 5
##          D 1 2 4 7 2
##          F 0 0 3 5 2
## 
## Overall Statistics
##                                           
##                Accuracy : 0.2222          
##                  95% CI : (0.1327, 0.3356)
##     No Information Rate : 0.3611          
##     P-Value [Acc > NIR] : 0.9962          
##                                           
##                   Kappa : 0.0069          
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: A Class: B Class: C Class: D Class: F
## Sensitivity            0.0000  0.25000  0.25000  0.26923  0.20000
## Specificity            0.8438  0.78125  0.71154  0.80435  0.87097
## Pos Pred Value         0.0000  0.12500  0.25000  0.43750  0.20000
## Neg Pred Value         0.8710  0.89286  0.71154  0.66071  0.87097
## Prevalence             0.1111  0.11111  0.27778  0.36111  0.13889
## Detection Rate         0.0000  0.02778  0.06944  0.09722  0.02778
## Detection Prevalence   0.1389  0.22222  0.27778  0.22222  0.13889
## Balanced Accuracy      0.4219  0.51562  0.48077  0.53679  0.53548
# ============================================================
# PREDICCIÓN EN CONJUNTO DE PRUEBA
# ============================================================
set.seed(123)
knn_cv_est <- train(letter_grade ~ ., data=cbind(prueba_z, letter_grade= test_labels),
                method = "knn", trControl = train_control, tuneGrid = data.frame(k=19))  

# Resultados de validación cruzada
cv_knn_con= confusionMatrix(knn_cv_est$pred$pred, knn_cv_est$pred$obs)
cv_knn_con
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  A  B  C  D  F
##          A  2  1  1  1  0
##          B  2  0  0  0  0
##          C  4  6  8  5  1
##          D  0  1 11 19  9
##          F  0  0  0  1  0
## 
## Overall Statistics
##                                          
##                Accuracy : 0.4028         
##                  95% CI : (0.2888, 0.525)
##     No Information Rate : 0.3611         
##     P-Value [Acc > NIR] : 0.2676         
##                                          
##                   Kappa : 0.1395         
##                                          
##  Mcnemar's Test P-Value : NA             
## 
## Statistics by Class:
## 
##                      Class: A Class: B Class: C Class: D Class: F
## Sensitivity           0.25000  0.00000   0.4000   0.7308  0.00000
## Specificity           0.95312  0.96875   0.6923   0.5435  0.98387
## Pos Pred Value        0.40000  0.00000   0.3333   0.4750  0.00000
## Neg Pred Value        0.91045  0.88571   0.7500   0.7812  0.85915
## Prevalence            0.11111  0.11111   0.2778   0.3611  0.13889
## Detection Rate        0.02778  0.00000   0.1111   0.2639  0.00000
## Detection Prevalence  0.06944  0.02778   0.3333   0.5556  0.01389
## Balanced Accuracy     0.60156  0.48438   0.5462   0.6371  0.49194

6.0.2 Evaluación del Modelo de KNN con normalización (Z-Score)

El modelo de KNN construido con Estandarización (Z-Score) resultó en un rendimiento similar al escalamiento Min-Max en las predicciones, pero superior en la validación cruzada. Este obtuvo una tasa de exactitud de 22.23% y una exactitud de validación cruzada de 40.28%

7 Clasificador 2: Árbol de Decisión (CART)

# ============================================================
# ÁRBOL DE DECISIÓN (C50 - Classification and Regression Trees)
# ============================================================

set.seed(2024)
arbol_c50 <- train(letter_grade ~ ., 
                   data=entrenamiento, 
                   method = "C5.0", 
                   trControl = train_control, 
                   tuneLength = 10)

final_preds <- predict(arbol_c50, prueba)
confusionMatrix(final_preds, as.factor(prueba$letter_grade))
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  A  B  C  D  F
##          A  8  7  2  0  0
##          B  0  0 10  5  0
##          C  0  1  8  7  1
##          D  0  0  0 13  5
##          F  0  0  0  1  4
## 
## Overall Statistics
##                                         
##                Accuracy : 0.4583        
##                  95% CI : (0.3402, 0.58)
##     No Information Rate : 0.3611        
##     P-Value [Acc > NIR] : 0.05695       
##                                         
##                   Kappa : 0.3101        
##                                         
##  Mcnemar's Test P-Value : NA            
## 
## Statistics by Class:
## 
##                      Class: A Class: B Class: C Class: D Class: F
## Sensitivity            1.0000   0.0000   0.4000   0.5000  0.40000
## Specificity            0.8594   0.7656   0.8269   0.8913  0.98387
## Pos Pred Value         0.4706   0.0000   0.4706   0.7222  0.80000
## Neg Pred Value         1.0000   0.8596   0.7818   0.7593  0.91045
## Prevalence             0.1111   0.1111   0.2778   0.3611  0.13889
## Detection Rate         0.1111   0.0000   0.1111   0.1806  0.05556
## Detection Prevalence   0.2361   0.2083   0.2361   0.2500  0.06944
## Balanced Accuracy      0.9297   0.3828   0.6135   0.6957  0.69194
# stability
set.seed(123)
arbol_cv <- train(letter_grade ~ ., data= prueba, 
                  method = "C5.0", trControl = train_control, 
                  tuneLength = 10)

# Matriz de confusión usando predicciones guardadas por caret

cv_tree= confusionMatrix(arbol_cv$pred$pred, arbol_cv$pred$obs)
cv_tree
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   A   B   C   D   F
##          A 106 131  38   8   0
##          B 212  57  66  19   0
##          C   2 132 487 294  25
##          D   0   0 205 555 265
##          F   0   0   4 164 110
## 
## Overall Statistics
##                                          
##                Accuracy : 0.4566         
##                  95% CI : (0.4383, 0.475)
##     No Information Rate : 0.3611         
##     P-Value [Acc > NIR] : < 2.2e-16      
##                                          
##                   Kappa : 0.2685         
##                                          
##  Mcnemar's Test P-Value : NA             
## 
## Statistics by Class:
## 
##                      Class: A Class: B Class: C Class: D Class: F
## Sensitivity           0.33125  0.17813   0.6088   0.5337  0.27500
## Specificity           0.93086  0.88398   0.7822   0.7446  0.93226
## Pos Pred Value        0.37456  0.16102   0.5181   0.5415  0.39568
## Neg Pred Value        0.91760  0.89588   0.8387   0.7385  0.88855
## Prevalence            0.11111  0.11111   0.2778   0.3611  0.13889
## Detection Rate        0.03681  0.01979   0.1691   0.1927  0.03819
## Detection Prevalence  0.09826  0.12292   0.3264   0.3559  0.09653
## Balanced Accuracy     0.63105  0.53105   0.6955   0.6391  0.60363

7.0.1 Evaluación de Modelo de Árboles de Decisión

El modelo de Árboles de Decisión alcanzó una exactitud del 45.83% en las predicciones, superando el rendimiento del modelo de K Vecinos Más Cercanos (KNN). Los resultados de la validación cruzada demostraron ser bastante estables, con una tasa de aciertos del 45.66%. No obstante, al presentar una exactitud por debajo del 50%, este modelo se considerar con cautela para la clasificación

8 Clasificador 3: Máquina de Soporte Vectorial (SVM)

# ============================================================
# MÁQUINA DE SOPORTE VECTORIAL (SVM) CON KERNEL RADIAL (RBF)
# Parámetros optimizados:
#   C     = parámetro de regularización (penalización por error)
#   sigma = ancho del kernel RBF (gamma en otras implementaciones)
# ============================================================



set.seed(2024)
modelo_svm <- train(
  x         = entrenamiento_z[, -ncol(entrenamiento_z)],
  y         = as.factor(train_labels),
  method    = "svmRadial",    
  trControl = train_control,
  metric    = "ROC",
  tuneLength = 8,             
  preProcess = NULL           
)



cat("=== RESULTADOS SVM ===\n")
## === RESULTADOS SVM ===
print(modelo_svm)
## Support Vector Machines with Radial Basis Function Kernel 
## 
## 590 samples
##  16 predictor
##   5 classes: 'A', 'B', 'C', 'D', 'F' 
## 
## No pre-processing
## Resampling: Cross-Validated (5 fold) 
## Summary of sample sizes: 472, 471, 470, 475, 472 
## Resampling results across tuning parameters:
## 
##   C      Accuracy   Kappa    
##    0.25  0.6982732  0.6110248
##    0.50  0.7251072  0.6425098
##    1.00  0.7437528  0.6655678
##    2.00  0.7591696  0.6857485
##    4.00  0.7625410  0.6910721
##    8.00  0.7608623  0.6886393
##   16.00  0.7404312  0.6616844
##   32.00  0.7541249  0.6794697
## 
## Tuning parameter 'sigma' was held constant at a value of 0.0644028
## Accuracy was used to select the optimal model using the largest value.
## The final values used for the model were sigma = 0.0644028 and C = 4.
cat("\nMejores parámetros - C:", modelo_svm$bestTune$C,
    "| sigma:", round(modelo_svm$bestTune$sigma, 5), "\n")
## 
## Mejores parámetros - C: 4 | sigma: 0.0644

8.1 NOTA:

El proceso evalúa de forma iterativa el costo (C) manteniendo fijo el parámetro de suavizado local (sigma = 0.0644028).El remuestreo se ejecuta mediante validación cruzada de 5 pliegues (5-fold CV).

8.2 RESULTADOS DEL PROCESO DE OPTIMIZACIÓN:

  1. El criterio de selección automática escoge el Accuracy más alto.
  2. Los hiperparámetros seleccionados son: C = 4 y sigma = 0.0644028.

8.3 Evaluación de Máquinas de Soporte Vectorial (SVM con Kernel Radial)

Este modelo ajustado, utilizando los hiperparámetros óptimos (C=4 y sigma=0.0644028), alcanzó una tasa de aciertos en el conjunto de validación de 76.25%. El índice Kappa de 0.6911 se clasifica como bueno, logrando una concordancia robusta.La validación cruzada resulta en una tasa de aciertos de 40.28% , manteniendo los parámetros previamente seleccionados.

# ============================================================
# PREDICCIÓN EN CONJUNTO DE PRUEBA
# ============================================================
set.seed(2024)
pred_svm      <- predict(modelo_svm, prueba_z[, -ncol(prueba_z)])
pred_svm_prob <- predict(modelo_svm, prueba_z[, -ncol(prueba_z)], type = "prob")

cm_svm <- confusionMatrix(pred_svm, as.factor(test_labels), positive = "Estresado")
cat("=== MATRIZ DE CONFUSIÓN: SVM ===\n")
## === MATRIZ DE CONFUSIÓN: SVM ===
print(cm_svm)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  A  B  C  D  F
##          A  4  6  7  0  0
##          B  1  1  2  2  0
##          C  0  0  6  6  0
##          D  0  0  3 14  6
##          F  3  1  2  4  4
## 
## Overall Statistics
##                                          
##                Accuracy : 0.4028         
##                  95% CI : (0.2888, 0.525)
##     No Information Rate : 0.3611         
##     P-Value [Acc > NIR] : 0.2676         
##                                          
##                   Kappa : 0.2302         
##                                          
##  Mcnemar's Test P-Value : NA             
## 
## Statistics by Class:
## 
##                      Class: A Class: B Class: C Class: D Class: F
## Sensitivity           0.50000  0.12500  0.30000   0.5385  0.40000
## Specificity           0.79688  0.92188  0.88462   0.8043  0.83871
## Pos Pred Value        0.23529  0.16667  0.50000   0.6087  0.28571
## Neg Pred Value        0.92727  0.89394  0.76667   0.7551  0.89655
## Prevalence            0.11111  0.11111  0.27778   0.3611  0.13889
## Detection Rate        0.05556  0.01389  0.08333   0.1944  0.05556
## Detection Prevalence  0.23611  0.08333  0.16667   0.3194  0.19444
## Balanced Accuracy     0.64844  0.52344  0.59231   0.6714  0.61935

9 Comparación de Clasificadores

9.1 Tabla de métricas de desempeño

# ============================================================
# FUNCIÓN AUXILIAR: Extraer métricas de un objeto confusionMatrix
# ============================================================

extraer_metricas <- function(cm, nombre_modelo) {
  data.frame(
    Modelo      = nombre_modelo,
    Accuracy    = round(cm$overall["Accuracy"],    4),
    Kappa       = round(cm$overall["Kappa"],        4),
    row.names   = NULL
  )
}

# Compilar métricas de los tres modelos
tabla_metricas <- bind_rows(
  extraer_metricas(cv_knn_con,   "KNN-Z score"),
  extraer_metricas(cv_tree, "Árbol de Decisión"),
  extraer_metricas(cm_svm,   "SVM"),
  extraer_metricas(cv1_knn_con,   "KNN- Min Max")
  
)

kable(tabla_metricas,
      caption = "Comparación de Métricas de Desempeño en Conjunto de Prueba",
      align   = "c") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed","bordered"),
                full_width = FALSE) %>%
  column_spec(1, bold = TRUE) %>%
  row_spec(which.max(tabla_metricas$F1_Score),
           background = "#d4efdf",
           bold       = TRUE)
Comparación de Métricas de Desempeño en Conjunto de Prueba
Modelo Accuracy Kappa
KNN-Z score 0.4028 0.1395
Árbol de Decisión 0.4566 0.2685
SVM 0.4028 0.2302
KNN- Min Max 0.2778 0.0341

10 ANÁLISIS

El ‘Árbol de Decisión’ resultó el mejor clasificador para predecir el estrés fiscal en el conjunto de prueba, alcanzando la precisión más alta (Accuracy = 45.66%) y el mejor índice de concordancia (Kappa = 0.2685 ). Aunque un Accuracy del 45.66% podría parecer moderado debido a la complejidad de la variable respuesta de 5 clases: ( A, B, C, D, F) , es un rendimiento aceptable

Cabe mencionar la degradación del rendimiento de los modelos al pasar de la validación al conjunto de prueba. Por ejemplo, SVM exhibía un Accuracy estimado del ~76.25% en el entrenamiento, pero cayó a un 40.28% en la prueba. Esto pudiera sugerir un cambio estructural en las condiciones fiscales y financieras de los municipios en 2020, en el que se basan los datos de prueba

KNN entrenado con estandarización Z-score superó al KNN entrenado con Min-Max (Accuracy: 40.28 % vs 27.78%| Kappa: 0.1395 vs 0.0341). Esto confirma que la escala Z-Score (scale()) es sustancialmente más robusta ante la presencia de valores atípicos (outliers) en las variables financieras municipales, para este modelo en particular

11 CONCLUSIÓN

Se selecciona el ‘Árbol de Decisión’ como el modelo definitivo para la implementación de este sistema de alerta de estrés fiscal, debido a su rendimiento superior de poder predictivo, y robustez ante datos no vistos.