1- Introducción

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.

2- Carga y limpieza

# =========================
# 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- Union de Data set

# =========================
# 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)
}

4- Estadisticas basicas

# =========================
# 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)
}

5- Correlaciones

#
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'

6-Outliers

#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"

7- Clustering PAM

# =========================
# 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)
  )
}

8- Conclusion

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.