Análisis estadístico de la asistencia a la Ruta del PGV

Cargar librerías

En esta parte, se cargan las librerías esenciales para el análisis estadístico y la visualización. tidyverse y readxl resultan fundamentales para manipular y leer datos, mientras que ggplot2 facilita la generación de gráficos.

library(tidyverse)       # Manipulación y análisis de datos (incluye dplyr, etc.)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.0.4     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(ggplot2)         # Creación de gráficos y visualizaciones
library(readxl)          # Lectura de archivos Excel
# install.packages("viridis")   # Paletas de color adicionales
library(geodata)         # Descarga de datos geográficos (GADM)
## Loading required package: terra
## terra 1.8.21
## 
## Attaching package: 'terra'
## 
## The following object is masked from 'package:tidyr':
## 
##     extract
library(sf)              # Manejo de datos espaciales en R
## Linking to GEOS 3.13.0, GDAL 3.10.1, PROJ 9.5.1; sf_use_s2() is TRUE
library(dplyr)           # Manipulación de data frames
library(ggplot2)         # Visualizaciones con gramática de gráficos
library(viridis)         # Paletas de color para ggplot
## Loading required package: viridisLite
library(stringi)         # Transformaciones de texto (remover tildes, etc.)
library(ggthemes)        # Temas adicionales para ggplot
library(ggrepel)         # Evitar superposición de etiquetas en gráficos
# install.packages("rnaturalearth")
library(rnaturalearth)   # Datos geoespaciales de países del mundo
library(rnaturalearthdata) # Conjunto de datos complementarios para rnaturalearth
## 
## Attaching package: 'rnaturalearthdata'
## 
## The following object is masked from 'package:rnaturalearth':
## 
##     countries110

Lectura y limpieza de datos

Aquí se lee el archivo Excel que contiene la información de los webinars, asegurándose de transformar la columna de celular en carácter para evitar problemas con ceros iniciales o formatos numéricos. Luego se verifica la estructura del dataframe con glimpse().

RutaPGV <- read_excel("Estadísticas de asistencia - Ruta PGV.xlsx")

# para evitar problemas con ceros a la izquierda o formatos numéricos.
RutaPGV <- RutaPGV %>%
  mutate(`Celular 1` = as.character(`Celular 1`)) %>%
  rename(Celular = `Celular 1`)


# Verificamos la estructura para asegurarnos de que todo quedó bien
glimpse(RutaPGV)
## Rows: 1,458
## Columns: 11
## $ Tipo                   <chr> "Asistencia", "Asistencia", "Asistencia", "Asis…
## $ Webinar                <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
## $ Sector                 <chr> "Autoridad", "Sociedad Civil", "Autoridad", "Pr…
## $ Entidad                <chr> "INTRASOG", "Ninguna", "Unidad de Tránsito Depa…
## $ Departamento           <chr> "Boyacá", "NA", "Caldas", "Guatemala", "Caldas"…
## $ Territorio             <chr> "Sogamoso", "NA", "Caldas", "Guatemala", "Maniz…
## $ Cargo                  <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ Nombres                <chr> "Reynel Andrés", "Luis Ignacio", "Juan Esteban"…
## $ Apellidos              <chr> "Sandoval Lopez", "Suárez Barrera", "Aristizába…
## $ `Correos electrónicos` <chr> "planeacion@intrasog.gov.co", "Luissuarez00@yah…
## $ Celular                <chr> "3.144518082E9", "3.153419038E9", "3.226113831E…

Análisis general

Conteo de personas

Se filtran los datos según el tipo (Inscripción o Asistencia) y se cuentan cuántos celulares únicos hay en cada caso, para conocer cuántas personas diferentes se inscribieron y cuántas asistieron.

# Conteo de personas únicas que se inscribieron
inscritos_unicos <- RutaPGV %>%
  filter(Tipo == "Inscripción") %>%
  summarize(total_inscritos = n_distinct(Celular))

# Conteo de personas únicas que asistieron
asistentes_unicos <- RutaPGV %>%
  filter(Tipo == "Asistencia") %>%
  summarize(total_asistentes = n_distinct(Celular))

inscritos_unicos
## # A tibble: 1 × 1
##   total_inscritos
##             <int>
## 1             716
asistentes_unicos
## # A tibble: 1 × 1
##   total_asistentes
##              <int>
## 1              335

Análisis por sector y origen

En este apartado, se crea un nuevo dataframe con personas únicas (usando distinct), se cuenta el número de participantes totales y, posteriormente, se obtiene la distribución por departamento/país y por sector. Esto sirve para tener una visión general de dónde provienen y cuál es su sector.

# Podemos crear un dataframe solo con personas únicas (independiente del tipo),
# quedándonos con la primera aparición de cada Celular.
personas_unicas <- RutaPGV %>%
  distinct(Celular, .keep_all = TRUE)

# Número total de personas únicas
n_total_personas <- nrow(personas_unicas)
n_total_personas
## [1] 817
# Distribución por Departamento/País
dist_departamento <- personas_unicas %>%
  count(Departamento, sort = TRUE)

dist_departamento
## # A tibble: 38 × 2
##    Departamento        n
##    <chr>           <int>
##  1 NA                169
##  2 Valle del Cauca    91
##  3 Bogotá D. C.       84
##  4 Antioquia          79
##  5 Cundinamarca       54
##  6 Caldas             36
##  7 Boyacá             34
##  8 Quindío            30
##  9 Bolívar            28
## 10 Nariño             24
## # ℹ 28 more rows
# Distribución por Sector
dist_sector <- personas_unicas %>%
  count(Sector, sort = TRUE)

dist_sector
## # A tibble: 9 × 2
##   Sector             n
##   <chr>          <int>
## 1 Autoridad        563
## 2 Privado          151
## 3 Sociedad Civil    74
## 4 Academia          10
## 5 Coalición         10
## 6 Independiente      3
## 7 Legislativo        3
## 8 Medio              2
## 9 Sociedad civil     1

Visualizaciones

Ahora generamos gráficos de barras para visualizar el número de participantes por departamento y por sector. Cada uno tiene su propio theme y etiquetas en el eje X rotadas 45 grados para mejorar la legibilidad.

# Gráfico de barras por Departamento
ggplot(dist_departamento, aes(x = reorder(Departamento, -n), y = n)) +
  geom_col(fill = "steelblue") +
  labs(
    title = "Distribución de participantes por Departamento/País",
    x = "Departamento/País",
    y = "Número de personas únicas"
  ) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

# Gráfico de barras por Sector
ggplot(dist_sector, aes(x = reorder(Sector, -n), y = n)) +
  geom_col(fill = "tomato") +
  labs(
    title = "Distribución de participantes por Sector",
    x = "Sector",
    y = "Número de personas únicas"
  ) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Análisis por cada webinar

Inscritos y asistentes

El siguiente bloque agrupa los datos por Webinar y Tipo, para determinar cuántos registros (en este caso, filas) corresponden a cada categoría. Esto permite comparar inscripciones y asistencias por cada uno de los 5 webinars.

# Agrupamos por webinar y por tipo, y calculamos el número de celulares únicos
webinar_tipo <- RutaPGV %>%
  group_by(Webinar, Tipo) %>%
  summarize(
    n_personas = n(),
    .groups = "drop"
  )

webinar_tipo
## # A tibble: 11 × 3
##    Webinar Tipo        n_personas
##      <dbl> <chr>            <int>
##  1       1 Asistencia          50
##  2       1 Inscripción        109
##  3       2 Asistencia          81
##  4       2 Inscripción        152
##  5       3 Asistencia          75
##  6       3 Inscripción        212
##  7       4 Inscripción         77
##  8       5 Asistencia          90
##  9       5 Inscripción        255
## 10       6 Asistencia          96
## 11       6 Inscripción        261

Asistentes a los webinars

Para averiguar si hay personas que han asistido a más de un webinar y, específicamente, a cuáles han ido, se puede usar el celular como identificador de cada persona.

asistentes_varios <- RutaPGV %>%
  filter(Tipo == "Asistencia") %>%
  group_by(Celular) %>%
  summarize(
    n_webinars = n_distinct(Webinar), 
    webinars_list = paste(sort(unique(Webinar)), collapse = ", ")
  ) %>%
  # Nos quedamos solo con quienes hayan asistido a más de un webinar
  filter(n_webinars > 1) %>%
  arrange(desc(n_webinars))

# Vemos cuántas personas son
nrow(asistentes_varios)
## [1] 33
# Vemos el listado detallado
asistentes_varios
## # A tibble: 33 × 3
##    Celular       n_webinars webinars_list
##    <chr>              <int> <chr>        
##  1 3.024314572E9          4 1, 2, 3, 5   
##  2 3.233050078E9          4 1, 2, 5, 6   
##  3 3.014908488E9          3 3, 5, 6      
##  4 3.138707343E9          3 2, 3, 5      
##  5 3.154827532E9          3 1, 2, 3      
##  6 3.165764772E9          3 1, 3, 5      
##  7 3.167450522E9          3 3, 5, 6      
##  8 3.178315436E9          3 2, 3, 5      
##  9 3.178933247E9          3 1, 2, 3      
## 10 3.20344475E9           3 2, 3, 5      
## # ℹ 23 more rows

Visualización por webinar

Para visualizar la evolución, se convierte la columna Webinar a factor (si se desea un eje categórico) o a numérico (si se prefiere una gráfica de línea). Se muestran dos gráficos: uno de barras comparando inscritos vs asistentes, y otro de líneas para ver la tendencia a lo largo de los distintos webinars.

# Convertimos Webinar en factor si queremos un eje categórico (1,2,3,4,5).
# Si preferimos un eje numérico (línea de tiempo), podemos dejarlo numérico.
webinar_tipo <- webinar_tipo %>%
  mutate(Webinar = as.factor(Webinar))

# Visualización en barras, inscritos vs asistentes en cada webinar
ggplot(webinar_tipo, aes(x = Webinar, y = n_personas, fill = Tipo)) +
  geom_col(position = "dodge") +
  labs(
    title = "Número de inscritos y asistentes por webinar (personas únicas)",
    x = "Webinar",
    y = "Número de personas"
  ) +
  scale_fill_manual(values = c("#1b9e77", "#d95f02")) + 
  theme_minimal()

# Si prefieres ver la evolución en forma de línea:
# (En este caso, conviene que Webinar sea numérico; si no, conviértelo antes)
webinar_tipo_line <- RutaPGV %>%
  group_by(Webinar, Tipo) %>%
  summarize(n_personas = n(), .groups = "drop") %>%
  mutate(Webinar = as.numeric(Webinar))

ggplot(webinar_tipo_line, aes(x = Webinar, y = n_personas, color = Tipo)) +
  geom_line() +
  geom_point() +
  labs(
    title = "Evolución de inscritos y asistentes por webinar",
    x = "Webinar",
    y = "Número de personas"
  ) +
  scale_color_manual(values = c("#1b9e77", "#d95f02")) +
  theme_minimal()

Análisis de origen y sector en cada webinar

Estos bloques finalizan el análisis por webinar, mostrando de qué departamentos provienen los participantes en cada evento, primero en una sola gráfica apilada y luego separando inscripciones y asistencias con facet_wrap(~ Tipo).

webinar_departamento <- RutaPGV %>%
  group_by(Webinar, Departamento, Tipo) %>%
  summarize(n_personas = n(), .groups = "drop")

webinar_departamento
## # A tibble: 239 × 4
##    Webinar Departamento Tipo        n_personas
##      <dbl> <chr>        <chr>            <int>
##  1       1 Antioquia    Asistencia           7
##  2       1 Antioquia    Inscripción          9
##  3       1 Argentina    Inscripción          1
##  4       1 Bogotá D. C. Asistencia           1
##  5       1 Bogotá D. C. Inscripción          3
##  6       1 Bolívar      Asistencia           8
##  7       1 Bolívar      Inscripción          8
##  8       1 Boyacá       Asistencia           2
##  9       1 Boyacá       Inscripción          5
## 10       1 Caldas       Asistencia           5
## # ℹ 229 more rows
ggplot(webinar_departamento, aes(x = factor(Webinar), y = n_personas, fill = Departamento)) +
  geom_col(position = "stack") +
  labs(
    title = "Participantes (inscritos/asistentes) por Departamento en cada Webinar",
    x = "Webinar",
    y = "Número de personas únicas"
  ) +
  theme_minimal() +
  theme(legend.position = "bottom")

webinar_departamento_tipo <- RutaPGV %>%
  group_by(Webinar, Departamento, Tipo) %>%
  summarize(n_personas = n(), .groups = "drop")

ggplot(webinar_departamento_tipo, aes(x = factor(Webinar), y = n_personas, fill = Departamento)) +
  geom_col(position = "stack") +
  facet_wrap(~ Tipo, ncol = 1) +
  labs(
    title = "Inscripciones y asistencias por Departamento en cada Webinar",
    x = "Webinar",
    y = "Número de personas únicas"
  ) +
  coord_flip() +
  theme_minimal() +
  theme(legend.position = "bottom")

Mapas

Mapa departamental

En este apartado, se descargan los polígonos de los departamentos de Colombia usando la librería geodata y nivel 1 de GADM. Luego, se convierten en un sf, se preparan las columnas de departamento (removiendo acentos y pasando a mayúsculas) y se hace el join con la tabla dist_departamento. Finalmente, se dibuja un mapa coloreando cada departamento según el número de participantes y se añaden etiquetas con la función geom_sf_text() en el centroide de cada polígono.

# Descarga el shapefile de GADM para Colombia (level=1 = departamentos)
colombia_map <- geodata::gadm("COL", level = 1, path = ".")

# Convierte a objeto sf (geom espacial)
colombia_map <- sf::st_as_sf(colombia_map)

# Se crea una nueva función para eliminar las tildes
remove_accents <- function(x) {
  stri_trans_general(x, "Latin-ASCII")
}

# Crea una columna con el nombre del departamento en mayúsculas
# para hacer el cruce (join) con tus datos.
colombia_map <- colombia_map %>%
  mutate(
    NAME_1 = toupper(remove_accents(NAME_1)), 
    Departamento_gadm = NAME_1
  )

dist_departamento <- dist_departamento %>%
  mutate(
    Departamento = toupper(remove_accents(Departamento)), 
  )

dist_departamento <- dist_departamento %>%
  mutate(Departamento = case_when(
    Departamento == "BOGOTA D. C." ~ "BOGOTA D.C.",
    TRUE ~ Departamento
  ))

# Left join del mapa con tu tabla de departamentos
colombia_map_data <- left_join(colombia_map, dist_departamento,
                               by = c("Departamento_gadm" = "Departamento"))

colombia_map_centroids <- colombia_map_data %>%
  # Calcula el centro geométrico de cada polígono
  st_centroid()  
## Warning: st_centroid assumes attributes are constant over geometries
ggplot() +
  # Polígonos de departamentos
  geom_sf(data = colombia_map_data, aes(fill = n), color = "white") +
  # Etiquetas en los centroides, usando la columna 'n'
  geom_label_repel(
    data = colombia_map_centroids,
    aes(label = n, geometry = geometry),
    size = 2,
    stat = "sf_coordinates",
    min.segment.length = 0
  ) +
  scale_fill_viridis_c(
    na.value = "grey80",
    option = "plasma"
  ) +
  theme_minimal() +
  labs(
    title = "Cantidad de participantes por Departamento",
    fill = "Cantidad"
  )
## Warning in st_point_on_surface.sfc(sf::st_zm(x)): st_point_on_surface may not
## give correct results for longitude/latitude data
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_label_repel()`).

Mapa municipal

En esta sección, se filtran y agrupan los registros por municipio y departamento, eliminando valores NA. Luego, se descargan los polígonos municipales de Colombia (GADM nivel 2), se limpian nombres (remove_accents, mayúsculas) y se establecen transformaciones específicas (por ejemplo, “CALI” a “SANTIAGO DE CALI”). Tras el join, generan un mapa donde cada municipio está coloreado según el número de participantes, añadiendo etiquetas para aquellos con 5 o más asistentes.

municipios_colombia <- RutaPGV %>%
  filter(!is.na(Departamento)) %>%                # Asegurarnos de excluir filas sin depto.
  filter(Departamento != "NA") %>%                # O excluir las que digan "NA" literal
  group_by(Departamento, Territorio) %>%
  summarize(
    participantes = n_distinct(Celular),
    .groups = "drop"
  ) %>%
  mutate(
    Departamento = toupper(remove_accents(Departamento)),
    Territorio = toupper(remove_accents(Territorio))
  ) %>%
  mutate(Departamento = case_when(
    Departamento == "BOGOTA D. C." ~ "BOGOTA D.C.",
    TRUE ~ Departamento
  )) %>%
  mutate(Territorio = case_when(
    Territorio == "BOGOTA D. C." ~ "BOGOTA D.C.",
    Territorio == "DONMATIAS" ~ "DON MATIAS",
    Territorio == "PASTO" ~ "SAN JUAN DE PASTO",
    Territorio == "CALI" ~ "SANTIAGO DE CALI",
    Territorio == "CUCUTA" ~ "SAN JOSE DE CUCUTA",
    Territorio == "MOCOA" ~ "SAN MIGUEL DE MOCOA",
    Territorio == "CARTAGENA" ~ "CARTAGENA DE INDIAS",
    Territorio == "CARMEN DE BOLIVAR" ~ "EL CARMEN DE BOLIVAR",
    TRUE ~ Territorio
  ))
  
# Descargamos GADM para Colombia al nivel 2
colombia_muni_map <- geodata::gadm("COL", level = 2, path = ".") %>%
  st_as_sf(colombia_muni_map) %>%
  mutate(
    DEPTO_SHP = toupper(remove_accents(NAME_1)),
    MUNI_SHP  = toupper(remove_accents(NAME_2))
  )


colombia_muni_data <- left_join(colombia_muni_map, 
                                municipios_colombia,
                                by = c("DEPTO_SHP" = "Departamento",
                                       "MUNI_SHP"  = "Territorio"))

colombia_muni_centroids <- colombia_muni_data %>%
  st_centroid() %>%
  filter(!is.na(participantes), participantes >= 5)
## Warning: st_centroid assumes attributes are constant over geometries
ggplot() +
  geom_sf(data = colombia_muni_data, aes(fill = participantes)) +
  theme_map() +
  geom_label_repel(
    data = colombia_muni_centroids,
    aes(label = participantes, geometry = geometry),
    size = 2,
    stat = "sf_coordinates",
    min.segment.length = 0,
    max.overlaps = 20
  ) +
  scale_fill_viridis_c(na.value = "grey80", option = "plasma") +
  theme_minimal() +
  labs(
    title = "Participantes por municipio en Colombia",
    fill = "N° Part."
  )
## Warning in st_point_on_surface.sfc(sf::st_zm(x)): st_point_on_surface may not
## give correct results for longitude/latitude data

Mapa LATAM

Por último, generan un mapa de países de Latinoamérica para visualizar la participación de personas que no provienen de Colombia. Se filtra la base original para extraer los registros donde la columna Departamento corresponde realmente a un país. Luego, se descargan polígonos de países con rnaturalearth y se realiza la unión por el nombre del país. Se dibuja un mapa coloreado según el número de participantes y se sitúan etiquetas con los valores en sus centroides.

# Filtramos solo filas que tengan un 'Departamento' que en realidad sea país:
paises_latam <- RutaPGV %>%
  distinct(Celular, .keep_all = TRUE) %>% # Si quieres conteo de personas únicas
  filter(!is.na(Departamento)) %>%
  group_by(Departamento) %>%
  summarize(
    participantes = n_distinct(Celular),
    .groups = "drop"
  ) %>%
  mutate(Departamento = toupper(remove_accents(Departamento)))
world <- ne_countries(scale = "medium", returnclass = "sf")
# world es un sf con todos los países del mundo.

# Filtrar solo América Latina (aprox.). Podríamos filtrar por subregión:
latam <- world %>%
  filter(subregion == "South America" | subregion == "Central America")  # Esto incluye todo el continente
  # o filter(subregion == "South America" | subregion == "Central America" | name == "Mexico") 
  # según tu preferencia

latam <- latam %>%
  mutate("ADMIN_UP" = toupper(remove_accents(admin)))

paises_latam_data <- left_join(
  latam,
  paises_latam,
  by = c("ADMIN_UP" = "Departamento")
)

latam_centroids <- paises_latam_data %>%
  st_centroid()
## Warning: st_centroid assumes attributes are constant over geometries
ggplot() +
  geom_sf(data = paises_latam_data, aes(fill = participantes), color = "white") +
  theme_map() +
  geom_label_repel(
    data = latam_centroids,
    aes(label = participantes, geometry = geometry),
    stat = "sf_coordinates",
    min.segment.length = 0
  ) +
  scale_fill_viridis_c(na.value = "grey80", option = "plasma") +
  theme_minimal() +
  labs(
    title = "Participantes por país en América Latina",
    fill = "N° Part."
  )
## Warning in st_point_on_surface.sfc(sf::st_zm(x)): st_point_on_surface may not
## give correct results for longitude/latitude data
## Warning: Removed 18 rows containing missing values or values outside the scale range
## (`geom_label_repel()`).

Estos últimos chunks sirven para generar listas de municipios y compararlas con los nombres del shapefile, o realizar comprobaciones adicionales de correspondencias.

lista_muni <- RutaPGV %>%
  distinct(Departamento, Territorio)

mapa_muni <- colombia_muni_map %>%
  distinct(DEPTO_SHP, MUNI_SHP)