Introducción

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.

1. Generación de la base

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…

2. Simulación de problemas de calidad de datos

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)
  )

3. Depuración y validación

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

4. Diccionario de datos

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

5. Enriquecimiento: centros educativos

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

6. Indicadores de trayectoria, permanencia y egreso

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.

7. Visualizaciones

Tasa de abandono por departamento

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()

Distribución de la trayectoria educativa

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()

Conclusiones y notas metodológicas

  • La base es enteramente simulada; no proviene de registros administrativos reales de INEFOP, ANEP ni MEC.
  • El volumen (14.700 registros) se calibró con la cifra de becarios 2025 publicada en los propios términos de referencia del llamado.
  • Las relaciones entre departamento, asistencia y trayectoria son supuestos metodológicos definidos para este ejercicio, con el fin de que los indicadores y alertas resultantes tengan una lectura coherente — no reflejan un patrón real verificado.
  • Este documento busca demostrar el flujo de trabajo típico solicitado por el llamado: generación y depuración de una base (Producto 3), documentación mediante diccionario de datos (Producto 4), visualización (Producto 5) e indicadores de trayectoria, permanencia y egreso (Producto 11).