Este reporte documenta de forma reproducible las operaciones
ejecutadas en la consola de R sobre el archivo
saber11_limpio.parquet. Aunque la consola original fue
denominada Consola_04_imputacion.R, en esta etapa
no se realizó imputación de valores faltantes. El
trabajo efectuado correspondió a:
Por esta razón, metodológicamente esta etapa se interpreta como separación de regímenes de evaluación, previa a la imputación y al modelado.
Se cargaron los paquetes:
arrow, para lectura y escritura de archivos
Parquet;tidyverse, para manipulación, transformación y
auditoría de datos;stringi, para mantener disponibles herramientas de
procesamiento textual.Durante la carga se informaron conflictos normales de nombres de
funciones, como lubridate::duration() frente a
arrow::duration(), y
dplyr::filter()/dplyr::lag() frente a
funciones de stats.
Los paquetes necesarios se cargaron correctamente. Los mensajes de enmascaramiento observados son informativos y no constituyeron errores de ejecución.
Se definieron las rutas:
ruta_processed <- "data/processed"
ruta_auditorias <- "resultados/auditorias"
archivo_entrada <- file.path(ruta_processed, "saber11_limpio.parquet")
Posteriormente se comprobó la existencia física de
saber11_limpio.parquet mediante file.exists().
La ejecución continuó sin activar stop(), lo que confirmó
que el archivo estaba disponible.
El dataset limpio generado en la etapa anterior estaba correctamente disponible y pudo utilizarse como fuente certificada para esta fase.
El archivo fue cargado con:
saber11 <- arrow::read_parquet(archivo_entrada)
Resultados:
| Indicador | Resultado |
|---|---|
| Filas | 7.109.704 |
| Columnas | 83 |
La base de entrada contiene 7.109.704 observaciones y 83 variables. No se detectó pérdida de información durante la carga.
Se recorrieron las 83 columnas para registrar:
La auditoría mostró una estructura heterogénea compuesta
principalmente por variables character, además de variables
numeric, integer, logical y
Date.
Entre los resultados relevantes:
periodo se conservó como character;anio es integer;periodo_valido es logical;Date;numeric;character para trazabilidad.La estructura de tipos definida durante la limpieza se conservó. La coexistencia de variables originales y versiones analíticas permite mantener trazabilidad sin sacrificar una representación adecuada para análisis posteriores.
auditar_nulos()Se creó una función reutilizable que, para cada variable, calculó:
NA;El resultado se ordenó de mayor a menor cantidad de nulos.
Resultados:
| Indicador | Resultado |
|---|---|
| Variables auditadas | 83 |
| Variables con al menos un nulo | 59 |
| Variables sin nulos | 24 |
La auditoría cubrió las 83 variables y todas presentaron la longitud esperada de 7.109.704 registros.
La auditoría fue estructuralmente válida. Sin embargo, que 59
variables contengan algún NA no significa que 59 variables
deban ser imputadas. Es necesario distinguir faltantes reales,
faltantes estructurales, códigos especiales, identificadores y campos
que requieren otro tratamiento.
Antes de separar los regímenes, los mayores porcentajes de faltantes fueron:
| Variable o grupo | Nulos | Porcentaje |
|---|---|---|
punt_c_naturales y versión numérica |
2.609.523 | 36,7 % |
punt_global y versión numérica |
2.609.523 | 36,7 % |
punt_lectura_critica y versión
numérica |
2.609.523 | 36,7 % |
punt_sociales_ciudadanas y versión
numérica |
2.609.523 | 36,7 % |
cole_bilingue |
916.870 | 12,9 % |
fami_estratovivienda |
218.219 | 3,07 % |
fami_educacionmadre |
212.826 | 2,99 % |
fami_educacionpadre |
212.423 | 2,99 % |
fami_tieneinternet |
191.624 | 2,70 % |
fami_tienecomputador |
151.217 | 2,13 % |
fami_tieneautomovil |
134.769 | 1,90 % |
fami_cuartoshogar |
128.762 | 1,81 % |
fami_personashogar |
128.228 | 1,80 % |
fami_tienelavadora |
128.086 | 1,80 % |
cole_caracter |
110.212 | 1,55 % |
La presencia exacta de 2.609.523 nulos en cada uno de los cuatro puntajes modernos, tanto en su versión original como numérica, constituyó una señal de que no se trataba de ausencia aleatoria. El patrón correspondía al régimen histórico anterior a la introducción de esas variables.
La auditoría global permitió detectar que una parte sustancial de los
NA era consecuencia de un cambio en el diseño de la
evaluación, no de un problema que debiera resolverse mediante
imputación estadística.
Se estableció como punto de corte:
periodo_inicio_evaluacion_nueva <- "20142"
Se construyeron dos datasets:
saber11_evaluacion_antigua <- saber11 %>%
dplyr::filter(periodo < periodo_inicio_evaluacion_nueva)
saber11_evaluacion_nueva <- saber11 %>%
dplyr::filter(periodo >= periodo_inicio_evaluacion_nueva)
Resultados:
| Dataset | Registros |
|---|---|
| Dataset completo | 7.109.704 |
| Evaluación antigua | 2.609.523 |
| Evaluación nueva | 4.500.181 |
| Suma de ambos regímenes | 7.109.704 |
La suma:
2.609.523 + 4.500.181 = 7.109.704
coincide exactamente con el número de registros del dataset original.
La partición fue exhaustiva y sin pérdida de registros. Todos los casos quedaron asignados a uno de los dos regímenes.
El régimen antiguo contiene 2.609.523 registros.
El hallazgo más importante fue que las siguientes variables presentaron 100 % de valores faltantes:
punt_c_naturales;punt_c_naturales_num;punt_global;punt_global_num;punt_lectura_critica;punt_lectura_critica_num;punt_sociales_ciudadanas;punt_sociales_ciudadanas_num.Cada una presentó exactamente 2.609.523 nulos (100 %).
Otros faltantes relevantes fueron:
| Variable | Nulos | Porcentaje |
|---|---|---|
cole_bilingue |
249.448 | 9,56 % |
fami_educacionmadre |
53.904 | 2,07 % |
fami_educacionpadre |
53.409 | 2,05 % |
fami_tienecomputador |
51.160 | 1,96 % |
fami_estratovivienda |
39.153 | 1,50 % |
| Variables de residencia | 37.541 | 1,44 % |
cole_codigo_icfes |
23.731 | 0,909 % |
cole_caracter |
14.433 | 0,553 % |
Los ocho campos asociados a los cuatro puntajes modernos están
completamente ausentes en el régimen antiguo. Esto demuestra que sus
NA son estructurales y no deben ser
imputados.
El régimen antiguo presenta una estructura de evaluación diferente e incompatible de forma directa con el régimen moderno para esas variables de desempeño.
El régimen nuevo contiene 4.500.181 registros.
Los principales faltantes fueron:
| Ranking | Variable | Nulos | Porcentaje |
|---|---|---|---|
| 1 | cole_bilingue |
667.422 | 14,8 % |
| 2 | fami_estratovivienda |
179.066 | 3,98 % |
| 3 | fami_tieneinternet |
162.321 | 3,61 % |
| 4 | fami_educacionpadre |
159.014 | 3,53 % |
| 5 | fami_educacionmadre |
158.922 | 3,53 % |
| 6 | fami_tieneautomovil |
105.853 | 2,35 % |
| 7 | fami_cuartoshogar |
100.445 | 2,23 % |
| 8 | fami_tienecomputador |
100.057 | 2,22 % |
| 9 | fami_personashogar |
99.292 | 2,21 % |
| 10 | fami_tienelavadora |
99.172 | 2,20 % |
| 11 | cole_caracter |
95.779 | 2,13 % |
Otros faltantes presentan proporciones reducidas:
estu_fechanacimiento_fecha_valida: 5.480 (0,122
%);punt_ingles, punt_ingles_num y
punt_ingles_num_valido: 4.179 (0,0929 %);desemp_ingles: 4.105 (0,0912 %);estu_genero: 3.229 (0,0718 %);cole_cod_dane_establecimiento: 164 (0,0036 %);En el régimen nuevo, los puntajes:
ya no presentan los 2.609.523 faltantes estructurales observados en la base completa. En la comparación por regímenes pasan de 100 % de nulos en el régimen antiguo a 0 % en el régimen nuevo.
El régimen nuevo constituye una base estructuralmente más homogénea para el análisis predictivo moderno. Los principales problemas de ausencia se concentran ahora en variables socioeconómicas e institucionales, y no en los cuatro puntajes modernos.
La comparación confirmó cambios importantes en los patrones de ausencia.
Entre los incrementos de faltantes en el régimen nuevo destacan:
| Variable | Antigua | Nueva | Diferencia aproximada |
|---|---|---|---|
cole_bilingue |
9,56 % | 14,8 % | +5,27 pp |
fami_estratovivienda |
1,50 % | 3,98 % | +2,48 pp |
fami_tieneinternet |
1,12 % | 3,61 % | +2,48 pp |
fami_educacionpadre |
2,05 % | 3,53 % | +1,49 pp |
fami_educacionmadre |
2,07 % | 3,53 % | +1,47 pp |
cole_caracter |
0,553 % | 2,13 % | +1,58 pp |
También se observaron mejoras claras en otras variables. Por ejemplo,
las variables de residencia disminuyeron aproximadamente de 1,44
% a 0,0456 %, y cole_codigo_icfes pasó de 23.731
faltantes en el régimen antiguo a solamente 2 en el nuevo.
El mecanismo de ausencia no es estable a lo largo del tiempo. Esto refuerza la decisión de no aplicar indiscriminadamente un único procedimiento de imputación a toda la serie histórica.
Los rangos obtenidos fueron:
| Régimen | Periodos | Años calendario |
|---|---|---|
| Evaluación antigua | 20101–20141 | 2010–2014 |
| Evaluación nueva | 20142–20224 | 2014–2022 |
El año 2014 aparece en ambos regímenes porque el cambio se produce
entre los periodos 20141 y 20142.
Para identificar inequívocamente los archivos no basta con usar los años calendario. Incluir los periodos exactos en los nombres de los archivos evita confundir el primer y segundo periodo de 2014.
Se generaron los siguientes archivos:
data/processed/saber11_evaluacion_antigua_2010_2014_periodos_20101_20141.parquet
data/processed/saber11_evaluacion_nueva_2014_2022_periodos_20142_20224.parquet
Los dos datasets fueron guardados con
arrow::write_parquet().
Se utilizó file.exists() para comprobar la existencia de
ambos archivos. Ninguna de las validaciones activó
stop().
Resultados:
| Dataset | Registros | Tamaño |
|---|---|---|
| Evaluación antigua | 2.609.523 | 0,215 GB |
| Evaluación nueva | 4.500.181 | 0,403 GB |
La consola mostró explícitamente:
GUARDADO DE DATASETS CONFIRMADO
La separación no quedó únicamente en memoria de R: los dos
datasets fueron materializados correctamente como archivos Parquet en
data/processed.
Después de revisar los resultados se tomó la decisión de:
Archivar el régimen antiguo y continuar la imputación y el modelado predictivo exclusivamente con el régimen nuevo.
Por tanto, la base principal de trabajo para las siguientes etapas será:
data/processed/saber11_evaluacion_nueva_2014_2022_periodos_20142_20224.parquet
con:
La base antigua se conserva como evidencia histórica y respaldo, pero no será utilizada en el pipeline predictivo principal.
NA y 24 variables
están completas.cole_bilingue y en variables socioeconómicas del
hogar.01_descarga_datos.R
↓
02_auditoria_datos.R
↓
03_limpieza_estandarizacion.R
↓
04_separacion_regimenes_evaluacion.R
│
├── Régimen antiguo 20101–20141 → ARCHIVADO
│
└── Régimen nuevo 20142–20224 → BASE DE TRABAJO
↓
05_imputacion.R
↓
06_construccion_panel.R
↓
07_feature_engineering.R
↓
08_dataset_supervisado.R
↓
09_modelado_prediccion.R
La siguiente fase se desarrollará sobre el archivo
saber11_evaluacion_nueva_2014_2022_periodos_20142_20224.parquet.
En esta etapa se clasificarán los valores faltantes del régimen nuevo según su tratamiento metodológico:
Una vez diferenciados los regímenes de evaluación, se definió como base de trabajo para las etapas posteriores el conjunto correspondiente al régimen nuevo de Saber 11, comprendido entre los períodos 20142 y 20224.
El archivo consolidado del régimen nuevo contiene 4.500.181 registros y 83 variables. Antes de realizar cualquier transformación adicional se verificó su existencia, dimensiones y cobertura temporal.
# DEFINIR LA RAÍZ DEL PROYECTO
ruta_proyecto <- "D:/Proyectos_IA/prueba_saber_11" # Define explícitamente la carpeta raíz del proyecto
# DEFINIR LA RUTA DEL DATASET DEL RÉGIMEN NUEVO
archivo_evaluacion_nueva <- file.path(
ruta_proyecto,
"data",
"processed",
"saber11_evaluacion_nueva_2014_2022_periodos_20142_20224.parquet"
) # Construye la ruta absoluta del archivo Parquet correspondiente al régimen nuevo
# VALIDAR LA EXISTENCIA DEL ARCHIVO
if (!file.exists(archivo_evaluacion_nueva)) {
stop("ERROR: no se encontró el archivo del régimen nuevo en la ruta esperada.")
} # Detiene la ejecución si el archivo no existe
# CARGAR EL DATASET
saber11_nueva <- arrow::read_parquet(
archivo_evaluacion_nueva
) # Carga exclusivamente el dataset correspondiente al régimen nuevo
# VALIDAR LAS DIMENSIONES Y COBERTURA TEMPORAL
cat("\nVALIDACIÓN DEL RÉGIMEN NUEVO\n")
##
## VALIDACIÓN DEL RÉGIMEN NUEVO
cat("Archivo encontrado:", file.exists(archivo_evaluacion_nueva), "\n")
## Archivo encontrado: TRUE
cat("Registros:", nrow(saber11_nueva), "\n")
## Registros: 4500181
cat("Variables:", ncol(saber11_nueva), "\n")
## Variables: 83
cat("Periodo inicial:", min(saber11_nueva$periodo, na.rm = TRUE), "\n")
## Periodo inicial: 20142
cat("Periodo final:", max(saber11_nueva$periodo, na.rm = TRUE), "\n")
## Periodo final: 20224
Debido al elevado volumen de información y a la heterogeneidad temporal observada durante la auditoría, se decidió dividir físicamente el régimen nuevo según el período de aplicación del examen.
Esta decisión responde a dos criterios complementarios.
Criterio metodológico. El período constituye una unidad temporal natural dentro de la base Saber 11. Mantener cada período completo evita introducir particiones arbitrarias determinadas exclusivamente por un número fijo de observaciones y permite conservar conjuntamente los registros generados bajo una misma aplicación del examen.
Criterio computacional. El procesamiento mediante archivos independientes reduce la cantidad de información que debe mantenerse simultáneamente en memoria durante las etapas posteriores, particularmente durante la imputación de valores faltantes. Esto permite cargar, procesar, validar, guardar y liberar cada período de manera independiente.
La partición no altera los valores originales ni realiza imputaciones. Su función consiste únicamente en reorganizar físicamente el conjunto de datos para facilitar las siguientes etapas del pipeline.
# CONSTRUIR LA DISTRIBUCIÓN DE REGISTROS POR PERIODO
auditoria_periodos_nueva <- saber11_nueva |>
dplyr::count(
periodo,
name = "registros"
) |>
dplyr::arrange(periodo) # Cuenta los registros disponibles en cada periodo
## Warning: Can't find generic `filter_out` in package dplyr to register S3 method.
## ℹ This message is only shown to developers using devtools.
## ℹ Do you need to update dplyr to the latest version?
## Can't find generic `filter_out` in package dplyr to register S3 method.
## ℹ This message is only shown to developers using devtools.
## ℹ Do you need to update dplyr to the latest version?
## Can't find generic `filter_out` in package dplyr to register S3 method.
## ℹ This message is only shown to developers using devtools.
## ℹ Do you need to update dplyr to the latest version?
## Can't find generic `filter_out` in package dplyr to register S3 method.
## ℹ This message is only shown to developers using devtools.
## ℹ Do you need to update dplyr to the latest version?
# AGREGAR EL TOTAL GENERAL
tabla_periodos_reporte <- auditoria_periodos_nueva |>
dplyr::mutate(
periodo = as.character(periodo)
) |>
dplyr::bind_rows(
tibble::tibble(
periodo = "Total",
registros = sum(auditoria_periodos_nueva$registros)
)
) # Incorpora una fila final con el total de registros
# VALIDAR EL NÚMERO DE PERIODOS
numero_periodos <- nrow(auditoria_periodos_nueva) # Calcula el número de periodos identificados antes de agregar la fila Total
cat("Número de periodos identificados:", numero_periodos, "\n")
## Número de periodos identificados: 14
cat("Total de registros:", sum(auditoria_periodos_nueva$registros), "\n")
## Total de registros: 4500181
cat("Coincide con el dataset:", sum(auditoria_periodos_nueva$registros) == nrow(saber11_nueva), "\n")
## Coincide con el dataset: TRUE
# MOSTRAR LA TABLA EN EL INFORME
knitr::kable(
tabla_periodos_reporte,
col.names = c("Período", "Número de registros"),
caption = "Distribución de registros del régimen nuevo de Saber 11 por período.",
align = c("c", "r"),
format.args = list(
big.mark = ".",
decimal.mark = ","
)
)
| Período | Número de registros |
|---|---|
| 20142 | 546.727 |
| 20151 | 25.973 |
| 20152 | 544.491 |
| 20161 | 13.095 |
| 20162 | 550.275 |
| 20171 | 13.018 |
| 20172 | 548.269 |
| 20181 | 32.348 |
| 20191 | 12.561 |
| 20194 | 1.096.524 |
| 20201 | 15.435 |
| 20211 | 15.528 |
| 20221 | 20.049 |
| 20224 | 1.065.888 |
| Total | 4.500.181 |
La distribución temporal evidencia diferencias importantes en el tamaño de los períodos. Los períodos 20194 y 20224 superan el millón de registros, mientras que otros períodos contienen volúmenes considerablemente menores.
Esta heterogeneidad refuerza la conveniencia de mantener el período como unidad explícita de procesamiento, en lugar de dividir el conjunto mediante bloques artificiales definidos únicamente por número de filas.
Cada período fue almacenado como un archivo Parquet independiente dentro del directorio:
data/processed/saber11_evaluacion_nueva_por_periodo/
La estructura generada fue:
saber11_evaluacion_nueva_por_periodo/
├── saber11_periodo_20142.parquet
├── saber11_periodo_20151.parquet
├── saber11_periodo_20152.parquet
├── saber11_periodo_20161.parquet
├── saber11_periodo_20162.parquet
├── saber11_periodo_20171.parquet
├── saber11_periodo_20172.parquet
├── saber11_periodo_20181.parquet
├── saber11_periodo_20191.parquet
├── saber11_periodo_20194.parquet
├── saber11_periodo_20201.parquet
├── saber11_periodo_20211.parquet
├── saber11_periodo_20221.parquet
└── saber11_periodo_20224.parquet
La utilización del formato Parquet permite conservar la estructura tabular de los datos mediante almacenamiento eficiente y facilita la lectura selectiva de los períodos requeridos durante las siguientes etapas del procesamiento.
La separación temporal fue sometida a controles de integridad antes de continuar con el pipeline.
Se verificó que:
# DEFINIR EL DIRECTORIO DE LOS ARCHIVOS SEPARADOS POR PERIODO
ruta_periodos_nueva <- file.path(
ruta_proyecto,
"data",
"processed",
"saber11_evaluacion_nueva_por_periodo"
) # Define la ubicación física de los archivos separados por periodo
# VALIDAR LA EXISTENCIA DEL DIRECTORIO
if (!dir.exists(ruta_periodos_nueva)) {
stop("ERROR: no existe el directorio de archivos separados por periodo.")
} # Detiene el reporte si el directorio no se encuentra
# IDENTIFICAR LOS ARCHIVOS PARQUET
archivos_periodos_creados <- list.files(
ruta_periodos_nueva,
pattern = "^saber11_periodo_[0-9]+\\.parquet$",
full.names = TRUE
) # Recupera exclusivamente los archivos Parquet correspondientes a los periodos
# VALIDAR LA CANTIDAD DE ARCHIVOS
if (length(archivos_periodos_creados) != 14) {
stop(
paste0(
"ERROR: se esperaban 14 archivos por periodo, pero se encontraron ",
length(archivos_periodos_creados),
"."
)
)
} # Comprueba que estén disponibles exactamente los catorce archivos
# CONTAR LOS REGISTROS DIRECTAMENTE DESDE CADA ARCHIVO
registros_archivos_periodos <- purrr::map_dbl(
archivos_periodos_creados,
function(archivo_actual) {
arrow::open_dataset(archivo_actual) |>
dplyr::summarise(
registros = dplyr::n()
) |>
dplyr::collect() |>
dplyr::pull(registros)
}
) # Cuenta los registros almacenados físicamente en cada archivo Parquet
# CALCULAR EL TOTAL DE REGISTROS ALMACENADOS
total_registros_archivos <- sum(registros_archivos_periodos) # Suma los registros de los catorce archivos
# VALIDAR LA INTEGRIDAD
integridad_confirmada <- total_registros_archivos == nrow(saber11_nueva) # Comprueba que no exista pérdida de registros
# MOSTRAR LOS RESULTADOS DE LA VALIDACIÓN
cat("\nVALIDACIÓN FINAL DE INTEGRIDAD\n")
##
## VALIDACIÓN FINAL DE INTEGRIDAD
cat("Archivos por periodo encontrados:", length(archivos_periodos_creados), "\n")
## Archivos por periodo encontrados: 14
cat("Registros dataset consolidado:", nrow(saber11_nueva), "\n")
## Registros dataset consolidado: 4500181
cat("Registros archivos por periodo:", total_registros_archivos, "\n")
## Registros archivos por periodo: 4500181
cat("Integridad confirmada:", integridad_confirmada, "\n")
## Integridad confirmada: TRUE
El procedimiento debe producir una validación equivalente a:
VALIDACIÓN FINAL DE INTEGRIDAD
Archivos por periodo encontrados: 14
Registros dataset consolidado: 4500181
Registros archivos por periodo: 4500181
Integridad confirmada: TRUE
El resultado confirma que la partición temporal conserva exactamente el número de observaciones del dataset consolidado. Por tanto, no se identificó pérdida de registros como consecuencia del procedimiento de separación.
A partir de esta organización, la etapa de imputación se desarrollará mediante un esquema de procesamiento secuencial por período:
Dataset régimen nuevo
│
↓
4.500.181 registros
│
↓
Separación por período
│
├── 20142
├── 20151
├── 20152
├── ...
└── 20224
│
↓
Procesamiento individual
│
↓
Imputación y validación
│
↓
Archivo procesado
│
↓
Liberación de memoria
Esta arquitectura reduce la cantidad de información que debe mantenerse simultáneamente en memoria y conserva explícitamente la estructura temporal del conjunto de datos.
La separación por período debe interpretarse como una estrategia de organización computacional y temporal, no como una transformación estadística de los valores originales.
Las decisiones relacionadas con las variables imputables, los métodos
de imputación, los predictores y los mecanismos de validación serán
desarrolladas específicamente en el script
05_imputacion.R.