Este documento es una muestra de trabajo elaborada en el marco de la postulación a la consultoría de análisis de datos del Programa “Becas Butiá” (INEFOP, SC-CP-09/2026). Dado que los registros administrativos reales del programa no son de acceso público, se construyó una base de datos ficticia que replica la estructura y el volumen de información esperados (14.700 becarios, cifra publicada en los propios términos de referencia para 2025), con el objetivo de demostrar el flujo de trabajo solicitado en los productos 3, 4, 5 y 11 del llamado: generación y depuración de datos, documentación, cálculo de indicadores y visualización.
Nota metodológica: todos los datos que siguen son simulados. Las relaciones entre variables (por ejemplo, entre departamento y asistencia) son supuestos definidos para este ejercicio, no estadísticas reales.
Se simulan 14.700 becarios. La asistencia depende del departamento (supuesto: menor infraestructura educativa relativa en el interior), y el estado de trayectoria depende a su vez de la asistencia, para que el ejercicio tenga una estructura realista.
set.seed(2026)
n <- 14700
departamentos <- c("Montevideo", "Canelones", "Maldonado", "Rocha", "Treinta y Tres",
"Cerro Largo", "Rivera", "Artigas", "Salto", "Paysandú",
"Río Negro", "Soriano", "Colonia", "San José", "Flores",
"Florida", "Lavalleja", "Durazno", "Tacuarembó")
pesos_departamento <- c(0.30, 0.16, 0.05, 0.02, 0.015, 0.02, 0.03, 0.015, 0.04, 0.04,
0.02, 0.02, 0.03, 0.03, 0.01, 0.02, 0.02, 0.02, 0.02)
pesos_departamento <- pesos_departamento / sum(pesos_departamento)
medias_asistencia <- tibble(
departamento = departamentos,
media_asistencia = c(80, 78, 74, 65, 62, 64, 68, 60, 70, 71,
66, 72, 76, 75, 73, 74, 70, 63, 65)
)
base_becas <- tibble(
id_estudiante = 1:n,
edad = sample(11:21, n, replace = TRUE,
prob = c(.10, .10, .10, .10, .10, .10, .09, .08, .08, .08, .07)),
sexo = sample(c("Mujer", "Varón"), n, replace = TRUE, prob = c(.51, .49)),
departamento = sample(departamentos, n, replace = TRUE, prob = pesos_departamento),
ciclo = if_else(edad <= 14, "Ciclo Básico", "Bachillerato"),
año_lectivo = 2025L,
modalidad_financiamiento = sample(c("INEFOP", "ANEP", "MEC"), n, replace = TRUE,
prob = c(.45, .35, .20)),
fecha_inscripcion_beca = sample(seq(as.Date("2025-02-01"), as.Date("2025-04-30"), by = "day"),
n, replace = TRUE)
) %>%
left_join(medias_asistencia, by = "departamento") %>%
mutate(
asistencia_pct = round(pmin(100, pmax(0, rnorm(n(), mean = media_asistencia, sd = 15))), 1),
estado_trayectoria = case_when(
asistencia_pct < 60 ~ sample(c("Abandono", "Rezago", "Activo", "Egresado"), n(),
replace = TRUE, prob = c(.35, .35, .25, .05)),
asistencia_pct < 80 ~ sample(c("Abandono", "Rezago", "Activo", "Egresado"), n(),
replace = TRUE, prob = c(.10, .20, .60, .10)),
TRUE ~ sample(c("Abandono", "Rezago", "Activo", "Egresado"), n(),
replace = TRUE, prob = c(.02, .05, .68, .25))
),
monto_mensual = 2500
) %>%
select(-media_asistencia)
glimpse(base_becas)
## Rows: 14,700
## Columns: 11
## $ id_estudiante <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14…
## $ edad <int> 20, 13, 14, 15, 13, 11, 16, 18, 15, 13, 11, 2…
## $ sexo <chr> "Mujer", "Mujer", "Mujer", "Mujer", "Mujer", …
## $ departamento <chr> "Montevideo", "Treinta y Tres", "San José", "…
## $ ciclo <chr> "Bachillerato", "Ciclo Básico", "Ciclo Básico…
## $ año_lectivo <int> 2025, 2025, 2025, 2025, 2025, 2025, 2025, 202…
## $ modalidad_financiamiento <chr> "INEFOP", "ANEP", "ANEP", "ANEP", "ANEP", "ME…
## $ fecha_inscripcion_beca <date> 2025-03-16, 2025-03-06, 2025-03-16, 2025-02-…
## $ asistencia_pct <dbl> 59.9, 69.0, 76.3, 75.4, 64.9, 84.2, 55.2, 75.…
## $ estado_trayectoria <chr> "Abandono", "Rezago", "Rezago", "Activo", "Ac…
## $ monto_mensual <dbl> 2500, 2500, 2500, 2500, 2500, 2500, 2500, 250…
Para demostrar el proceso de depuración (Producto 3), se introducen a propósito errores típicos de datos administrativos reales: duplicados, valores faltantes, formato inconsistente y errores de carga.
set.seed(99)
n_dup <- 50; n_na_sexo <- 80; n_na_depto <- 60
n_na_asist <- 100; n_formato <- 300; n_inyectado_rango <- 30
base_sucia <- bind_rows(base_becas, slice_sample(base_becas, n = n_dup))
filas <- nrow(base_sucia)
idx_na_sexo <- sample(filas, n_na_sexo)
idx_na_depto <- sample(filas, n_na_depto)
idx_na_asist <- sample(filas, n_na_asist)
idx_formato <- sample(filas, n_formato)
idx_rango <- sample(filas, n_inyectado_rango)
base_sucia <- base_sucia %>%
mutate(
sexo = if_else(row_number() %in% idx_na_sexo, NA_character_, sexo),
departamento = if_else(row_number() %in% idx_na_depto, NA_character_, departamento),
asistencia_pct = if_else(row_number() %in% idx_na_asist, NA_real_, asistencia_pct),
departamento = if_else(row_number() %in% idx_formato & !is.na(departamento),
paste0(" ", str_to_upper(departamento), " "), departamento),
asistencia_pct = if_else(row_number() %in% idx_rango & !is.na(asistencia_pct),
asistencia_pct * 10, asistencia_pct)
)
n_duplicados <- sum(duplicated(base_sucia$id_estudiante))
base_limpia <- base_sucia %>% distinct(id_estudiante, .keep_all = TRUE)
# ojo: str_to_title() no sabe español y mayusculiza conectores (ej. "Treinta Y Tres")
base_limpia <- base_limpia %>%
mutate(departamento = str_trim(departamento),
departamento = str_to_title(departamento),
departamento = str_replace_all(departamento, "\\bY\\b", "y"))
n_fuera_rango_detectado <- sum(base_limpia$asistencia_pct > 100, na.rm = TRUE)
base_limpia <- base_limpia %>%
mutate(asistencia_pct = if_else(asistencia_pct > 100, asistencia_pct / 10, asistencia_pct))
# criterio: sexo/departamento faltante -> "Sin dato" (no se descarta la fila);
# asistencia_pct faltante -> se excluye (es la variable clave para los indicadores)
base_limpia <- base_limpia %>%
mutate(
sexo = if_else(is.na(sexo), "Sin dato", sexo),
departamento = if_else(is.na(departamento), "Sin dato", departamento)
) %>%
filter(!is.na(asistencia_pct))
Resumen de la depuración:
| Paso | Cantidad |
|---|---|
| Filas originales | 14750 |
| Duplicados eliminados | 50 |
| Valores de asistencia fuera de rango corregidos | 30 |
| Filas excluidas por asistencia faltante | 100 |
| Filas finales | 14600 |
diccionario <- tribble(
~variable, ~descripcion, ~tipo, ~valores_posibles, ~criterio_depuracion,
"id_estudiante", "Identificador único del estudiante becario", "entero", "1 a 14.700", "se eliminaron duplicados por este campo",
"edad", "Edad del estudiante en años", "entero", "11 a 21", "sin depuración adicional",
"sexo", "Sexo del estudiante", "texto", "Mujer / Varón / Sin dato", "faltantes documentados como 'Sin dato'",
"departamento", "Departamento de residencia", "texto", "19 departamentos de Uruguay / Sin dato", "formato normalizado; faltantes como 'Sin dato'",
"ciclo", "Ciclo educativo al que corresponde", "texto", "Ciclo Básico / Bachillerato", "derivado de edad (<=14 años = Ciclo Básico)",
"año_lectivo", "Año lectivo de la beca", "entero", "2025", "sin depuración adicional",
"modalidad_financiamiento", "Institución que financia la beca", "texto", "INEFOP / ANEP / MEC", "sin depuración adicional",
"fecha_inscripcion_beca", "Fecha de registro de inscripción a la beca", "fecha", "01/02/2025 a 30/04/2025", "sin depuración adicional",
"estado_trayectoria", "Estado educativo del becario en el período", "texto", "Activo / Egresado / Abandono / Rezago", "sin depuración adicional",
"asistencia_pct", "Porcentaje de asistencia registrado", "numérico", "0 a 100", "valores >100 corregidos; filas sin dato excluidas",
"monto_mensual", "Monto mensual de la beca (pesos uruguayos)", "numérico", "2500 (fijo, simulación)", "sin depuración adicional"
)
kable(diccionario)
| variable | descripcion | tipo | valores_posibles | criterio_depuracion |
|---|---|---|---|---|
| id_estudiante | Identificador único del estudiante becario | entero | 1 a 14.700 | se eliminaron duplicados por este campo |
| edad | Edad del estudiante en años | entero | 11 a 21 | sin depuración adicional |
| sexo | Sexo del estudiante | texto | Mujer / Varón / Sin dato | faltantes documentados como ‘Sin dato’ |
| departamento | Departamento de residencia | texto | 19 departamentos de Uruguay / Sin dato | formato normalizado; faltantes como ‘Sin dato’ |
| ciclo | Ciclo educativo al que corresponde | texto | Ciclo Básico / Bachillerato | derivado de edad (<=14 años = Ciclo Básico) |
| año_lectivo | Año lectivo de la beca | entero | 2025 | sin depuración adicional |
| modalidad_financiamiento | Institución que financia la beca | texto | INEFOP / ANEP / MEC | sin depuración adicional |
| fecha_inscripcion_beca | Fecha de registro de inscripción a la beca | fecha | 01/02/2025 a 30/04/2025 | sin depuración adicional |
| estado_trayectoria | Estado educativo del becario en el período | texto | Activo / Egresado / Abandono / Rezago | sin depuración adicional |
| asistencia_pct | Porcentaje de asistencia registrado | numérico | 0 a 100 | valores >100 corregidos; filas sin dato excluidas |
| monto_mensual | Monto mensual de la beca (pesos uruguayos) | numérico | 2500 (fijo, simulación) | sin depuración adicional |
Se incorpora una segunda fuente de información —un padrón ficticio de
centros educativos por departamento— y se integra a la base principal
mediante left_join(), replicando el escenario real en que
la información proviene de distintos registros administrativos.
n_centros <- pmax(2, round(pesos_departamento * 40))
centros_educativos <- map2_dfr(departamentos, n_centros, function(depto, n_c) {
tibble(
departamento = depto,
nombre_centro = if_else(seq_len(n_c) %% 2 == 1,
paste0("Liceo N°", seq_len(n_c), " - ", depto),
paste0("UTU ", depto)),
tipo_centro = if_else(seq_len(n_c) %% 2 == 1, "Liceo", "UTU")
)
}) %>%
mutate(id_centro = row_number()) %>%
relocate(id_centro)
centros_por_depto <- split(centros_educativos$id_centro, centros_educativos$departamento)
base_final <- base_limpia %>%
mutate(
id_centro = sapply(departamento, function(d) {
opciones <- centros_por_depto[[d]]
if (is.null(opciones)) return(NA_integer_)
sample(opciones, 1)
})
) %>%
left_join(centros_educativos %>% select(id_centro, nombre_centro, tipo_centro),
by = "id_centro")
promedio_general <- base_final %>%
summarise(tasa = round(100 * mean(estado_trayectoria == "Abandono"), 1)) %>%
pull(tasa)
indicador_trayectoria <- base_final %>%
count(estado_trayectoria, name = "cantidad") %>%
mutate(porcentaje = round(100 * cantidad / sum(cantidad), 1))
kable(indicador_trayectoria)
| estado_trayectoria | cantidad | porcentaje |
|---|---|---|
| Abandono | 1660 | 11.4 |
| Activo | 8336 | 57.1 |
| Egresado | 2159 | 14.8 |
| Rezago | 2445 | 16.7 |
indicador_departamento <- base_final %>%
filter(departamento != "Sin dato") %>%
group_by(departamento) %>%
summarise(
becarios = n(),
tasa_abandono = round(100 * mean(estado_trayectoria == "Abandono"), 1),
.groups = "drop"
) %>%
arrange(desc(tasa_abandono))
alertas <- indicador_departamento %>% filter(tasa_abandono > promedio_general)
kable(alertas)
| departamento | becarios | tasa_abandono |
|---|---|---|
| Artigas | 244 | 24.2 |
| Treinta y Tres | 272 | 21.0 |
| Durazno | 341 | 18.5 |
| Tacuarembó | 320 | 17.8 |
| Lavalleja | 356 | 17.7 |
| Rocha | 346 | 17.6 |
| Río Negro | 366 | 17.2 |
| Rivera | 502 | 16.5 |
| Cerro Largo | 319 | 15.4 |
| Flores | 170 | 14.7 |
| Paysandú | 659 | 13.2 |
| Salto | 676 | 12.1 |
| Soriano | 319 | 11.6 |
Promedio general de abandono: 11.4%. Se identifican 13 departamentos por encima de ese umbral, encabezados por Artigas y Treinta y Tres — consistente con el supuesto de menor asistencia promedio definido para esos departamentos en la simulación.
indicador_departamento <- indicador_departamento %>%
mutate(alerta = tasa_abandono > promedio_general)
ggplot(indicador_departamento,
aes(x = reorder(departamento, tasa_abandono), y = tasa_abandono, fill = alerta)) +
geom_col() +
geom_hline(yintercept = promedio_general, linetype = "dashed", color = "gray30") +
coord_flip() +
scale_fill_manual(values = c("FALSE" = "#4C72B0", "TRUE" = "#C44E52"),
labels = c("Dentro del promedio", "Por encima del promedio"), name = NULL) +
labs(title = "Tasa de abandono por departamento",
subtitle = paste0("Línea punteada = promedio general (", promedio_general, "%)"),
x = NULL, y = "Tasa de abandono (%)") +
theme_minimal()
orden_trayectoria <- c("Egresado", "Activo", "Rezago", "Abandono")
indicador_trayectoria <- indicador_trayectoria %>%
mutate(estado_trayectoria = factor(estado_trayectoria, levels = orden_trayectoria))
ggplot(indicador_trayectoria, aes(x = estado_trayectoria, y = porcentaje, fill = estado_trayectoria)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = paste0(porcentaje, "%")), vjust = -0.5, size = 4) +
scale_fill_manual(values = c("Egresado" = "#55A868", "Activo" = "#4C72B0",
"Rezago" = "#DD8452", "Abandono" = "#C44E52")) +
labs(title = "Distribución de la trayectoria educativa de los becarios",
x = NULL, y = "Porcentaje de becarios (%)") +
ylim(0, max(indicador_trayectoria$porcentaje) * 1.15) +
theme_minimal()