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
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…
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
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
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))
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
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
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()
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")
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()`).
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
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)