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:
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 |
Para optimizar el rendimiento de los modelos de clasificación, el dataset final fue sometido a un proceso de reducción de dimensionalidad :
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.
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.
VARIABLES INGENIERADAS :
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)| 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
##
## A B C D F
## 87 153 286 188 97
##
## Porcentaje por categoría:
##
## A B C D F
## 10.7 18.9 35.3 23.2 12.0
# 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:
## 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
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.
# ============================================================
# 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
## Tamaño del conjunto de prueba: 72
## Distribución en entrenamiento:
##
## A B C D F
## 0.1067797 0.2000000 0.3542373 0.2220339 0.1169492
##
## Distribución en prueba:
##
## A B C D F
## 0.1111111 0.1111111 0.2777778 0.3611111 0.1388889
# ============================================================
# 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)# ============================================================
# 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
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
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%
# ============================================================
# Á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
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
# ============================================================
# 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 ===
## 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
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).
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 ===
## 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
# ============================================================
# 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)| 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 |
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
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.