Este informe ejecuta de principio a fin el proyecto Elecciones (Cartago 2022) + IDS (2023). Se cargan los datos originales, se limpian y normalizan, se unen por Cantón + Distrito, se generan estadísticas descriptivas y correlaciones, se detectan outliers y se aplica clustering PAM con selección automática de k por silhouette. Las tablas clave se muestran en formato interactivo (buscador, orden, “Show entries”) y se incluyen gráficos para una lectura clara.
# =========================
# Librerías
# =========================
suppressPackageStartupMessages({
library(readxl)
library(dplyr)
library(tidyr)
library(stringr)
library(stringi)
library(ggplot2)
library(cluster)
library(factoextra)
})
## Warning: package 'readxl' was built under R version 4.5.1
## Warning: package 'dplyr' was built under R version 4.5.1
## Warning: package 'tidyr' was built under R version 4.5.1
## Warning: package 'stringr' was built under R version 4.5.1
## Warning: package 'ggplot2' was built under R version 4.5.1
## Warning: package 'factoextra' was built under R version 4.5.1
use_dt <- requireNamespace("DT", quietly = TRUE) # para tablas interactivas
if (use_dt) library(DT)
## Warning: package 'DT' was built under R version 4.5.1
# =========================
# 2) Cargar datos
# =========================
elec <- read_excel("C:/Users/andre/Documents/CUATRIMESTRE_2_2025/BIG_DATA/Lenguajes_Mineria/ProyectoFinal/Cartago_Votos2022_limpio.xlsx", sheet = 1)
ids <- read_excel("C:/Users/andre/Documents/CUATRIMESTRE_2_2025/BIG_DATA/Lenguajes_Mineria/ProyectoFinal/IDS_CARTAGO_COMPLETO (2).xlsx", sheet = 1)
# =========================
# 2.1) Estandarizar nombres de columnas (ANTES de cualquier mutate)
# =========================
if ("territorio" %in% names(elec)) elec <- dplyr::rename(elec, Distrito = territorio)
if ("territorio" %in% names(ids)) ids <- dplyr::rename(ids, Distrito = territorio)
if ("distrito" %in% names(elec)) elec <- dplyr::rename(elec, Distrito = distrito)
if ("distrito" %in% names(ids)) ids <- dplyr::rename(ids, Distrito = distrito)
if ("canton" %in% names(elec)) elec <- dplyr::rename(elec, Canton = canton)
if ("canton" %in% names(ids)) ids <- dplyr::rename(ids, Canton = canton)
if ("cantón" %in% names(elec)) names(elec)[names(elec)=="cantón"] <- "Canton"
if ("cantón" %in% names(ids)) names(ids)[names(ids)=="cantón"] <- "Canton"
# Si alguna tabla no trae 'Canton', crearla para no fallar luego
if (!"Canton" %in% names(elec)) elec$Canton <- NA_character_
if (!"Canton" %in% names(ids)) ids$Canton <- NA_character_
# =========================
# 2.2) Normalizar textos y quitar tildes; rellenar Cantón
# =========================
elec <- elec %>%
mutate(
Canton = stri_trans_general(toupper(str_squish(Canton)), "Latin-ASCII"),
Distrito = stri_trans_general(toupper(str_squish(Distrito)), "Latin-ASCII")
) %>%
tidyr::fill(Canton, .direction = "down") # por si el Excel dejó celdas vacías hacia abajo
ids <- ids %>%
mutate(
Canton = stri_trans_general(toupper(str_squish(Canton)), "Latin-ASCII"),
Distrito = stri_trans_general(toupper(str_squish(Distrito)), "Latin-ASCII")
)
# =========================
# 2.3) Quitar filas de totales por cantón en elecciones
# =========================
elec_distritos <- elec %>%
filter(!str_detect(Distrito, regex("CANT[OÓ]N", ignore_case = TRUE)))
# =========================
# 2.4) Normalización fina de 'Distrito' y limpieza de 'Canton'
# =========================
normalizar <- function(x) {
x %>%
toupper() %>%
str_trim() %>%
str_replace_all("\\s+", " ") %>% # compactar espacios
stri_trans_general("Latin-ASCII") %>% # quitar tildes
str_replace("^EL\\s+", "") %>% # EL TEJAR -> TEJAR
str_replace("\\s+O\\s+.*$", "") # AGUACALIENTE O SAN FRANCISCO -> AGUACALIENTE
}
elec_distritos <- elec_distritos %>% mutate(Distrito = normalizar(Distrito))
ids <- ids %>% mutate(Distrito = normalizar(Distrito))
elec_distritos <- elec_distritos %>%
mutate(Canton = str_replace_all(Canton, "[^A-ZÁÉÍÓÚÑ ]", "") |> str_squish())
ids <- ids %>%
mutate(Canton = str_replace_all(Canton, "[^A-ZÁÉÍÓÚÑ ]", "") |> str_squish())
# =========================
# Chequeos previos
# =========================
cat("\n--- Chequeos previos ---\n")
##
## --- Chequeos previos ---
dup_elec <- elec_distritos %>% count(Canton, Distrito) %>% filter(n > 1)
dup_ids <- ids %>% count(Canton, Distrito) %>% filter(n > 1)
if (nrow(dup_elec) > 0) { cat("Duplicados en ELEC (Canton, Distrito):\n"); print(dup_elec) } else { cat("ELEC sin duplicados.\n") }
## ELEC sin duplicados.
if (nrow(dup_ids) > 0) { cat("Duplicados en IDS (Canton, Distrito):\n"); print(dup_ids) } else { cat("IDS sin duplicados.\n") }
## IDS sin duplicados.
faltan_en_ids <- anti_join(elec_distritos, ids, by = c("Canton","Distrito"))
faltan_en_elec <- anti_join(ids, elec_distritos, by = c("Canton","Distrito"))
cat("En ELEC pero NO en IDS:", nrow(faltan_en_ids), "\n")
## En ELEC pero NO en IDS: 2
cat("En IDS pero NO en ELEC:", nrow(faltan_en_elec), "\n")
## En IDS pero NO en ELEC: 1
# =========================
# 3) Unión final por llave compuesta
# =========================
datos <- inner_join(elec_distritos, ids, by = c("Canton","Distrito"))
exists("datos"); nrow(datos); names(datos)[1:8]
## [1] TRUE
## [1] 51
## [1] "Distrito" "juntas"
## [3] "electores" "votos_recibidos"
## [5] "participacion" "votos_validos"
## [7] "Partido_Accesibilidad" "Partido_AccionCiudadana"
# =========================
# 3.1) Parseo de porcentajes y tipos
# =========================
parse_pct <- function(x) {
x <- as.character(x)
x <- str_replace_all(x, "%", "")
x <- str_replace_all(x, ",", ".")
as.numeric(x)
}
tiene <- function(df, col) col %in% names(df)
if (tiene(datos, "Abstencion_Porcentaje")) datos$Abstencion_Porcentaje <- parse_pct(datos$Abstencion_Porcentaje)
if (tiene(datos, "participacion")) datos$participacion <- parse_pct(datos$participacion)
if (tiene(datos, "Partido_UnidadSocialCristiana")) datos$Partido_UnidadSocialCristiana <- parse_pct(datos$Partido_UnidadSocialCristiana)
if (tiene(datos, "IDS_2023")) datos$IDS_2023 <- as.numeric(datos$IDS_2023)
if (tiene(datos, "votos_recibidos")) datos$votos_recibidos <- as.numeric(datos$votos_recibidos)
cat("\n--- NAs post-unión (claves) ---\n")
##
## --- NAs post-unión (claves) ---
cols_rev <- c("votos_recibidos","participacion","Abstencion_Porcentaje","IDS_2023","Partido_UnidadSocialCristiana")
cols_rev <- cols_rev[cols_rev %in% names(datos)]
print(sapply(datos[, cols_rev, drop = FALSE], function(x) sum(is.na(x))))
## votos_recibidos participacion
## 0 0
## Abstencion_Porcentaje IDS_2023
## 0 0
## Partido_UnidadSocialCristiana
## 0
# =========================
# Vista previa de datos usados (interactiva si DT está disponible)
# =========================
preview <- datos %>%
select(Canton, Distrito, IDS_2023, participacion, Abstencion_Porcentaje) %>%
head(20)
if (use_dt) {
DT::datatable(
preview,
caption = htmltools::tags$caption(style="caption-side: top; text-align:left; font-weight:bold;",
"Muestra de datos (post-unión)"),
rownames = FALSE,
options = list(pageLength = 10, autoWidth = TRUE)
)
} else {
print(preview)
}
# =========================
# Estadísticas básicas por cantón
# =========================
moda <- function(x) as.numeric(names(which.max(table(round(x, 1)))))
Estadisticas <- datos %>%
dplyr::group_by(Canton) %>%
dplyr::summarise(
Promedio_Participacion = mean(participacion, na.rm = TRUE),
Mediana_Participacion = median(participacion, na.rm = TRUE),
Moda_Participacion = moda(participacion),
.groups = "drop"
)
if (use_dt) {
DT::datatable(
Estadisticas %>% mutate(across(-Canton, ~round(.x,1))),
rownames = FALSE,
caption = htmltools::tags$caption(style="caption-side: top; text-align:left; font-weight:bold;",
"Tabla 1. Estadísticas por Cantón"),
options = list(pageLength = 10, dom = "lfrtip", autoWidth = TRUE)
)
} else {
print(Estadisticas)
}
#
cat("\n--- Correlaciones ---\n")
##
## --- Correlaciones ---
# PUSC (votos) vs IDS_2023
if (all(c("Partido_UnidadSocialCristiana","IDS_2023") %in% names(datos))) {
# Valor de r y prueba
r_pusc_ids <- cor(datos$Partido_UnidadSocialCristiana, datos$IDS_2023, use="complete.obs")
ct_pusc <- try(cor.test(datos$Partido_UnidadSocialCristiana, datos$IDS_2023), silent = TRUE)
cat("\nCorrelación PUSC (votos) vs IDS_2023:", round(r_pusc_ids,3), "\n")
suppressWarnings(print(ct_pusc))
# Gráfico
print(
ggplot(datos, aes(x = IDS_2023, y = Partido_UnidadSocialCristiana)) +
geom_point(alpha = 0.7, size = 3) +
geom_smooth(method = "lm", se = TRUE) +
labs(title = "PUSC (votos) vs IDS_2023", x = "IDS_2023", y = "PUSC (votos)")
)
}
##
## Correlación PUSC (votos) vs IDS_2023: 0.46
##
## Pearson's product-moment correlation
##
## data: datos$Partido_UnidadSocialCristiana and datos$IDS_2023
## t = 3.6239, df = 49, p-value = 0.0006888
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## 0.2108771 0.6526390
## sample estimates:
## cor
## 0.4597432
## `geom_smooth()` using formula = 'y ~ x'
# Participación vs IDS_2023
if (all(c("participacion","IDS_2023") %in% names(datos))) {
r_part_ids <- cor(datos$participacion, datos$IDS_2023, use="complete.obs")
ct_part <- try(cor.test(datos$participacion, datos$IDS_2023), silent = TRUE)
cat("Correlación Participación vs IDS_2023:", round(r_part_ids,3), "\n")
suppressWarnings(print(ct_part))
print(
ggplot(datos, aes(x = IDS_2023, y = participacion)) +
geom_point(alpha = 0.7, size = 3) +
geom_smooth(method = "lm", se = TRUE) +
labs(title = "Participación (%) vs IDS_2023", x = "IDS_2023", y = "Participación (%)")
)
}
## Correlación Participación vs IDS_2023: 0.76
##
## Pearson's product-moment correlation
##
## data: datos$participacion and datos$IDS_2023
## t = 8.1853, df = 49, p-value = 9.963e-11
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## 0.6127399 0.8562421
## sample estimates:
## cor
## 0.7599907
## `geom_smooth()` using formula = 'y ~ x'
#outliers
cat("\n--- Outliers Partido Unidad Social Cristiana (PUSC) ---\n")
##
## --- Outliers Partido Unidad Social Cristiana (PUSC) ---
if ("Partido_UnidadSocialCristiana" %in% names(datos)) {
print(
ggplot(datos, aes(y = Partido_UnidadSocialCristiana)) +
geom_boxplot() +
labs(title = "Boxplot PUSC votos", y = "PUSC (cantidad de votos)")
)
}
cat("\n--- Outliers Participación ---\n")
##
## --- Outliers Participación ---
if ("participacion" %in% names(datos)) {
print(
ggplot(datos, aes(y = participacion)) +
geom_boxplot() +
labs(title = "Boxplot Participación (%)", y = "Participación (%)")
)
}
# Listas de outliers (texto)
if ("Partido_UnidadSocialCristiana" %in% names(datos)) {
out_pusc <- datos$Distrito[ datos$Partido_UnidadSocialCristiana %in% boxplot.stats(datos$Partido_UnidadSocialCristiana)$out ]
if (length(out_pusc)) { cat("Distritos outlier en PUSC:\n"); print(unique(out_pusc)) }
}
## Distritos outlier en PUSC:
## [1] "AGUACALIENTE" "TURRIALBA" "SAN RAFAEL"
if ("participacion" %in% names(datos)) {
out_part <- datos$Distrito[ datos$participacion %in% boxplot.stats(datos$participacion)$out ]
if (length(out_part)) { cat("Distritos outlier en Participación:\n"); print(unique(out_part)) }
}
## Distritos outlier en Participación:
## [1] "CHIRRIPO"
# =========================
# Variables base (no usadas directamente para PAM final, pero útiles)
# =========================
vars <- c("votos_recibidos", "participacion", "IDS_2023", "Abstencion_Porcentaje")
base_k <- datos %>% select(any_of(vars)) %>% drop_na()
X <- scale(base_k)
D <- dist(X)
# =========================
# 7) Clustering (PAM con elección automática de k) usando votos por partido + IDS + participación + abstención
# =========================
# Si IDS_2023 == 0 considerarlo NA para evitar sesgos
datos$IDS_2023 <- dplyr::na_if(datos$IDS_2023, 0)
# Detectar columnas de votos por partido (prefijo "votos_"), excluyendo el total
partido_cols <- names(datos)[grepl("^votos_", names(datos), ignore.case = TRUE)]
partido_cols <- setdiff(partido_cols, c("votos_recibidos"))
if (length(partido_cols) == 0) {
stop("No se encontraron columnas de votos por partido que empiecen con 'votos_'. Renombra o ajusta el patrón.")
}
# Variables finales
vars2 <- c(partido_cols, "IDS_2023", "participacion", "Abstencion_Porcentaje")
vars2 <- intersect(vars2, names(datos))
# Base limpia
base_k2 <- datos %>% select(any_of(vars2)) %>% drop_na()
X2 <- scale(base_k2)
D2 <- dist(X2)
# Elegir k por silhouette promedio
sil_avg <- function(cl) mean(silhouette(cl, D2)[, 3])
set.seed(123)
ks <- 2:6
sil_pam <- sapply(ks, function(k) { fit <- pam(X2, k); sil_avg(fit$clustering) })
k_best <- ks[which.max(sil_pam)]
pam_fit <- pam(X2, k_best)
cat("\nPAM: mejor k =", k_best, " | silhouette =", round(max(sil_pam), 3), "\n")
##
## PAM: mejor k = 4 | silhouette = 0.386
# Adjuntar grupos (solo filas sin NA en vars2)
datos_cluster <- datos %>%
filter(complete.cases(select(., any_of(vars2)))) %>%
mutate(grupo_num = pam_fit$clustering)
# Perfil por grupo (medianas)
perfil <- datos_cluster %>%
group_by(grupo_num) %>%
summarise(
n = n(),
IDS_med = median(IDS_2023, na.rm = TRUE),
Part_med = median(participacion, na.rm = TRUE),
Abst_med = median(Abstencion_Porcentaje, na.rm = TRUE),
.groups = "drop"
)
# Paleta y etiquetas
PALETA5 <- c("red", "black", "blue", "green", "yellow")
COLOR_NOMBRES <- c("Rojo", "Negro", "Azul", "Verde", "Amarillo")
n_gr <- length(unique(datos_cluster$grupo_num))
colores_usar <- rep(PALETA5, length.out = n_gr)
nombres_ley <- rep(COLOR_NOMBRES, length.out = n_gr)
# Etiquetas descriptivas (se recortan al número de grupos hallados)
datos_cluster <- datos_cluster %>%
mutate(
grupo = factor(
grupo_num,
levels = sort(unique(grupo_num)),
labels = c(
"Alto IDS + Muy alta participación",
"Alto IDS + Alta participación",
"Medio IDS + Participación media",
"Bajo IDS + Baja participación"
)[seq_len(n_gr)]
)
)
# Mostrar perfil (interactivo si DT)
if (use_dt) {
DT::datatable(
perfil %>% mutate(across(c(IDS_med, Part_med, Abst_med), ~round(.x,1))),
rownames = FALSE,
caption = htmltools::tags$caption(style="caption-side: top; text-align:left; font-weight:bold;",
"Tabla 2. Perfil de clusters (medianas)"),
options = list(pageLength = 10, dom = "lfrtip", autoWidth = TRUE)
)
} else {
print(perfil)
}
# Gráfico de clusters (IDS vs Participación)
if (all(c("IDS_2023","participacion") %in% names(datos_cluster))) {
print(
ggplot(datos_cluster, aes(x = IDS_2023, y = participacion, color = grupo)) +
geom_point(size = 3, alpha = 0.9) +
scale_color_manual(values = colores_usar, name = "Clusters (color)") +
labs(
title = paste0("Clusters PAM con votos por partido + IDS (k = ", n_gr, ")"),
subtitle = paste0("Silhouette: ", round(max(sil_pam), 3),
" | Variables: ", paste(vars2, collapse = ", ")),
x = "IDS 2023",
y = "Participación (%)"
) +
theme_minimal(base_size = 13)
)
}
El análisis mostró que los distritos de Cartago pueden agruparse en cuatro perfiles según su nivel de desarrollo social y participación electoral. Los distritos con IDS más alto tienden a votar más, mientras que los de IDS más bajo presentan mayor abstención. Estos hallazgos pueden orientar acciones para mejorar la participación y reducir desigualdades.