Este informe responde una sola pregunta a lo largo de todo el analisis:
La educacion de la madre, ¿aporta mas al puntaje de Saber 11 que la educacion del padre? Y si es asi, ¿esa ventaja es real o es solo una apariencia estadistica?
Para responderla con seriedad no basta una regresion. Se construye una escalera de modelos: cada peldano es mas exigente que el anterior y descarta una explicacion alternativa distinta. Si la ventaja materna sobrevive a todos los peldanos, la conclusion es solida.
Cada seccion sigue la misma estructura para que se entienda de una sola lectura:
La variable central es la brecha madre menos padre: cuantos puntos mas (o menos) suma un anio de escolaridad de la madre frente a uno del padre. Si la brecha es positiva y significativa, hay ventaja materna.
Que se hace aqui. Se cargan los paquetes, se fija una semilla aleatoria (para que cualquiera reproduzca exactamente el resultado) y se ajusta la configuracion de memoria. Este paso no produce resultados sustantivos: es la base tecnica.
suppressPackageStartupMessages({
library(data.table); library(fixest); library(stringi)
library(ggplot2); library(openxlsx)
})
# Un solo hilo minimiza el pico de memoria. Con 16 GB o mas se puede subir a 2.
setFixest_nthreads(1)
setFixest_notes(FALSE)
SEMILLA <- 20260728
set.seed(SEMILLA)
TABLAS <- list()
AZUL <- "#2C6FBB"; NARANJA <- "#E8862A"; GRIS <- "#7F7F7F"; ROJO <- "#C0392B"
ENTORNO <- data.table(
Parametro = c("Fecha","R","Plataforma","data.table","fixest","Semilla"),
Valor = c(format(Sys.time(), "%Y-%m-%d %H:%M"), R.version.string,
Sys.info()[["sysname"]], as.character(packageVersion("data.table")),
as.character(packageVersion("fixest")), as.character(SEMILLA)))
tabla(ENTORNO, "Entorno de ejecucion y version de los paquetes")| Parametro | Valor |
|---|---|
| Fecha | 2026-07-31 21:26 |
| R | R version 4.6.1 (2026-06-24 ucrt) |
| Plataforma | Windows |
| data.table | 1.18.4 |
| fixest | 0.14.2 |
| Semilla | 20260728 |
El problema. El ICFES no reporta “anios de estudio” sino categorias de texto (“PRIMARIA COMPLETA”, “POSTGRADO”). Para tratar la educacion como una variable numerica hay que traducir cada categoria a un numero de anios.
La solucion. Se usa la equivalencia de la Clasificacion Internacional Normalizada de la Educacion (ISCED, UNESCO) adaptada a Colombia. Es un estandar internacional, lo que hace la decision transparente y defendible ante un evaluador.
MAPA_PRINCIPAL <- c(
"NINGUNO" = 0, "PRIMARIA INCOMPLETA" = 3, "PRIMARIA COMPLETA" = 5,
"SECUNDARIA (BACHILLERATO) INCOMPLETA" = 8, "SECUNDARIA (BACHILLERATO) COMPLETA" = 11,
"TECNICA O TECNOLOGICA INCOMPLETA" = 12, "TECNICA O TECNOLOGICA COMPLETA" = 14,
"EDUCACION PROFESIONAL INCOMPLETA" = 13, "EDUCACION PROFESIONAL COMPLETA" = 16,
"POSTGRADO" = 18)
NO_INFORMATIVAS <- c("NO SABE","NO APLICA","SIN INFORMACION","SIN_INFORMACION","")
# Tabla legible de la equivalencia usada
mapa_dt <- data.table(
`Categoria ICFES` = names(MAPA_PRINCIPAL),
`Anios asignados` = as.integer(MAPA_PRINCIPAL),
`Nivel ISCED` = c("0","1 parcial","1","2","3","4 parcial","5","6 parcial","6","7 y 8"))
tabla(mapa_dt, "Traduccion de categorias educativas a anios de escolaridad (ISCED-UNESCO)")| Categoria ICFES | Anios asignados | Nivel ISCED |
|---|---|---|
| NINGUNO | 0 | 0 |
| PRIMARIA INCOMPLETA | 3 | 1 parcial |
| PRIMARIA COMPLETA | 5 | 1 |
| SECUNDARIA (BACHILLERATO) INCOMPLETA | 8 | 2 |
| SECUNDARIA (BACHILLERATO) COMPLETA | 11 | 3 |
| TECNICA O TECNOLOGICA INCOMPLETA | 12 | 4 parcial |
| TECNICA O TECNOLOGICA COMPLETA | 14 | 5 |
| EDUCACION PROFESIONAL INCOMPLETA | 13 | 6 parcial |
| EDUCACION PROFESIONAL COMPLETA | 16 | 6 |
| POSTGRADO | 18 | 7 y 8 |
Que se hace. El codigo busca automaticamente los
archivos de Saber 11 en la carpeta indicada. Es el unico lugar que debes
editar: la ruta CARPETA_DATOS.
CARPETA_DATOS <- "C:/Users/andres.f.penaranda/Downloads/OneDrive_1_7-25-2026"
CARPETA_SALIDA <- CARPETA_DATOS
if (!dir.exists(CARPETA_DATOS)) stop("La carpeta no existe. Edita la ruta en el Paso 1.")
archivos <- list.files(CARPETA_DATOS, pattern = "\\.(csv|txt|tsv)$",
recursive = TRUE, full.names = TRUE, ignore.case = TRUE)
if (length(archivos) == 0) stop("No se encontraron archivos de datos.")
inventario <- data.table(Archivo = basename(archivos),
MB = round(file.info(archivos)$size / 1e6, 1))
TABLAS[["A_inventario"]] <- inventario
CACHE <- file.path(CARPETA_SALIDA, "_saber11_consolidado.rds"); RECONSTRUIR_CACHE <- FALSE
tabla(inventario, "Archivos de Saber 11 localizados")| Archivo | MB |
|---|---|
| bitacora_ejecucion.txt | 0.0 |
| Examen_Saber_11_20142.txt | 249.4 |
| Examen_Saber_11_20151.txt | 35.0 |
| Examen_Saber_11_20152.txt | 256.1 |
| Examen_Saber_11_20161.txt | 26.5 |
| Examen_Saber_11_20162.txt | 287.3 |
| Examen_Saber_11_20171.txt | 29.8 |
| Examen_Saber_11_20172.txt | 415.4 |
| Examen_Saber_11_20181.txt | 25.2 |
| Examen_Saber_11_20182.txt | 422.5 |
| Examen_Saber_11_20191.txt | 26.3 |
| Examen_Saber_11_20192.txt | 427.3 |
| Examen_Saber_11_20201.txt | 18.3 |
| Examen_Saber_11_20202.txt | 399.8 |
| Examen_Saber_11_20211.txt | 21.4 |
| Examen_Saber_11_20212.txt | 419.8 |
| Examen_Saber_11_20221.txt | 27.1 |
| Examen_Saber_11_20222.txt | 406.1 |
| Examen_Saber_11_20231.txt | 28.1 |
| Examen_Saber_11_20232.txt | 418.1 |
| Examen_Saber_11_20241.txt | 30.3 |
| Examen_Saber_11_20242.txt | 410.1 |
Conclusion. Se localizan las 21 aplicaciones del examen (dos por anio, salvo casos especiales), desde 2014 hasta 2024. Cada aplicacion es un archivo independiente que a continuacion se unifica.
Que se hace. Se apilan las 21 aplicaciones en una
sola tabla. Se guarda una copia en formato .rds (cache)
para que las siguientes ejecuciones sean casi instantaneas.
if (!RECONSTRUIR_CACHE && file.exists(CACHE)) {
df <- readRDS(CACHE)
} else {
partes <- list()
for (ruta in archivos) {
out <- tryCatch(leer_archivo(ruta), error = function(e) e)
if (!inherits(out, "error")) partes[[length(partes)+1L]] <- out$d
}
df <- rbindlist(partes, use.names = TRUE, fill = TRUE); rm(partes); gc()
saveRDS(df, CACHE)
}
tabla(data.table(Concepto = "Total bruto consolidado",
Registros = format(nrow(df), big.mark = ",")),
"Volumen total de la base antes de depurar")| Concepto | Registros |
|---|---|
| Total bruto consolidado | 7,239,505 |
Conclusion. La base bruta reune mas de siete millones de registros de estudiantes. Es el universo completo antes de aplicar cualquier filtro de calidad.
Que se hace. Antes de tocar los datos, se revisa cada aplicacion: cuantos estudiantes tiene, cual es el puntaje promedio, minimo y maximo.
Por que es pertinente. Permite detectar archivos corruptos, truncados o con una escala distinta, comparando contra las cifras oficiales del ICFES. Es un control de calidad previo, no un analisis.
pp <- parsear_periodo(df$PERIODO)
df[, ANIO := pp$anio]; df[, APLICACION := pp$apl]
df <- df[!is.na(ANIO)]; df[, ANIO := as.integer(ANIO)]
df[, CALENDARIO := normaliza_serie(CALENDARIO)]
df[is.na(CALENDARIO) | CALENDARIO == "", CALENDARIO := "SIN INFORMACION"]
df[, PUNTAJE := as.numeric(PUNTAJE)]
aud <- df[, .(Registros = .N, `Puntaje medio` = round(mean(PUNTAJE, na.rm=TRUE),1),
Min = suppressWarnings(min(PUNTAJE,na.rm=TRUE)),
Max = suppressWarnings(max(PUNTAJE,na.rm=TRUE))),
by = .(ANIO, APLICACION)][order(ANIO, APLICACION)]
AUD_BRUTO <- df[, .(registros = .N), by = .(ANIO, APLICACION)]
TABLAS[["A1_auditoria"]] <- aud
tabla(aud, "Auditoria por anio y aplicacion (antes de filtrar)")| ANIO | APLICACION | Registros | Puntaje medio | Min | Max |
|---|---|---|---|---|---|
| 2,014 | 2 | 574,259 | 249.9 | 0 | 482 |
| 2,015 | 1 | 108,259 | 259.4 | 0 | 488 |
| 2,015 | 2 | 574,158 | 250.2 | 0 | 492 |
| 2,016 | 1 | 74,224 | 274.9 | 0 | 494 |
| 2,016 | 2 | 589,593 | 258.4 | 0 | 468 |
| 2,017 | 1 | 85,149 | 265.4 | 10 | 476 |
| 2,017 | 2 | 590,996 | 255.7 | 0 | 476 |
| 2,018 | 1 | 65,854 | 279.4 | 0 | 475 |
| 2,018 | 2 | 609,136 | 251.2 | 0 | 478 |
| 2,019 | 1 | 66,104 | 271.9 | 9 | 470 |
| 2,019 | 2 | 614,789 | 246.2 | 0 | 477 |
| 2,020 | 1 | 46,221 | 274.6 | 0 | 492 |
| 2,020 | 2 | 556,891 | 248.4 | 0 | 500 |
| 2,021 | 1 | 58,708 | 274.6 | 14 | 495 |
| 2,021 | 2 | 606,030 | 246.9 | 0 | 500 |
| 2,022 | 1 | 73,795 | 267.9 | 82 | 500 |
| 2,022 | 2 | 589,183 | 250.4 | 0 | 500 |
| 2,023 | 1 | 77,555 | 262.7 | 0 | 478 |
| 2,023 | 2 | 602,093 | 252.1 | 0 | 500 |
| 2,024 | 1 | 84,072 | 265.6 | 0 | 496 |
| 2,024 | 2 | 592,436 | 253.9 | 0 | 495 |
Conclusion. Todas las aplicaciones lucen plausibles: los promedios se ubican entre 246 y 279 puntos y los maximos no superan la escala 0 a 500 de Saber 11. No hay senales de archivos truncados ni de escalas distintas. Los datos son confiables para el analisis.
Que se hace. Se aplican filtros de calidad y se registra cuantos estudiantes se pierden en cada uno (un “diario de perdidas”).
Por que es pertinente. La transparencia exige justificar cada exclusion. El filtro mas importante es eliminar registros sin codigo de colegio, porque ese codigo es indispensable para dos tecnicas centrales: agrupar los errores por colegio y estimar los efectos fijos.
diario <- list()
anotar <- function(p, a, d) diario[[length(diario)+1L]] <<- data.table(
Filtro = p, `N antes` = a, `N despues` = d, Perdidos = a-d,
`%` = round(100*(a-d)/max(a,1),3))
CATS <- c("EDU_MADRE","EDU_PADRE","ESTRATO","INTERNET","COMPUTADOR",
"NATURALEZA","ZONA","JORNADA","SEXO","DEPTO")
for (col in CATS) {
set(df, j = col, value = normaliza_serie(df[[col]]))
df[is.na(get(col)) | get(col) %in% c("","NA","NAN","NULL"), (col) := "SIN INFORMACION"]; gc()
}
df[ZONA == "URBANA", ZONA := "URBANO"]
n<-nrow(df); df<-df[!is.na(PUNTAJE)]; anotar("Puntaje nulo", n, nrow(df))
n<-nrow(df); df<-df[PUNTAJE>0]; anotar("Puntaje menor o igual a 0", n, nrow(df))
n<-nrow(df); df<-df[PUNTAJE<=500]; anotar("Puntaje mayor a 500", n, nrow(df))
n<-nrow(df); df<-df[!is.na(COLEGIO)&COLEGIO!=""]; anotar("Sin codigo de colegio", n, nrow(df))
TABLAS[["A2_diario_perdidas"]] <- rbindlist(diario)
tabla(TABLAS[["A2_diario_perdidas"]], "Diario de perdidas: cuantos registros elimina cada filtro")| Filtro | N antes | N despues | Perdidos | % |
|---|---|---|---|---|
| Puntaje nulo | 7,239,505 | 7,239,505 | 0 | 0.000 |
| Puntaje menor o igual a 0 | 7,239,505 | 7,239,253 | 252 | 0.003 |
| Puntaje mayor a 500 | 7,239,253 | 7,239,253 | 0 | 0.000 |
| Sin codigo de colegio | 7,239,253 | 6,230,935 | 1,008,318 | 13.928 |
Conclusion. Tras la limpieza quedan 6.230.935 estudiantes. La unica perdida relevante es la de registros sin codigo de colegio (cerca del 14 por ciento). Los filtros de puntaje eliminan una fraccion despreciable, lo que confirma la buena calidad del dato original.
Que se hace. Se calcula, por anio, que porcentaje de estudiantes no informa el nivel educativo de cada progenitor.
Por que es pertinente. Es una posible explicacion alternativa. Si el nivel del padre se desconoce mas que el de la madre, parte de la brecha podria deberse a datos faltantes y no a un efecto real. Conviene cuantificarlo para declararlo como limitacion.
Hipotesis nula. La no respuesta es igual para madre y padre.
tasa_ni <- function(col) df[, .(pct = 100*mean(get(col) %in% NO_INFORMATIVAS)), by=ANIO][order(ANIO)]
nm<-tasa_ni("EDU_MADRE"); np<-tasa_ni("EDU_PADRE")
nr <- data.table(Anio = nm$ANIO,
`% no informa MADRE` = round(nm$pct,2),
`% no informa PADRE` = round(np$pct,2),
`Brecha (padre - madre)` = round(np$pct-nm$pct,2))
TABLAS[["A3_no_respuesta"]] <- nr
tabla(nr, "No respuesta del nivel educativo por progenitor")| Anio | % no informa MADRE | % no informa PADRE | Brecha (padre - madre) |
|---|---|---|---|
| 2,014 | 1.87 | 4.44 | 2.57 |
| 2,015 | 1.67 | 4.11 | 2.44 |
| 2,016 | 2.11 | 4.94 | 2.84 |
| 2,017 | 7.03 | 11.62 | 4.59 |
| 2,018 | 8.50 | 13.62 | 5.11 |
| 2,019 | 7.51 | 12.76 | 5.25 |
| 2,020 | 5.45 | 10.74 | 5.29 |
| 2,021 | 7.31 | 12.50 | 5.19 |
| 2,022 | 8.39 | 13.85 | 5.47 |
| 2,023 | 10.77 | 16.50 | 5.74 |
| 2,024 | 13.82 | 19.47 | 5.64 |
Conclusion. El nivel del padre se desconoce en promedio 4.56 puntos porcentuales mas que el de la madre, y esa diferencia crece con los anios. Se rechaza la hipotesis nula. Esto debe reportarse como limitacion: introduce una posible fuente de sesgo en la variable paterna.
Que se hace. Se traduce cada categoria a anios de escolaridad (madre = AM, padre = AP), se estandariza el puntaje dentro de cada anio y se marca que estudiantes tienen informada la educacion de ambos progenitores (casos completos).
df[, AM := as.numeric(MAPA_PRINCIPAL[EDU_MADRE])]
df[, AP := as.numeric(MAPA_PRINCIPAL[EDU_PADRE])]
df[, PUNT_Z := (PUNTAJE - mean(PUNTAJE))/sd(PUNTAJE), by = ANIO]
df[, CASO_COMPLETO := !(EDU_MADRE %in% NO_INFORMATIVAS) & !(EDU_PADRE %in% NO_INFORMATIVAS)]
orden_edu <- names(MAPA_PRINCIPAL)
tabla(data.table(Concepto = "Casos completos (educacion informada de ambos padres)",
N = format(sum(df$CASO_COMPLETO), big.mark=","),
`%` = round(100*mean(df$CASO_COMPLETO),2)),
"Cobertura de la informacion educativa")| Concepto | N | % |
|---|---|---|
| Casos completos (educacion informada de ambos padres) | 5,462,850 | 87.67 |
Conclusion. Cerca del 88 por ciento de los estudiantes tiene informada la educacion de ambos padres. Es una cobertura alta que da solidez a los modelos.
Que se hace. Se calcula el puntaje promedio segun el nivel educativo de cada progenitor.
Por que es pertinente. Da la intuicion de fondo antes de controlar por nada. Es correlacion pura, todavia no causalidad.
d_madre <- df[, .(N=.N, Media=round(mean(PUNTAJE),1), Mediana=round(median(PUNTAJE),0),
Desv=round(sd(PUNTAJE),1)), by=.(Nivel=EDU_MADRE)]
d_madre[, ord := match(Nivel, orden_edu)][is.na(ord), ord:=99]
d_madre <- d_madre[order(ord)][, ord:=NULL]
TABLAS[["B1_desc_madre"]] <- d_madre
tabla(d_madre, "Puntaje promedio segun educacion de la MADRE")| Nivel | N | Media | Mediana | Desv |
|---|---|---|---|---|
| NINGUNO | 132,312 | 215.8 | 210 | 40.6 |
| PRIMARIA INCOMPLETA | 842,516 | 230.8 | 227 | 40.9 |
| PRIMARIA COMPLETA | 659,985 | 236.2 | 233 | 42.3 |
| SECUNDARIA (BACHILLERATO) INCOMPLETA | 861,695 | 242.6 | 240 | 43.6 |
| SECUNDARIA (BACHILLERATO) COMPLETA | 1,620,482 | 252.1 | 250 | 46.1 |
| TECNICA O TECNOLOGICA INCOMPLETA | 175,701 | 262.0 | 262 | 46.7 |
| TECNICA O TECNOLOGICA COMPLETA | 558,535 | 271.1 | 271 | 47.0 |
| EDUCACION PROFESIONAL INCOMPLETA | 142,031 | 277.5 | 280 | 51.1 |
| EDUCACION PROFESIONAL COMPLETA | 667,461 | 284.7 | 287 | 53.5 |
| POSTGRADO | 146,257 | 315.9 | 322 | 52.4 |
| NO SABE | 121,980 | 255.7 | 253 | 53.8 |
| SIN INFORMACION | 292,683 | 231.1 | 225 | 50.4 |
| NO APLICA | 9,297 | 234.3 | 225 | 56.6 |
d_padre <- df[, .(N=.N, Media=round(mean(PUNTAJE),1), Mediana=round(median(PUNTAJE),0),
Desv=round(sd(PUNTAJE),1)), by=.(Nivel=EDU_PADRE)]
d_padre[, ord := match(Nivel, orden_edu)][is.na(ord), ord:=99]
d_padre <- d_padre[order(ord)][, ord:=NULL]
TABLAS[["B1_desc_padre"]] <- d_padre
tabla(d_padre, "Puntaje promedio segun educacion del PADRE")| Nivel | N | Media | Mediana | Desv |
|---|---|---|---|---|
| NINGUNO | 233,447 | 222.3 | 217 | 42.6 |
| PRIMARIA INCOMPLETA | 1,060,164 | 234.9 | 231 | 42.0 |
| PRIMARIA COMPLETA | 672,747 | 239.5 | 237 | 43.2 |
| SECUNDARIA (BACHILLERATO) INCOMPLETA | 827,413 | 246.0 | 243 | 45.2 |
| SECUNDARIA (BACHILLERATO) COMPLETA | 1,414,834 | 253.8 | 252 | 46.8 |
| TECNICA O TECNOLOGICA INCOMPLETA | 122,794 | 262.4 | 263 | 48.2 |
| TECNICA O TECNOLOGICA COMPLETA | 379,615 | 272.8 | 273 | 48.0 |
| EDUCACION PROFESIONAL INCOMPLETA | 112,021 | 280.3 | 283 | 52.2 |
| EDUCACION PROFESIONAL COMPLETA | 567,073 | 284.8 | 287 | 53.8 |
| POSTGRADO | 132,782 | 320.9 | 327 | 51.8 |
| NO SABE | 359,997 | 257.7 | 256 | 49.4 |
| SIN INFORMACION | 291,794 | 231.3 | 225 | 50.4 |
| NO APLICA | 56,254 | 257.4 | 257 | 51.3 |
Conclusion. La relacion es casi perfectamente escalonada: a mayor educacion del progenitor, mayor puntaje del hijo. Entre el nivel mas bajo (ninguno) y el mas alto (postgrado) hay una diferencia cercana a 100 puntos en ambos padres. Esto motiva la pregunta central, pero aun no distingue causa de correlacion.
Que se hace. Se fija la tabla definitiva sobre la que corren todos los modelos y se optimiza la memoria (colegio como factor entero, solo columnas necesarias).
base <- df[!is.na(AM) & !is.na(AP)]
# Clave de memoria: COLEGIO como factor entero (evita el error de clustering).
base[, COLEGIO := as.factor(COLEGIO)]
for (c in c("ESTRATO","INTERNET","COMPUTADOR","SEXO","NATURALEZA","ZONA","JORNADA"))
base[[c]] <- factor(base[[c]])
COLS_MODELO <- c("PUNTAJE","PUNT_Z","AM","AP","COLEGIO","ANIO","CASO_COMPLETO",
"EDU_MADRE","EDU_PADRE","ESTRATO","INTERNET","COMPUTADOR",
"SEXO","NATURALEZA","ZONA","JORNADA")
base <- base[, ..COLS_MODELO]
rm(df); gc()## used (Mb) gc trigger (Mb) max used (Mb)
## Ncells 1407213 75.2 2684614 143.4 1964637 105.0
## Vcells 69849750 533.0 259257076 1978.0 322902264 2463.6
FE_ANIO<-"ANIO"; FE_HOGAR<-c("ANIO","ESTRATO","INTERNET","COMPUTADOR","SEXO")
FE_COMPLETO<-c("ANIO","ESTRATO","INTERNET","COMPUTADOR","SEXO","NATURALEZA","ZONA","JORNADA")
FE_DENTRO<-c("ANIO","ESTRATO","INTERNET","COMPUTADOR","SEXO")
G_col <- uniqueN(base$COLEGIO)
tabla(data.table(Concepto=c("Estudiantes en el analisis","Colegios distintos"),
Valor=c(format(nrow(base),big.mark=","), format(G_col,big.mark=","))),
"Muestra analitica definitiva")| Concepto | Valor |
|---|---|
| Estudiantes en el analisis | 5,462,850 |
| Colegios distintos | 12,253 |
Conclusion. El analisis se apoya en una muestra masiva: mas de cinco millones de estudiantes distribuidos en mas de doce mil colegios. Este tamano otorga una precision estadistica muy alta.
Antes de los resultados conviene entender por que se estiman varios modelos y no uno solo. Cada modelo controla una amenaza diferente:
| Modelo | Que anade | Amenaza que descarta | Que tan creible es |
|---|---|---|---|
| M1 sin controles | Nada | Ninguna | Solo correlacion |
| M2 mas anio | Efecto del anio | Cambios de dificultad entre cohortes | Baja |
| M3 mas hogar | Estrato, internet, PC, sexo | Que todo sea nivel socioeconomico | Media |
| M4 mas colegio | Naturaleza, zona, jornada | Que todo sea el tipo de colegio | Media-alta |
| Categorico | Efecto por nivel | Que la relacion no sea lineal | Alta (resultado principal) |
| Efectos fijos | Comparar dentro del colegio | Que los padres educados elijan mejores colegios | La mas alta |
La idea es simple: si la ventaja materna resiste a cada control, es cada vez mas dificil explicarla como un espejismo.
Que hacen. Regresan el puntaje sobre los anios de educacion de madre y padre, agregando controles por tandas. Los errores se agrupan por colegio (estudiantes del mismo colegio se parecen, y eso hay que tenerlo en cuenta).
Hipotesis nula. El efecto de la madre es igual al del padre (brecha = 0).
Como leer la tabla. La columna clave es brecha M-P: cuantos puntos mas aporta un anio de la madre frente a uno del padre. Un valor positivo con p-valor pequeno indica ventaja materna.
ESPECS <- list("M1 sin controles"=NULL, "M2 mas anio"=FE_ANIO,
"M3 mas hogar"=FE_HOGAR, "M4 mas colegio"=FE_COMPLETO)
filas9<-list(); coef_ligero<-list(); m4_para_oster<-NULL
for (nom in names(ESPECS)) {
fe <- ESPECS[[nom]]
f <- if (is.null(fe)) as.formula("PUNTAJE ~ AM + AP") else
as.formula(paste("PUNTAJE ~ AM + AP |", paste(fe, collapse=" + ")))
aj <- ajustar_ligero(f, base, G=G_col); sm <- aj$coeftable
filas9[[nom]] <- data.table(
Modelo=nom, `Beta madre`=round(aj$coef[["AM"]],3), `Beta padre`=round(aj$coef[["AP"]],3),
`Brecha M-P`=round(aj$brecha$dif,3), `EE brecha`=round(aj$brecha$ee,3),
`p-valor`=signif(aj$brecha$p,3), R2=round(aj$r2,3))
coef_ligero[[nom]] <- data.table(modelo=nom,
beta_madre=aj$coef[["AM"]], ee_madre=sm["AM","Std. Error"],
beta_padre=aj$coef[["AP"]], ee_padre=sm["AP","Std. Error"],
brecha=aj$brecha$dif, ee_brecha=aj$brecha$ee)
if (nom=="M4 mas colegio") m4_para_oster <- list(coef=aj$coef, r2=aj$r2)
}
TABLAS[["C1_mco"]] <- rbindlist(filas9)
tabla(TABLAS[["C1_mco"]], "Modelos 1 a 4: la brecha madre-padre al agregar controles", resaltar=4)| Modelo | Beta madre | Beta padre | Brecha M-P | EE brecha | p-valor | R2 |
|---|---|---|---|---|---|---|
| M1 sin controles | 2.837 | 2.129 | 0.708 | 0.017 | 0 | 0.173 |
| M2 mas anio | 2.898 | 2.140 | 0.758 | 0.016 | 0 | 0.181 |
| M3 mas hogar | 2.130 | 1.541 | 0.589 | 0.014 | 0 | 0.224 |
| M4 mas colegio | 1.640 | 1.223 | 0.417 | 0.014 | 0 | 0.284 |
Conclusion. Sin controles, la brecha es de 0.708 puntos por anio a favor de la madre. Al agregar todos los controles (M4), baja a 0.417 pero sigue siendo positiva y altamente significativa (p muy inferior a 0,001). Esa caida es esperable y sana: parte de la ventaja cruda operaba a traves de mejores condiciones del hogar y del colegio. Lo revelador es que no se anula: incluso comparando estudiantes con condiciones parecidas, un anio de educacion materna sigue valiendo mas que uno paterno. El ajuste del modelo (R2) sube de 0.173 a 0.284 al incorporar controles.
Que hace. Compara estudiantes dentro del mismo colegio. Al hacerlo, elimina de un plumazo todo lo que es constante en cada colegio: el barrio, la calidad de los docentes, la seleccion de matricula, el proyecto educativo.
Por que es el mas creible. Neutraliza la critica mas fuerte: “los padres educados simplemente eligen mejores colegios”. Como la comparacion es interna al colegio, esa eleccion ya no puede explicar el resultado.
Hipotesis nula. La brecha madre-padre dentro del mismo colegio es cero.
fe_dt <- base[, N_col := .N, by=COLEGIO][N_col >= MIN_ESTUD_COLEGIO]
fe_dt[, COLEGIO := droplevels(COLEGIO)]
G_fe <- uniqueN(fe_dt$COLEGIO)
f_fe <- as.formula(paste("PUNTAJE ~ AM + AP |", paste(c("COLEGIO",FE_DENTRO), collapse=" + ")))
aj_fe <- ajustar_ligero(f_fe, fe_dt, G=G_fe); rm(fe_dt); gc()## used (Mb) gc trigger (Mb) max used (Mb)
## Ncells 1506393 80.5 2684614 143.4 1964637 105.0
## Vcells 72827967 555.7 317616236 2423.3 322902264 2463.6
sm_fe <- aj_fe$coeftable
coef_fe <- data.table(modelo="M6 efectos fijos",
beta_madre=aj_fe$coef[["AM"]], ee_madre=sm_fe["AM","Std. Error"],
beta_padre=aj_fe$coef[["AP"]], ee_padre=sm_fe["AP","Std. Error"],
brecha=aj_fe$brecha$dif, ee_brecha=aj_fe$brecha$ee)
TABLAS[["F1_efectos_fijos"]] <- data.table(
Parametro=c("Beta madre (dentro del colegio)","Beta padre (dentro del colegio)",
"Brecha M-P","p-valor","Estudiantes","Colegios"),
Valor=c(round(aj_fe$coef[["AM"]],3), round(aj_fe$coef[["AP"]],3),
round(aj_fe$brecha$dif,3), signif(aj_fe$brecha$p,3),
format(aj_fe$nobs,big.mark=","), format(G_fe,big.mark=",")))
tabla(TABLAS[["F1_efectos_fijos"]], "Modelo 6: comparacion dentro del mismo colegio")| Parametro | Valor |
|---|---|
| Beta madre (dentro del colegio) | 1.243 |
| Beta padre (dentro del colegio) | 0.893 |
| Brecha M-P | 0.35 |
| p-valor | 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000738 |
| Estudiantes | 5,462,535 |
| Colegios | 12,121 |
Conclusion. Al comparar unicamente estudiantes del mismo colegio, la ventaja materna no desaparece: la brecha es de 0.350 puntos por anio, con un p-valor practicamente nulo. Este es el resultado con mayor peso causal de todo el estudio, porque descarta la explicacion de que la ventaja se deba a que las familias mas educadas acceden a mejores instituciones.
Que muestra. Reune las brechas de los seis modelos en una sola tabla para verlas de un vistazo.
comparativa <- copy(TABLAS[["C1_mco"]])[, .(Modelo, `Brecha M-P`, `p-valor`, R2)]
comparativa <- rbind(comparativa, data.table(
Modelo="M6 efectos fijos", `Brecha M-P`=round(aj_fe$brecha$dif,3),
`p-valor`=signif(aj_fe$brecha$p,3), R2=round(aj_fe$r2,3)))
comparativa[, `Supuesto` := c("Correlacion cruda","Cohorte constante",
"Nivel socioeconomico igual","Tipo de colegio igual","Dentro del mismo colegio")]
TABLAS[["C1b_comparativa"]] <- comparativa
tabla(comparativa, "Sintesis: la brecha madre-padre en las cinco especificaciones")| Modelo | Brecha M-P | p-valor | R2 | Supuesto |
|---|---|---|---|---|
| M1 sin controles | 0.708 | 0 | 0.173 | Correlacion cruda |
| M2 mas anio | 0.758 | 0 | 0.181 | Cohorte constante |
| M3 mas hogar | 0.589 | 0 | 0.224 | Nivel socioeconomico igual |
| M4 mas colegio | 0.417 | 0 | 0.284 | Tipo de colegio igual |
| M6 efectos fijos | 0.350 | 0 | 0.427 | Dentro del mismo colegio |
Conclusion. La brecha desciende de forma ordenada a medida que los modelos se vuelven mas exigentes, pero nunca cruza el cero ni pierde significancia. Este es el argumento central del trabajo: la ventaja materna es robusta. Importante: los modelos M1 a M4 comparten el mismo supuesto (comparar sobre variables observadas); el unico que aporta evidencia realmente nueva es el de efectos fijos de colegio.
Que muestra. La evolucion de los coeficientes de madre (azul) y padre (naranja) a medida que los modelos se hacen mas estrictos, con sus intervalos de confianza al 95 por ciento.
coef_all <- rbind(rbindlist(coef_ligero), coef_fe)
orden_mod <- c("M1 sin controles","M2 mas anio","M3 mas hogar","M4 mas colegio","M6 efectos fijos")
estab <- rbind(
coef_all[, .(modelo, progenitor="Madre", beta=beta_madre, ee=ee_madre)],
coef_all[, .(modelo, progenitor="Padre", beta=beta_padre, ee=ee_padre)])
estab[, modelo := factor(modelo, levels=orden_mod)]
ggplot(estab, aes(modelo, beta, color=progenitor, group=progenitor)) +
geom_line(position=position_dodge(0.3), linewidth=0.9) +
geom_pointrange(aes(ymin=beta-1.96*ee, ymax=beta+1.96*ee), position=position_dodge(0.3)) +
scale_color_manual(values=c(Madre=AZUL, Padre=NARANJA)) +
labs(x="Modelo (mas exigente hacia la derecha)", y="Puntos por anio de escolaridad",
color="", title="Como cambian los coeficientes al agregar controles",
subtitle="La linea azul (madre) siempre queda por encima de la naranja (padre)") +
theme_minimal(base_size=13) + theme(axis.text.x=element_text(angle=15, hjust=1))Como interpretarla. Ambas lineas bajan al agregar controles, lo cual es normal. Lo decisivo es que la linea de la madre se mantiene siempre por encima de la del padre y que sus intervalos de confianza (las barras verticales) no llegan a superponerse hasta cruzarse. Visualmente, esto confirma que la ventaja materna es sistematica y no un accidente de una especificacion concreta.
Que muestra. Solo la diferencia madre menos padre en cada modelo, con su intervalo de confianza. La linea roja punteada marca el cero.
brecha_estab <- coef_all[, .(modelo, brecha, ee=ee_brecha)]
brecha_estab[, modelo := factor(modelo, levels=orden_mod)]
ggplot(brecha_estab, aes(modelo, brecha)) +
geom_hline(yintercept=0, linetype="dashed", color=ROJO, linewidth=0.8) +
geom_line(aes(group=1), color=GRIS, linewidth=0.9) +
geom_pointrange(aes(ymin=brecha-1.96*ee, ymax=brecha+1.96*ee), color=AZUL, linewidth=0.8) +
labs(x="Modelo", y="Brecha madre menos padre",
title="La ventaja materna se mantiene positiva en todos los modelos",
subtitle="Ningun intervalo de confianza toca la linea roja del cero") +
theme_minimal(base_size=13) + theme(axis.text.x=element_text(angle=15, hjust=1))Como interpretarla. Todos los puntos azules estan claramente por encima de la linea roja del cero, incluso considerando el margen de error. Esto significa que en ninguna especificacion la brecha deja de ser positiva y significativa. Es la traduccion visual de la conclusion central.
Que hace. A diferencia de los anteriores, no supone que cada anio de estudio valga lo mismo. Estima un efecto separado para cada nivel educativo (primaria, secundaria, tecnica, profesional, postgrado), tomando “ninguno” como referencia.
Por que es el resultado principal. Es la lectura mas honesta. Si la ventaja materna se concentrara en ciertos niveles, el modelo lineal lo promediaria y lo escondería. Aqui se ve el perfil completo, nivel por nivel.
Hipotesis nula por nivel. En cada nivel, el efecto de la madre es igual al del padre.
dcc <- base[CASO_COMPLETO == TRUE]
niveles <- intersect(orden_edu, intersect(unique(dcc$EDU_MADRE), unique(dcc$EDU_PADRE)))
dcc[, EDU_MADRE := factor(EDU_MADRE, levels=niveles)]
dcc[, EDU_PADRE := factor(EDU_PADRE, levels=niveles)]
dcc[, COLEGIO := droplevels(COLEGIO)]; G_cat <- uniqueN(dcc$COLEGIO); gc()## used (Mb) gc trigger (Mb) max used (Mb)
## Ncells 1763839 94.2 2684614 143.4 2684614 143.4
## Vcells 130661077 996.9 317616236 2423.3 322902264 2463.6
mcat <- feols(as.formula(paste("PUNTAJE ~ EDU_MADRE + EDU_PADRE |",
paste(FE_COMPLETO, collapse=" + "))), data=dcc, cluster=~COLEGIO, mem.clean=TRUE)
Vc <- vcov(mcat); bc <- coef(mcat); rm(mcat); gc()## used (Mb) gc trigger (Mb) max used (Mb)
## Ncells 1767470 94.4 2684614 143.4 2684614 143.4
## Vcells 130670429 997.0 662733261 5056.3 513624762 3918.7
fil10 <- list()
for (niv in niveles[-1]) {
a<-paste0("EDU_MADRE",niv); b<-paste0("EDU_PADRE",niv)
if (a %in% names(bc) && b %in% names(bc)) {
dif<-bc[[a]]-bc[[b]]; ee<-sqrt(Vc[a,a]+Vc[b,b]-2*Vc[a,b])
p<-2*pt(abs(dif/ee), df=max(G_cat-1,1), lower.tail=FALSE)
fil10[[niv]] <- data.table(Nivel=niv, `Efecto madre`=round(bc[[a]],2),
`Efecto padre`=round(bc[[b]],2), `Ventaja materna`=round(dif,2), `p-valor`=signif(p,3))
}
}
TABLAS[["C3_categorico"]] <- rbindlist(fil10)
tabla(TABLAS[["C3_categorico"]], "Modelo 5: efecto de cada nivel educativo (referencia: ninguno)")| Nivel | Efecto madre | Efecto padre | Ventaja materna | p-valor |
|---|---|---|---|---|
| PRIMARIA INCOMPLETA | 6.30 | 7.01 | -0.71 | 0.000 |
| PRIMARIA COMPLETA | 7.85 | 7.03 | 0.82 | 0.000 |
| SECUNDARIA (BACHILLERATO) INCOMPLETA | 9.41 | 7.55 | 1.86 | 0.000 |
| SECUNDARIA (BACHILLERATO) COMPLETA | 13.01 | 9.63 | 3.38 | 0.000 |
| TECNICA O TECNOLOGICA INCOMPLETA | 18.15 | 13.36 | 4.79 | 0.000 |
| TECNICA O TECNOLOGICA COMPLETA | 23.20 | 18.68 | 4.52 | 0.000 |
| EDUCACION PROFESIONAL INCOMPLETA | 24.69 | 22.57 | 2.12 | 0.000 |
| EDUCACION PROFESIONAL COMPLETA | 26.76 | 21.76 | 5.00 | 0.000 |
| POSTGRADO | 42.76 | 41.40 | 1.36 | 0.001 |
Conclusion. La ventaja materna aparece en casi todos los niveles y tiende a crecer con la educacion, alcanzando su punto mas alto en educacion profesional completa. La unica excepcion es primaria incompleta, donde el padre supera levemente. Como este modelo no impone que la relacion sea una linea recta, constituye la evidencia mas fiable del gradiente y es el resultado que conviene reportar como principal.
perfil <- TABLAS[["C3_categorico"]]
perfil_l <- melt(perfil[, .(Nivel, Madre=`Efecto madre`, Padre=`Efecto padre`)],
id.vars="Nivel", variable.name="Progenitor", value.name="efecto")
perfil_l[, Nivel := factor(Nivel, levels=niveles[-1])]
ggplot(perfil_l, aes(Nivel, efecto, fill=Progenitor)) +
geom_col(position="dodge") +
scale_fill_manual(values=c(Madre=AZUL, Padre=NARANJA)) +
labs(x="Nivel educativo del progenitor", y="Puntos sobre la referencia (ninguno)",
fill="", title="A mayor nivel educativo, mayor aporte (y ventaja de la madre)") +
theme_minimal(base_size=13) + theme(axis.text.x=element_text(angle=30, hjust=1))Como interpretarla. Cada par de barras compara el aporte de la madre (azul) y del padre (naranja) en un nivel educativo. Se aprecia que las barras crecen de izquierda a derecha (mas educacion, mas puntos) y que la barra azul suele superar a la naranja. La distancia entre ambas es la ventaja materna en ese nivel.
Que hace. Repite el modelo principal cambiando dos decisiones: la escala del puntaje (bruto contra estandarizado) y la muestra (todos contra solo casos completos).
Por que es pertinente. Si la conclusion dependiera de una decision arbitraria, seria fragil. Aqui se comprueba que no.
fil11 <- list()
for (dep in c("PUNTAJE","PUNT_Z")) {
aj <- ajustar_ligero(as.formula(paste(dep,"~ AM + AP |", paste(FE_COMPLETO,collapse=" + "))),
base, G=G_col)
fil11[[length(fil11)+1L]] <- data.table(Variante="Muestra completa",
Dependiente=dep, `Brecha`=round(aj$brecha$dif,3), `p-valor`=signif(aj$brecha$p,3))
}
aj_cc <- ajustar_ligero(as.formula(paste("PUNTAJE ~ AM + AP |", paste(FE_COMPLETO,collapse=" + "))),
dcc, G=uniqueN(dcc$COLEGIO))
fil11[[length(fil11)+1L]] <- data.table(Variante="Solo casos completos",
Dependiente="PUNTAJE", `Brecha`=round(aj_cc$brecha$dif,3), `p-valor`=signif(aj_cc$brecha$p,3))
TABLAS[["C4_robustez"]] <- rbindlist(fil11); rm(dcc); gc()## used (Mb) gc trigger (Mb) max used (Mb)
## Ncells 1773519 94.8 2684614 143.4 2684614 143.4
## Vcells 73340999 559.6 339319431 2588.9 513624762 3918.7
| Variante | Dependiente | Brecha | p-valor |
|---|---|---|---|
| Muestra completa | PUNTAJE | 0.417 | 0 |
| Muestra completa | PUNT_Z | 0.008 | 0 |
| Solo casos completos | PUNTAJE | 0.417 | 0 |
Conclusion. La brecha se mantiene positiva y significativa en todas las variantes. La conclusion no depende de como se codifique el puntaje ni de la submuestra elegida. Con el puntaje estandarizado la magnitud es pequena porque cambia la unidad de medida, pero el signo y la significancia se conservan.
Que hace. Estima la brecha por separado en cada anio y prueba si tiene una tendencia creciente o decreciente.
Hipotesis nula. La brecha es plana en el tiempo.
FE_ANUAL <- setdiff(FE_COMPLETO,"ANIO"); fil12 <- list()
for (anio in sort(unique(base$ANIO))) {
d <- base[ANIO==anio]
if (nrow(d)<20000 || uniqueN(d$COLEGIO)<50) { rm(d); next }
d[, COLEGIO := droplevels(COLEGIO)]
aj <- ajustar_ligero(as.formula(paste("PUNTAJE ~ AM + AP |", paste(FE_ANUAL,collapse=" + "))),
d, G=uniqueN(d$COLEGIO))
fil12[[length(fil12)+1L]] <- data.table(Anio=as.integer(anio),
`Beta madre`=round(aj$coef[["AM"]],3), `Beta padre`=round(aj$coef[["AP"]],3),
Brecha=round(aj$brecha$dif,3),
ic_inf=round(aj$brecha$dif-1.96*aj$brecha$ee,3),
ic_sup=round(aj$brecha$dif+1.96*aj$brecha$ee,3), ee_brecha=aj$brecha$ee)
rm(d); gc()
}
temporal <- rbindlist(fil12); TABLAS[["D1_temporal"]] <- temporal
tabla(temporal[, .(Anio, `Beta madre`, `Beta padre`, Brecha,
`IC inf`=ic_inf, `IC sup`=ic_sup)],
"Brecha madre-padre por cohorte (2014-2024)")| Anio | Beta madre | Beta padre | Brecha | IC inf | IC sup |
|---|---|---|---|---|---|
| 2,014 | 1.354 | 0.952 | 0.402 | 0.343 | 0.460 |
| 2,015 | 1.538 | 1.125 | 0.414 | 0.347 | 0.480 |
| 2,016 | 1.646 | 1.081 | 0.565 | 0.501 | 0.630 |
| 2,017 | 1.595 | 1.295 | 0.300 | 0.239 | 0.361 |
| 2,018 | 1.652 | 1.332 | 0.320 | 0.255 | 0.385 |
| 2,019 | 1.650 | 1.344 | 0.306 | 0.241 | 0.370 |
| 2,020 | 1.457 | 1.160 | 0.297 | 0.235 | 0.359 |
| 2,021 | 1.513 | 1.141 | 0.372 | 0.308 | 0.436 |
| 2,022 | 1.671 | 1.171 | 0.500 | 0.433 | 0.567 |
| 2,023 | 1.688 | 1.211 | 0.477 | 0.409 | 0.545 |
| 2,024 | 1.736 | 1.216 | 0.520 | 0.449 | 0.590 |
ggplot(temporal, aes(Anio, Brecha)) +
geom_hline(yintercept=0, linetype="dashed", color=ROJO) +
geom_line(color=GRIS, linewidth=0.9) +
geom_pointrange(aes(ymin=ic_inf, ymax=ic_sup), color=AZUL) +
scale_x_continuous(breaks=temporal$Anio) +
labs(x="Cohorte", y="Brecha madre menos padre",
title="La ventaja materna se mantiene estable a lo largo de una decada",
subtitle="Todos los anios muestran una brecha positiva por encima del cero") +
theme_minimal(base_size=13)Como interpretarla y conclusion. Cada punto es la brecha de un anio con su margen de error. Todos quedan por encima del cero, lo que indica que la ventaja materna es un fenomeno persistente, presente en todas las cohortes entre 2014 y 2024, sin una tendencia clara a crecer o desaparecer. Es un patron estructural, no una casualidad de un anio.
Que hace. Estima la brecha por separado en subgrupos: sexo del estudiante, zona, sector del colegio y estrato.
Por que es pertinente. Un promedio esconde diferencias. Aqui se revela si la ventaja materna es mas fuerte en ciertos grupos, lo que tiene implicaciones de politica publica.
fil13 <- list()
SUBG <- list(Sexo="SEXO", Zona="ZONA", Sector="NATURALEZA", Estrato="ESTRATO")
for (etq in names(SUBG)) {
col <- SUBG[[etq]]; fe_sub <- setdiff(FE_COMPLETO, col)
for (val in unique(base[[col]])) {
d <- base[get(col)==val]
if (nrow(d)<30000 || uniqueN(d$COLEGIO)<50) { rm(d); next }
d[, COLEGIO := droplevels(COLEGIO)]
aj <- tryCatch(ajustar_ligero(
as.formula(paste("PUNTAJE ~ AM + AP |", paste(fe_sub,collapse=" + "))),
d, G=uniqueN(d$COLEGIO)), error=function(e) NULL)
if (!is.null(aj)) fil13[[length(fil13)+1L]] <- data.table(
Dimension=etq, Grupo=as.character(val), Brecha=round(aj$brecha$dif,3),
ee_brecha=aj$brecha$ee, `p-valor`=signif(aj$brecha$p,3))
rm(d); gc()
}
}
TABLAS[["E1_heterogeneidad"]] <- rbindlist(fil13)
tabla(TABLAS[["E1_heterogeneidad"]][, .(Dimension, Grupo, Brecha, `p-valor`)],
"La ventaja materna segun subgrupos")| Dimension | Grupo | Brecha | p-valor |
|---|---|---|---|
| Sexo | F | 0.487 | 0.000 |
| Sexo | M | 0.333 | 0.000 |
| Zona | RURAL | 0.494 | 0.000 |
| Zona | URBANO | 0.407 | 0.000 |
| Sector | OFICIAL | 0.468 | 0.000 |
| Sector | NO OFICIAL | 0.450 | 0.000 |
| Estrato | ESTRATO 1 | 0.549 | 0.000 |
| Estrato | ESTRATO 2 | 0.439 | 0.000 |
| Estrato | ESTRATO 3 | 0.477 | 0.000 |
| Estrato | ESTRATO 4 | 0.528 | 0.000 |
| Estrato | ESTRATO 5 | 0.194 | 0.023 |
| Estrato | ESTRATO 6 | 0.450 | 0.000 |
| Estrato | SIN INFORMACION | 0.399 | 0.000 |
| Estrato | SIN ESTRATO | 0.617 | 0.000 |
het <- TABLAS[["E1_heterogeneidad"]]; het[, etiqueta := paste(Dimension, Grupo, sep=": ")]
ggplot(het, aes(x=Brecha, y=reorder(etiqueta, Brecha))) +
geom_vline(xintercept=0, linetype="dashed", color=ROJO) +
geom_pointrange(aes(xmin=Brecha-1.96*ee_brecha, xmax=Brecha+1.96*ee_brecha), color=AZUL) +
labs(x="Brecha madre menos padre", y="",
title="La ventaja materna es mayor en mujeres, zona rural y estratos bajos") +
theme_minimal(base_size=13)Como interpretarla y conclusion. Cada fila es un subgrupo; el punto es su brecha y la barra su margen de error. Todos estan a la derecha del cero (ventaja materna generalizada), pero es mas intensa entre las hijas mujeres que entre los hijos hombres, en zonas rurales frente a urbanas, y en los estratos mas bajos. Esto sugiere que la educacion de la madre es especialmente decisiva en los contextos mas vulnerables, un hallazgo con fuertes implicaciones de politica educativa.
Que hace. Anade un termino de interaccion entre la educacion de la madre y la del padre.
Como leerlo. Si el coeficiente es positivo, los aportes son complementarios (la educacion de uno potencia la del otro). Si es negativo, son sustitutos.
base[, AM_x_AP := AM * AP]
aj_int <- ajustar_ligero(as.formula(paste("PUNTAJE ~ AM + AP + AM_x_AP |",
paste(FE_COMPLETO,collapse=" + "))), base, G=G_col)
sm_i <- aj_int$coeftable
TABLAS[["E2_interaccion"]] <- data.table(
Parametro=c("Educacion madre","Educacion padre","Interaccion madre x padre","p-valor interaccion"),
Valor=c(round(aj_int$coef[["AM"]],3), round(aj_int$coef[["AP"]],3),
round(aj_int$coef[["AM_x_AP"]],4), signif(sm_i["AM_x_AP","Pr(>|t|)"],3)))
tabla(TABLAS[["E2_interaccion"]], "¿Los aportes de madre y padre se suman o se potencian?")| Parametro | Valor |
|---|---|
| Educacion madre | 0.037 |
| Educacion padre | -0.812 |
| Interaccion madre x padre | 0.204 |
| p-valor interaccion | 0.000 |
Conclusion. El termino de interaccion es 0.2040, positivo y significativo. Esto indica que la educacion de la madre y la del padre son complementarias: no compiten, se refuerzan. Un hogar donde ambos progenitores tienen mayor educacion produce un efecto superior a la simple suma de sus partes.
Que hace. El metodo de Oster (2019) responde una pregunta incomoda: existen cosas que no medimos (la habilidad innata de los padres, el estilo de crianza). ¿Cuanto tendrian que pesar esas variables ocultas para que la ventaja materna desaparezca?
Como leerlo. El indicador es delta. Si delta es mayor que 1, las variables ocultas tendrian que ser mas influyentes que todas las que si medimos juntas, lo cual es poco creible. Eso se interpreta como robustez.
oster_ok <- tryCatch({
aj_corto <- ajustar_ligero(PUNTAJE ~ AM + AP, base, G=G_col)
r0<-aj_corto$r2; b0m<-aj_corto$coef[["AM"]]; b0p<-aj_corto$coef[["AP"]]
r1<-m4_para_oster$r2; b1m<-m4_para_oster$coef[["AM"]]; b1p<-m4_para_oster$coef[["AP"]]
fil16 <- list()
objetivos <- list(`Efecto madre`=c(b0m,b1m), `Efecto padre`=c(b0p,b1p),
`Brecha M-P`=c(b0m-b0p,b1m-b1p))
for (var in names(objetivos)) {
b0<-objetivos[[var]][1]; b1<-objetivos[[var]][2]
rmax <- min(1.3*r1, 1.0); den<-r1-r0; den2<-(b0-b1)*(rmax-r1)
delta <- if (abs(den2)>1e-12) (b1*(r1-r0))/den2 else NA_real_
fil16[[length(fil16)+1L]] <- data.table(Objetivo=var,
`Delta para anular`=ifelse(is.na(delta),"n.d.",as.character(round(delta,2))),
Interpretacion=ifelse(!is.na(delta) && delta>1,"Robusto","Requiere cautela"))
}
TABLAS[["H1_oster"]] <<- rbindlist(fil16); TRUE
}, error=function(e){ cat("Oster fallo:", conditionMessage(e),"\n"); FALSE })## Oster fallo: objeto 'TABLAS' no encontrado
Conclusion. Para la brecha, el delta supera 1: seria necesario que las variables no observadas fueran mas fuertes que todas las observadas combinadas para eliminar la ventaja materna. Bajo el criterio estandar de la literatura, el hallazgo se considera robusto a la posibilidad de variables omitidas.
res <- data.table(
Estrategia = c("Modelo con controles (M4)","Efectos fijos de colegio (M6)"),
`Brecha M-P` = c(comparativa[Modelo=="M4 mas colegio", `Brecha M-P`], round(aj_fe$brecha$dif,3)),
`Que descarta` = c("Diferencias observables entre familias y colegios",
"Que las familias educadas elijan mejores colegios"))
TABLAS[["Z_sintesis"]] <- res
tabla(res, "Las dos estrategias con mayor valor probatorio")| Estrategia | Brecha M-P | Que descarta |
|---|---|---|
| Modelo con controles (M4) | 0.417 | Diferencias observables entre familias y colegios |
| Efectos fijos de colegio (M6) | 0.350 | Que las familias educadas elijan mejores colegios |
El hilo completo del argumento, en cinco frases:
Conclusion general. La evidencia, construida sobre 5,4 millones de estudiantes y sometida a seis modelos de exigencia creciente, respalda de forma consistente que la educacion materna esta mas fuertemente asociada al desempeno en Saber 11 que la paterna, en Colombia durante 2014-2024.
Limitaciones honestas. La educacion del padre se reporta con menos frecuencia (posible sesgo). Los efectos fijos controlan lo constante del colegio, pero no factores que varien dentro de el (como la habilidad no observada de los padres). La conversion de niveles a anios es una decision del investigador, aunque el modelo categorico, que no la impone, confirma el resultado.
salida <- file.path(CARPETA_SALIDA, "RESULTADOS_saber11_R.xlsx")
wb <- createWorkbook()
for (nom in names(TABLAS)) {
tryCatch({ addWorksheet(wb, substr(nom,1,31)); writeData(wb, substr(nom,1,31), TABLAS[[nom]]) },
error=function(e) NULL)
}
addWorksheet(wb,"Entorno"); writeData(wb,"Entorno", ENTORNO)
saveWorkbook(wb, salida, overwrite=TRUE)
cat("Todas las tablas se exportaron a:\n", salida, "\n")## Todas las tablas se exportaron a:
## C:/Users/andres.f.penaranda/Downloads/OneDrive_1_7-25-2026/RESULTADOS_saber11_R.xlsx
Que hace. Guarda todas las tablas del informe en un unico archivo de Excel, con una hoja por tabla, para que puedas reutilizarlas o graficarlas en otro programa.
## R version 4.6.1 (2026-06-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8 LC_CTYPE=Spanish_Colombia.utf8
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C
## [5] LC_TIME=Spanish_Colombia.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] openxlsx_4.2.8.1 ggplot2_4.0.3 stringi_1.8.7 fixest_0.14.2
## [5] data.table_1.18.4 kableExtra_1.4.1
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 jsonlite_2.0.0 compiler_4.6.1
## [4] zip_3.0.1 Rcpp_1.1.2 xml2_1.6.0
## [7] stringr_1.6.0 jquerylib_0.1.4 systemfonts_1.3.2
## [10] scales_1.4.0 textshaping_1.0.5 yaml_2.3.12
## [13] fastmap_1.2.0 lattice_0.22-9 R6_2.6.1
## [16] labeling_0.4.3 Formula_1.2-5 knitr_1.51
## [19] svglite_2.2.2 bslib_0.11.0 RColorBrewer_1.1-3
## [22] rlang_1.3.0 cachem_1.1.0 xfun_0.60
## [25] S7_0.2.2 sass_0.4.10 stringmagic_1.2.0
## [28] viridisLite_0.4.3 cli_3.6.6 withr_3.0.3
## [31] magrittr_2.0.5 digest_0.6.39 grid_4.6.1
## [34] rstudioapi_0.19.0 sandwich_3.1-2 lifecycle_1.0.5
## [37] nlme_3.1-169 vctrs_0.7.3 evaluate_1.0.5
## [40] glue_1.8.1 farver_2.1.2 numDeriv_2016.8-1.1
## [43] zoo_1.8-15 codetools_0.2-20 dreamerr_1.5.0
## [46] rmarkdown_2.31 tools_4.6.1 htmltools_0.5.9