Capítulo 10
Laboratorio de reproducción
Cómo ejecutar el código del libro de principio a fin y obtener la misma tabla de validación: entorno, datos, scripts, pruebas y problemas frecuentes.
Daniel Molina BarriosActualizado el 21 de octubre de 2026
Todas las cifras propias de este libro salen de diez scripts de Python reunidos en la carpeta geih-lab. Leen las bases anuales de la GEIH, auditan su estructura, calculan los indicadores con el factor de expansión, extraen las cifras oficiales de los anexos y boletines del DANE y comparan unas con otras. Las fichas de resultado de los capítulos anteriores muestran el final de esa cadena; este capítulo la recorre desde el principio, con los comandos exactos, para que el lector obtenga en su equipo la misma tabla de validación que acompaña al libro y sepa qué revisar cuando una fila no le sale igual.
Dos condiciones pesan sobre todo lo que sigue. El laboratorio no trae microdatos ni los redistribuye: cada lector los descarga del catálogo del DANE con su propio registro. Y el primer eslabón, el que convierte los módulos mensuales de la descarga en una base anual, está programado y probado con datos sintéticos, pero no se ha ejecutado contra los módulos reales, porque las bases con las que se calculó el libro llegaron ya unidas. La reproducción, por tanto, empieza en esas bases anuales, y el paso previo queda documentado como limitación.
Qué se reproduce y con qué alcance#
La palabra reproducir tiene aquí tres sentidos que no conviene mezclar. El primero es mecánico: con las mismas bases, el mismo código y las mismas versiones de las librerías, los JSON que alimentan las figuras deben salir idénticos, cifra por cifra. El segundo es el contraste con lo publicado: cada cifra propia que tiene un referente oficial se compara con la celda del anexo o con la página del boletín de donde sale, y recibe un estado según una regla fija. El tercero, que el laboratorio no puede demostrar, es que el DANE haga internamente el mismo cálculo. Cuando una tasa anual coincide hasta el cuarto decimal con una anualización hecha sobre las celdas mensuales del anexo, lo que queda comprobado es la aritmética; el estimador anual del DANE no está documentado en los materiales y la coincidencia no prueba que sea el mismo.
La figura 10.1 resume la cadena. Agrupa los scripts por función y no por orden de ejecución: el orden real es el numérico, de 00 a 09, de modo que 08_magdalena_grupos.py y 09_contenido_documental.py corren después del contraste oficial aunque el diagrama los dibuje entre los cálculos. Ninguno de los dos usa las salidas de 06 ni de 07, así que el orden no altera sus resultados.
Diagrama del laboratorio geih-lab
1. Fuentes
ANDA (microdatos.dane.gov.co)
Módulos mensuales de la GEIH; la descarga exige registro.
Anexos y boletines del DANE
Cifras oficiales con hoja y celda para el contraste.
2. Base anual
scripts/00_construir_base_anual.py
Une los módulos de cada mes con las llaves de vivienda, hogar y persona y apila los doce meses.
Produce: geih_AAAA.csv
Probado con módulos sintéticos; no se ha ejecutado contra los módulos reales de ANDA. Las bases del libro llegaron ya unidas.
3. Lectura tipada
geihlab/lectura.py
Lee todo como texto, castea por nombre de columna, rellena ceros a la izquierda y exige los 12 meses.
4. Cálculos
Cada script escribe uno o más JSON agregados; ninguno publica microdatos.
01_auditar_bases.py
Estructura, llaves y controles.
Produce: estructura_microdatos.json, columnas_por_anio.json
02_indicadores_laborales.py
Poblaciones y tasas, mensuales y anuales.
Produce: indicadores_laborales.json, ponderado_vs_simple.json, poblaciones_2025.json
03_informalidad.py
Regla oficial de informalidad del DANE.
Produce: informalidad.json
04_ingresos.py
Medias y cuantiles ponderados de INGLABO.
Produce: ingresos_cuantiles.json, ingresos_histograma.json
05_factor_expansion.py
Distribución de FEX_C18.
Produce: fex_distribucion.json
08_magdalena_grupos.py
Grupos de edad y educación con su precisión aproximada.
Produce: magdalena_grupos.json
09_contenido_documental.py
Valida el contenido escrito a mano desde los documentos: esquema, citas literales en su página y recorrido de cada caso del árbol.
Produce: linea_tiempo.json, desagregacion.json, arbol_clasificacion.json
5. Contraste oficial
06_oficiales.py
Extrae cifras de anexos y boletines con hoja y celda, o página, y fecha de publicación.
Produce: oficiales.json, precision_oficial.json, oficiales/cifras_oficiales.csv
07_validacion.py
Une lo propio y lo oficial, calcula diferencias y asigna estado.
Produce: validacion.json, public/libro-geih/validacion.csv
6. Manifiesto y pruebas
run_all.py
Corre los scripts en orden y registra fecha, hash y versiones.
Produce: _meta.json
tests/ (pytest)
Identidades, llaves, 12 meses, cuantiles y contraste con el anexo.
7. Libro
next-app/data/libro-geih/
Las figuras y tablas del libro leen estos JSON.
Los scripts se ejecutan en orden numérico con python run_all.py desde la carpeta del laboratorio y las pruebas con python -m pytest. La ruta de los microdatos se fija con la variable de entorno GEIH_DATOS, la de los anexos con GEIH_ANEXOS y la de salida con GEIH_SALIDA.
Propósito: Mostrar qué entra, qué script lo transforma y qué archivo produce cada etapa, para ubicar dónde se origina una cifra del libro.
- Unidad
- No aplica (diagrama de procesos)
- Periodo
- Corrida registrada el 21 de octubre de 2026
- Cobertura
- Los diez scripts, el módulo geihlab, el manifiesto y las pruebas
- Fuente
- Elaboración propia a partir de
geih-lab/run_all.pyy de las llamadas aescribir_jsonde cada script, verificadas con el manifiesto_meta.json.
Ver los datos de la figura
- Fuentes. ANDA (microdatos.dane.gov.co): Módulos mensuales de la GEIH; la descarga exige registro. Anexos y boletines del DANE: Cifras oficiales con hoja y celda para el contraste.
- Base anual. scripts/00_construir_base_anual.py: Une los módulos de cada mes con las llaves de vivienda, hogar y persona y apila los doce meses. Produce geih_AAAA.csv. Aviso: Probado con módulos sintéticos; no se ha ejecutado contra los módulos reales de ANDA. Las bases del libro llegaron ya unidas.
- Lectura tipada. geihlab/lectura.py: Lee todo como texto, castea por nombre de columna, rellena ceros a la izquierda y exige los 12 meses.
- Cálculos. 01_auditar_bases.py: Estructura, llaves y controles. Produce estructura_microdatos.json, columnas_por_anio.json. 02_indicadores_laborales.py: Poblaciones y tasas, mensuales y anuales. Produce indicadores_laborales.json, ponderado_vs_simple.json, poblaciones_2025.json. 03_informalidad.py: Regla oficial de informalidad del DANE. Produce informalidad.json. 04_ingresos.py: Medias y cuantiles ponderados de INGLABO. Produce ingresos_cuantiles.json, ingresos_histograma.json. 05_factor_expansion.py: Distribución de FEX_C18. Produce fex_distribucion.json. 08_magdalena_grupos.py: Grupos de edad y educación con su precisión aproximada. Produce magdalena_grupos.json. 09_contenido_documental.py: Valida el contenido escrito a mano desde los documentos: esquema, citas literales en su página y recorrido de cada caso del árbol. Produce linea_tiempo.json, desagregacion.json, arbol_clasificacion.json.
- Contraste oficial. 06_oficiales.py: Extrae cifras de anexos y boletines con hoja y celda, o página, y fecha de publicación. Produce oficiales.json, precision_oficial.json, oficiales/cifras_oficiales.csv. 07_validacion.py: Une lo propio y lo oficial, calcula diferencias y asigna estado. Produce validacion.json, public/libro-geih/validacion.csv.
- Manifiesto y pruebas. run_all.py: Corre los scripts en orden y registra fecha, hash y versiones. Produce _meta.json. tests/ (pytest): Identidades, llaves, 12 meses, cuantiles y contraste con el anexo.
- Libro. next-app/data/libro-geih/: Las figuras y tablas del libro leen estos JSON.
Requisitos e instalación#
El laboratorio se ejecutó con Python 3.14.2 en Windows. Las versiones de las librerías no se fijaron de memoria: se tomaron con pip freeze del entorno donde corrieron los scripts y quedaron en requirements.txt. La lista es corta a propósito. polars hace la lectura y casi todos los cálculos; numpy interviene en los cuantiles ponderados, en los histogramas y en algunos controles de ingresos y del factor; openpyxl lee los anexos en formato .xlsx y xlrd los departamentales, que están en .xls; pytest corre las pruebas. Las demás líneas del archivo son dependencias de esas cinco.
# Versiones exactas del entorno donde se ejecutó el laboratorio (Python 3.14.2),
# tomadas con `pip freeze`.
polars==1.39.3
polars-runtime-32==1.39.3
numpy==2.5.2
openpyxl==3.1.5
et_xmlfile==2.0.0
xlrd==2.0.2
pytest==9.0.2
iniconfig==2.3.0
packaging==26.2
pluggy==1.6.0
Pygments==2.20.0
colorama==0.4.6Hay una dependencia opcional que no está en la lista. Cuando 06_oficiales.py toma una cifra del texto de un boletín, busca la frase citada en la página indicada. Si junto al PDF existe un .txt con el texto extraído por páginas, lo usa; si no, recurre a pypdf, y si pypdf tampoco está instalado, registra la cita como no verificada sin detenerse. En la corrida del libro había pypdf 6.10.2 instalado y, además, un texto extraído al lado de cada boletín. Quien trabaje con los PDF tal como se descargan y quiera la misma verificación de citas debe instalar pypdf aparte.
En cuanto al equipo, lo que pesa son los datos. Las cuatro bases anuales ocupan en disco 2.671 MB, entre 627,8 MB la de 2025 y 726,6 MB la de 2022, y cada una tiene más de quinientas columnas. Los scripts nunca las cargan completas (la razón se explica en la sección sobre memoria), de modo que un portátil corriente basta. La instalación es la de cualquier proyecto de Python con entorno virtual:
cd geih-lab
python -m venv .venv
source .venv/bin/activate # Linux o macOS
# source .venv/Scripts/activate # Git Bash en Windows (use solo una de las dos)
python -m pip install -r requirements.txt
python -m pytest -q # comprobación del entornoEn PowerShell, la activación del entorno puede chocar con la política de ejecución de scripts de Windows. Para no depender de ella basta con llamar al intérprete del entorno por su ruta:
cd geih-lab
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe -m pytest -qAntes de escribir este capítulo se repitió la instalación en un entorno nuevo, creado solo con requirements.txt: pip freeze devolvió exactamente las doce líneas del archivo y las 136 pruebas pasaron. Esa comprobación descarta que el laboratorio dependa de algo instalado por fuera del entorno, con la salvedad de pypdf ya mencionada.
Obtener los datos#
El laboratorio necesita dos insumos de naturaleza distinta: los microdatos, que son registros de personas, y las publicaciones oficiales, que son cuadros ya agregados por el DANE. Los primeros se descargan con registro y no salen del equipo de quien los usa; los segundos son públicos y el propio laboratorio sabe bajarlos.
Según la ficha metodológica, el DANE publica los microdatos de forma anonimizada en el Archivo Nacional de Datos (ANDA) y cuenta con información desde el tercer trimestre de 2001 (Ficha metodológica GEIH V11, p. 11). Los de la GEIH están en el catálogo de mercado laboral de ese archivo (ANDA, DANE), y el capítulo 3 describe cómo se organiza. Según el instructivo de trabajo con el que se armaron las bases del libro, la descarga llega por meses y, dentro de cada mes, en archivos separados por módulos del cuestionario. Esa es la organización que supone el laboratorio, pero no se contrastó con la descarga vigente ni con la lista de módulos de cada año. Lo que el laboratorio sí exige es un archivo por año, de geih_2022.csv a geih_2025.csv, separado por comas, con punto decimal y una fila por persona y mes en la que los módulos ya están unidos en columnas. Los nombres deben ser exactamente esos, porque la función de lectura arma la ruta con el año.
La anonimización del DANE protege la identidad de los encuestados: los nombres y apellidos no están en las bases de uso público y la fecha de nacimiento se entrega sin el día (Metodología GEIH V11, p. 28). Eso no cubre lo que el encuestador escribe en las preguntas abiertas del tipo "¿cuál?", donde la auditoría encontró nombres de terceros (el capítulo 3 lo detalla). Ningún script lee esas columnas, y quien adapte el código debería mantener la misma regla.
Las cifras oficiales vienen de 22 documentos del DANE. Son 10 anexos estadísticos en Excel, 11 boletines técnicos y una presentación en PDF, que cubren las ediciones de diciembre de 2022 a 2025, las departamentales de 2023 a 2025, las de informalidad de octubre a diciembre de 2022 a 2025 y los anexos más recientes a la fecha de consulta (agosto de 2026 para el mercado laboral y el trimestre de junio a agosto de 2026 para la informalidad). No hay que buscarlos a mano: python scripts/06_oficiales.py --descargar baja desde las URL del DANE los que falten en la carpeta de anexos y los guarda con el nombre de archivo que el script espera. Además, registra en oficiales.json la URL, la fecha de publicación y el hash SHA-256 de cada documento que usó, y con eso el lector puede saber si su archivo es el mismo que leyó el libro o una edición posterior publicada con el mismo nombre.
El tercer insumo es opcional y solo lo usa 09_contenido_documental.py: el texto extraído por páginas de las metodologías del DANE. Con él, el script comprueba que cada cita textual de la línea de tiempo, de la tabla de desagregación y del árbol de clasificación aparezca en la página (o en la celda del anexo) que se indica. Esos textos no se versionan. Sin ellos el script valida igual los esquemas y reporta cuántas citas quedaron sin texto disponible; con ellos, una cita que no aparezca donde dice hace fallar la ejecución. En la corrida publicada quedaron 98 citas verificadas y ninguna sin texto, según el bloque verificacion_citas de linea_tiempo.json.
De los módulos mensuales a la base anual#
El script 00_construir_base_anual.py escribe en código el procedimiento para pasar de la descarga a la base anual. Dentro de cada mes toma como base el módulo de personas con más filas y le une los demás por las llaves que compartan entre DIRECTORIO, SECUENCIA_P, HOGAR y ORDEN; los módulos de hogar, que no tienen ORDEN, se unen por vivienda y hogar. Las columnas repetidas entre módulos, como FEX_C18, no se usan como llave: el script verifica que coincidan con la base y avisa si no. Después apila los doce meses admitiendo columnas que cambian de un mes a otro, cuenta los duplicados de la llave persona y mes y advierte si falta algún mes.
python scripts/00_construir_base_anual.py --modulos RUTA --anio 2025 \
--salida base_2025.parquet --compararLa opción --comparar contrasta la base reconstruida con la existente: columnas que sobran o faltan en cada una, número de filas y llaves presentes en una sola. Es la prueba que falta. Cuando se corrió el laboratorio no había módulos mensuales en el equipo, y el script, sin módulos, informa y termina sin error para no bloquear la corrida completa. Esta es su salida real:
$ python scripts/00_construir_base_anual.py
Módulos mensuales de ANDA no disponibles (defina --modulos o GEIH_MODULOS). Paso omitido: el laboratorio usa las bases anuales ya pegadas.Explicación del autor
Un eslabón sin validar
La lógica de pegado tiene cinco pruebas con módulos sintéticos, que verifican el apilado, el rechazo de llaves duplicadas y la detección de un factor que no coincide entre módulos. Lo que no está verificado es que la descarga real de ANDA tenga la forma que el script supone, una carpeta por mes con un CSV por módulo, ni qué módulos trae cada año. Hasta que alguien ejecute --comparar con módulos reales, el libro descansa en bases anuales cuyo pegado se auditó por su resultado (una fila por persona y mes, sin llaves duplicadas) y no por su procedimiento. El capítulo 3 describe esa auditoría.
Cómo está organizado el laboratorio#
La carpeta separa tres cosas: un módulo de funciones reutilizables, los scripts que producen salidas y las pruebas. Los scripts no repiten lógica entre sí; cuando dos de ellos necesitan leer una base o calcular una tasa, llaman a la misma función de geihlab. Esa decisión es la que permite probar una sola vez reglas que el libro usa en varios capítulos, como el relleno de ceros o la anualización.
geih-lab/
README.md
requirements.txt
run_all.py ejecuta los scripts y escribe _meta.json
geihlab/ funciones que usan los scripts
lectura.py lectura tipada de las bases y escritura de JSON
estimadores.py totales mensuales, promedios anuales y tasas
ponderacion.py media y cuantiles ponderados
informalidad.py regla oficial de informalidad (Código_STATA y Código_SAS)
precision.py CV aproximado por linealización
scripts/
00_construir_base_anual.py
01_auditar_bases.py
02_indicadores_laborales.py
03_informalidad.py
04_ingresos.py
05_factor_expansion.py
06_oficiales.py
07_validacion.py
08_magdalena_grupos.py
09_contenido_documental.py
contenido/ línea de tiempo, desagregación y árbol, escritos a mano
oficiales/cifras_oficiales.csv cifras públicas del DANE (versionado)
tests/ 12 archivos de pruebas
cache/ anexos y textos descargados (no se versiona)De los cinco módulos de geihlab, lectura.py es el que más errores evita, y por eso vuelve a aparecer en la sección de problemas frecuentes. estimadores.py define los ámbitos (total nacional, Magdalena y Santa Marta), los sexos y las dos formas de anualizar; ponderacion.py implementa el cuantil ponderado por acumulación del capítulo 7; informalidad.py traduce línea por línea el código oficial publicado en las hojas Código_STATA y Código_SAS del anexo de informalidad (Anexo informalidad oct.-dic. 2025); y precision.py calcula la aproximación del coeficiente de variación que el capítulo 8 presenta con todas sus reservas.
Las salidas no se quedan en la carpeta del laboratorio. Los JSON se escriben en next-app/data/libro-geih/, de donde los leen las figuras; la tabla de validación en CSV va a next-app/public/libro-geih/, que es la que se publica para descarga; y el registro de cifras oficiales queda en geih-lab/oficiales/cifras_oficiales.csv, versionado porque solo contiene información pública. La carpeta cache/, donde se guardan anexos y textos descargados, está excluida del control de versiones.
Configuración por variables de entorno#
Ningún script tiene rutas escritas para un equipo concreto. Todas se resuelven con variables de entorno que, si no existen, toman un valor por defecto pensado para la organización de carpetas del autor. En otro equipo lo normal es definir al menos GEIH_DATOS.
| Variable | Qué indica | Valor por defecto | La usan |
|---|---|---|---|
GEIH_DATOS | Carpeta con geih_2022.csv a geih_2025.csv | ../bases_datos/geih/datos junto a la carpeta del repositorio | 01 a 05, 08 y las pruebas sobre microdatos |
GEIH_ANEXOS | Anexos y boletines del DANE con su nombre original | geih-lab/cache/anexos | 02, 03, 05, 06, 09 y las pruebas de cifras oficiales |
GEIH_SALIDA | Dónde se escriben los JSON y el manifiesto | next-app/data/libro-geih | 01 a 09, run_all.py y las pruebas que leen salidas |
GEIH_MODULOS | Módulos mensuales de ANDA | Sin valor | 00 |
GEIH_FUENTES | Texto por páginas de las metodologías | geih-lab/cache/fuentes | 09 y sus pruebas |
Fuente: geih-lab/geihlab/lectura.py, scripts/00_construir_base_anual.py y scripts/09_contenido_documental.py.
Nota: Las rutas por defecto son relativas a la carpeta del repositorio. GEIH_SALIDA no cambia el destino de los dos CSV, que se escriben siempre en su ubicación del repositorio.
export GEIH_DATOS="$HOME/geih/datos" # geih_2022.csv a geih_2025.csv
export GEIH_ANEXOS="$HOME/geih/anexos" # anexos y boletines del DANE
export GEIH_SALIDA="$HOME/geih/salida" # opcional: JSON fuera del repositorio
export GEIH_FUENTES="$HOME/geih/fuentes" # opcional: texto de las metodologías$env:GEIH_DATOS = "D:\geih\datos"
$env:GEIH_ANEXOS = "D:\geih\anexos"
$env:GEIH_SALIDA = "D:\geih\salida"
$env:GEIH_FUENTES = "D:\geih\fuentes"Con su valor por defecto, GEIH_SALIDA hace que una corrida reescriba los JSON que lee el libro, que es lo que se quiere al regenerarlo pero no al experimentar; para probar un cambio sin tocar nada, lo prudente es apuntarla a una carpeta aparte. Aun así, dos archivos no obedecen a esa variable: 06_oficiales.py escribe siempre geih-lab/oficiales/cifras_oficiales.csv y 07_validacion.py escribe siempre next-app/public/libro-geih/validacion.csv. Si se trabaja sobre una copia del repositorio, git status muestra si cambiaron y git restore con la ruta del archivo los devuelve a la versión publicada.
Ejecutar de punta a punta#
run_all.py busca en scripts/ los archivos que empiezan por dos dígitos y un guion bajo, los ordena y los ejecuta uno tras otro con el mismo intérprete y las mismas variables de entorno. No se detiene si uno falla: sigue con el resto, anota el error en el manifiesto y termina con código de salida 1. Ese comportamiento obliga a mirar la salida completa y no solo la última línea.
El orden numérico tiene una consecuencia práctica en la primera corrida. Los scripts 02, 03 y 05 leen anexos del DANE para sus controles, y corren antes que 06, que es el que sabe descargarlos. Si la carpeta de anexos está vacía, 02 y 05 omiten el control y lo anotan, 03 deja vacíos los campos oficiales de informalidad, y 06 falla con un mensaje que pide definir GEIH_ANEXOS o usar --descargar. Por eso la secuencia recomendada empieza por la descarga:
python scripts/06_oficiales.py --descargar # una vez: baja los 22 documentos del DANE
python run_all.py # scripts 00 a 09 en orden y _meta.json
python -m pytest -q # las 136 pruebas
python run_all.py --solo 03 07 # solo algunos scripts
python run_all.py --manifiesto # solo reescribe _meta.jsonCon --solo se corren algunos scripts por su prefijo, lo que ahorra tiempo cuando se cambia uno solo. El costo es que run_all.py reescribe igual el manifiesto, y su bloque de ejecución queda con los scripts de esa corrida parcial; con --manifiesto ese bloque queda vacío. Para que el manifiesto describa el laboratorio completo, la última corrida antes de publicar tiene que ser la completa. En la corrida registrada en el manifiesto, del 21 de octubre de 2026, los diez scripts terminaron con estado "ok" y sumaron 102,1 segundos; el más lento fue 03_informalidad.py, con 41,6 segundos, porque clasifica a todos los ocupados de los cuatro años con las dos versiones del código oficial.
Qué produce cada script#
| Script | Qué hace | Produce | Segundos |
|---|---|---|---|
00_construir_base_anual.py | Une los módulos mensuales de ANDA y apila los doce meses. Sin módulos informa y termina sin error. | Nada si no hay módulos | 0,3 |
01_auditar_bases.py | Filas, columnas, llaves, hogares por vivienda e identidades de la clasificación laboral. | columnas_por_anio.json, estructura_microdatos.json | 5,7 |
02_indicadores_laborales.py | PT, PET, FT, OC, DS, PFFT, TGP, TO y TD mensuales y anuales, con y sin factor, por ámbito y sexo, con su cota de CV. | indicadores_laborales.json, poblaciones_2025.json, ponderado_vs_simple.json | 26,6 |
03_informalidad.py | Regla oficial de informalidad traducida del código del anexo, por posición, sexo y clase. | informalidad.json | 41,6 |
04_ingresos.py | Media y percentiles ponderados de INGLABO entre ocupados con ingreso positivo; histograma. | ingresos_cuantiles.json, ingresos_histograma.json | 5,7 |
05_factor_expansion.py | Distribución de FEX_C18 y control de la suma mensual frente a la población del anexo. | fex_distribucion.json | 2,8 |
06_oficiales.py | Cifras oficiales de anexos y boletines con documento, hoja, celda o página y fecha. | oficiales.json, precision_oficial.json, geih-lab/oficiales/cifras_oficiales.csv | 12,0 |
07_validacion.py | Une lo propio con lo oficial, calcula diferencias y asigna el estado de cada fila. | validacion.json, next-app/public/libro-geih/validacion.csv | 0,5 |
08_magdalena_grupos.py | Magdalena por grupos de edad y educación fijados de antemano, con regla de publicación. | magdalena_grupos.json | 6,0 |
09_contenido_documental.py | Valida la línea de tiempo, la tabla de desagregación y el árbol de clasificación, y verifica sus citas. | arbol_clasificacion.json, desagregacion.json, linea_tiempo.json | 0,9 |
Fuente: Bloques ejecucion y archivos del manifiesto next-app/data/libro-geih/_meta.json; descripciones tomadas de la documentación de cada script.
Nota: La duración depende del equipo. Los JSON se escriben en la carpeta de GEIH_SALIDA; los dos CSV, en su ruta fija.
Antes de correr un script suelto hay que conocer las pocas dependencias que hay entre ellos. Los scripts 01 a 05 y 08 parten de los microdatos; de ellos, 02, 03 y 05 consultan además los anexos para sus controles, si están disponibles. El 06 trabaja solo con los documentos oficiales. El 07 no lee microdatos: une los JSON que dejaron 02, 03, 04 y 06 en la carpeta de salida, de modo que, si se corre después de cambiar el código de informalidad sin haber corrido 03, comparará los resultados viejos. El 09 tampoco lee microdatos: valida el contenido escrito a mano a partir de los documentos, consulta los textos de metodologías y boletines solo para verificar citas y copia el resultado a la carpeta de salida.
El manifiesto de la corrida#
Cada corrida termina escribiendo _meta.json, la constancia de con qué se produjeron los datos: fecha, versión de Python y de las librerías principales, commit del repositorio, si la carpeta del laboratorio tenía cambios sin guardar en el control de versiones y, para cada script, su estado, su duración y el hash SHA-256 de su código. Para cada JSON registra además qué script lo produjo, su fecha y su tamaño. El extracto siguiente sale del manifiesto publicado con el libro, sin cambiar ningún valor, con solo dos de las 10 entradas de ejecución y uno de los 16 archivos.
{
"generado": "2026-10-21T17:23:55",
"python": "3.14.2",
"dependencias": {
"polars": "1.39.3",
"numpy": "2.5.2",
"openpyxl": "3.1.5",
"xlrd": "2.0.2",
"pytest": "9.0.2",
"pypdf": "6.10.2"
},
"commit": "08645254ae22591629c96013b81f84f66136bae4",
"laboratorio_con_cambios_sin_commit": false,
"ejecucion": [
{
"script": "geih-lab/scripts/06_oficiales.py",
"estado": "ok",
"segundos": 12,
"sha256": "dddae37bd3d465b8473fde6b7ab6f35c8ed8c80e40050178b3a06541bdb0ed65"
},
{
"script": "geih-lab/scripts/07_validacion.py",
"estado": "ok",
"segundos": 0.5,
"sha256": "675158b920c0835df6578f22a0f027742849735ad4ed25112354f1f154cacc45"
}
],
"archivos": [
{
"archivo": "validacion.json",
"id": "validacion",
"script": "geih-lab/scripts/07_validacion.py",
"generado": "2026-10-21",
"sha256_script": "675158b920c0835df6578f22a0f027742849735ad4ed25112354f1f154cacc45",
"bytes": 1547448
}
]
}El campo laboratorio_con_cambios_sin_commit vale false en esta corrida, así que el commit registrado identifica el código. El hash permite además comprobar script por script que su copia es la misma que produjo los datos del libro:
$ sha256sum scripts/07_validacion.py
675158b920c0835df6578f22a0f027742849735ad4ed25112354f1f154cacc45 *scripts/07_validacion.pyEn PowerShell el equivalente es Get-FileHash scripts\07_validacion.py -Algorithm SHA256. Al revisar este capítulo, el 21 de octubre de 2026, se calcularon los hashes de los diez scripts tal como están guardados en el repositorio, y coincidieron todos con los del manifiesto. Esa comprobación cubre los scripts, no el módulo geihlab, cuyos archivos no tienen hash en el manifiesto: para ellos la única referencia es el commit.
Las pruebas#
El laboratorio tiene 136 pruebas repartidas en doce archivos. Unas trabajan con datos sintéticos o con casos de resultado conocido, y comprueban que las funciones hacen lo que dicen; otras leen los microdatos reales y comprueban propiedades que el libro da por ciertas, como que la fuerza de trabajo sea la suma de ocupados y desocupados en cada fila o que la llave de persona y mes no se repita.
| Archivo | Pruebas | Qué comprueban |
|---|---|---|
test_microdatos.py | 46 | Identidades FT = OCI + DSI y PET = FT + FFT, PET desde 15 años, 12 meses, llave persona-mes única, factor constante en la vivienda-mes, Santa Marta como cabecera, P3271 con dos categorías. |
test_oficiales.py | 14 | Lectura de celdas por rótulo, columnas mensuales y trimestrales, fechas, derivados anuales y verificación de citas de boletines. |
test_informalidad.py | 12 | Regla oficial en sus versiones Stata y SAS, informalidad nacional exacta, relleno de OFICIO_C8 y regla de contraste no oficial. |
test_ponderacion.py | 11 | Cuantiles ponderados con casos de resultado conocido, equivalencia con replicar filas y contraste con econ_utils_co si está instalado. |
test_contenido.py | 10 | Esquema de los archivos documentales, rechazo de rayas largas y de citas falsas, recorrido de los casos del árbol. |
test_validacion.py | 9 | Regla de estados, tolerancias, causas con cita y ausencia de diferencias sin explicar. |
test_precision.py | 8 | CV aproximado frente a una implementación manual, muestreo aleatorio simple y simulación de Monte Carlo. |
test_ingresos.py | 7 | INGLABO sin códigos 98 y 99, histograma completo, segunda implementación idéntica. |
test_estimadores.py | 6 | Totales mensuales, tasas, las dos anualizaciones y exigencia de 12 meses con datos sintéticos. |
test_construir_base.py | 5 | Pegado y apilado de módulos sintéticos, llaves duplicadas y factor inconsistente entre módulos. |
test_lectura.py | 4 | Lectura tipada, columna ausente, 12 meses y escritura de JSON con meta completo. |
test_magdalena_grupos.py | 4 | Grupos de edad y educación disjuntos y completos que suman la PET. |
Fuente: python -m pytest --collect-only -q sobre geih-lab/tests/, ejecutado el 21 de octubre de 2026.
Con los microdatos y los anexos disponibles, la salida esperada es la de abajo. El tiempo cambia de un equipo a otro y de una corrida a otra; en un entorno recién creado, la misma batería tardó cerca de 17 segundos.
$ python -m pytest -q
........................................................................ [ 52%]
................................................................ [100%]
136 passed in 28.90sSin microdatos ni anexos, las pruebas que los necesitan se omiten con un aviso explícito en lugar de fallar, y las demás corren igual. Así se puede comprobar la instalación antes de descargar los casi 2,7 GB de las cuatro bases:
$ GEIH_DATOS=vacio GEIH_ANEXOS=vacio GEIH_FUENTES=vacio python -m pytest -q -rs
...
SKIPPED [4] tests\test_microdatos.py:15: Microdatos GEIH no disponibles (defina GEIH_DATOS)
...
SKIPPED [1] tests\test_oficiales.py:102: Anexos del DANE no disponibles (defina GEIH_ANEXOS o use --descargar)
...
77 passed, 59 skipped in 1.87sQue las pruebas pasen no significa que las cifras estén validadas. Una prueba verifica una propiedad del código o de los datos; la validación compara una cifra con su referente oficial y la clasifica. Las dos cosas se complementan: hay pruebas que leen la tabla de validación y fallan si aparece una sola fila en estado "diferencia sin explicar", pero el estado de cada fila lo decide 07_validacion.py con su propia regla, y es esa regla la que el lector debe revisar.
Comparar su corrida con la tabla del libro#
La tabla de validación del libro se descarga en /libro-geih/validacion.csv. Tiene 1.347 filas, una por cifra contrastada, y 37 columnas. Las que más se usan son id, que identifica la cifra de forma estable entre corridas; propio, oficial, diferencia y tolerancia; estado; y el localizador del referente: oficial_documento, oficial_hoja, oficial_celda u oficial_pagina. Las columnas causa_texto y causa_cita explican las diferencias, motivo_sin_referente dice por qué una cifra no tiene con qué compararse, y variables, filtros, universo, factor y metodo repiten en cada fila cómo se calculó.
| Estado | Criterio | Filas |
|---|---|---|
| Coincide | Diferencia menor que la tolerancia frente a un valor oficial con al menos cuatro decimales y el mismo método. | 1.056 |
| Coincide al redondeo | El valor oficial solo se publica redondeado (a un decimal o en miles enteros) y el propio redondea igual. No es una validación plena. | 28 |
| Diferencia explicada | Hay diferencia y su causa está documentada con fuente. | 108 |
| Diferencia sin explicar | Hay diferencia sin causa documentada; la cifra no se usa como resultado. | 0 |
| Sin referente oficial | El DANE no publica una cifra comparable; es una estimación propia. | 155 |
Fuente: Cálculo propio, next-app/data/libro-geih/validacion.json (script geih-lab/scripts/07_validacion.py).
Nota: De las 1.056 filas que coinciden, 756 se comparan con una celda del anexo y 300 con un valor derivado de celdas del anexo. La regla de estados es del autor.
La nota de la tabla 10.4 contiene la distinción que más importa al leerla. Las 300 filas que coinciden contra un valor derivado son todas anuales: poblaciones y tasas del total nacional, en total y por sexo, y la informalidad del total nacional (también por posición, sexo y clase) y de Santa Marta. El laboratorio las compara con una anualización que él mismo hace sobre celdas del anexo: el promedio de los doce totales mensuales, la razón de esos promedios o el promedio de las doce tasas en el caso del mercado laboral, y los cuatro trimestres calendario de la serie de trimestres móviles en el de la informalidad. Como el DANE no documenta su estimador anual nacional, esa coincidencia confirma la aritmética y no el procedimiento oficial. Cuando la misma cifra aparece además impresa en un boletín o en la presentación de resultados, con un decimal, se contrasta en una fila aparte que solo puede coincidir al redondeo; esas son las 28 filas de ese estado. El capítulo 5 desarrolla el punto con las tasas de 2022 a 2025.
Para revisar la tabla basta polars, que ya está en el entorno. El programa siguiente cuenta las filas por estado y extrae las dos que se usan en los ejercicios del final; se ejecutó sobre el CSV publicado y esta es su salida:
import polars as pl
val = pl.read_csv("validacion.csv", infer_schema_length=0).with_columns(
pl.col("propio", "oficial", "diferencia", "tolerancia").cast(pl.Float64)
)
# 1. Filas por estado
print(val.group_by("estado").len().sort("len", descending=True))
# 2. Las dos filas de los ejercicios del capítulo
ids = [
"informalidad_nacional.total-nacional.prop_inf.2023.oficial",
"santa_marta_anual.santa-marta.td.2024.razon_de_promedios",
]
print(
val.filter(pl.col("id").is_in(ids))
.select("indicador", "ambito", "periodo", "propio", "oficial", "diferencia", "estado")
)shape: (4, 2)
┌──────────────────────┬──────┐
│ estado ┆ len │
│ --- ┆ --- │
│ str ┆ u32 │
╞══════════════════════╪══════╡
│ coincide ┆ 1056 │
│ sin referente ┆ 155 │
│ diferencia explicada ┆ 108 │
│ coincide al redondeo ┆ 28 │
└──────────────────────┴──────┘
shape: (2, 7)
┌───────────┬────────────────┬─────────┬─────────┬───────────┬────────────┬──────────┐
│ indicador ┆ ambito ┆ periodo ┆ propio ┆ oficial ┆ diferencia ┆ estado │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ f64 ┆ f64 ┆ f64 ┆ str │
╞═══════════╪════════════════╪═════════╪═════════╪═══════════╪════════════╪══════════╡
│ TD ┆ Santa Marta ┆ 2024 ┆ 9.6252 ┆ 9.625234 ┆ -0.000034 ┆ coincide │
│ PROP_INF ┆ Total nacional ┆ 2023 ┆ 56.3626 ┆ 56.362624 ┆ -0.000024 ┆ coincide │
└───────────┴────────────────┴─────────┴─────────┴───────────┴────────────┴──────────┘La prueba de reproducción propiamente dicha es comparar la tabla que generó el lector con la publicada. La del libro debe descargarse antes de correr el laboratorio, porque 07_validacion.py sobrescribe la copia del repositorio. El cruce por id encuentra filas que existen en una sola versión y filas cuya cifra o estado cambió:
import polars as pl
def leer(ruta: str) -> pl.DataFrame:
return pl.read_csv(ruta, infer_schema_length=0).select(
"id", "estado", pl.col("propio").cast(pl.Float64)
)
libro = leer("validacion_publicada.csv") # descargada de /libro-geih/validacion.csv
mia = leer("mia/validacion.csv") # la que escribió 07_validacion.py en su equipo
cruce = libro.join(mia, on="id", how="full", suffix="_mia", coalesce=True)
solo_una = cruce.filter(pl.col("estado").is_null() | pl.col("estado_mia").is_null())
distintas = cruce.filter(
((pl.col("propio") - pl.col("propio_mia")).abs() > 1e-9)
| pl.col("estado").ne_missing(pl.col("estado_mia"))
)
print("filas en una sola versión:", solo_una.height)
print("filas con cifra o estado distinto:", distintas.height)
print(distintas.head(20))filas en una sola versión: 0
filas con cifra o estado distinto: 0
shape: (0, 5)
┌─────┬────────┬────────┬────────────┬────────────┐
│ id ┆ estado ┆ propio ┆ estado_mia ┆ propio_mia │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ f64 ┆ str ┆ f64 │
╞═════╪════════╪════════╪════════════╪════════════╡
└─────┴────────┴────────┴────────────┴────────────┘Si el cruce devuelve diferencias, el orden de revisión es el inverso al de la cadena. Primero, los documentos oficiales: un hash distinto en oficiales.json indica otra edición del anexo, y el DANE revisa series entre ediciones. Después, los microdatos: el número de filas y columnas por año que registra estructura_microdatos.json debe coincidir con el de las bases del lector. Luego, las versiones de las dependencias que registra el manifiesto. Por último, el código. Una diferencia en el sexto decimal no cambia el estado de una fila, porque la tolerancia es de 0,0001 puntos para tasas y de 0,001 miles de personas para poblaciones; una diferencia que sí lo cambia rara vez es de redondeo, y el capítulo 8 explica cómo buscar su causa.
Resultado reproducido
Calculado con los microdatos de la GEIH por el laboratorio del libro.
Lo que debe coincidir con las bases
Las bases con que se calculó el libro tienen, en 2025, 817.550 filas de personas y mes, 514 columnas y 627.847.992 bytes; en 2022, 919.459 filas y 534 columnas. Son conteos de la auditoría de 01_auditar_bases.py, sin referente oficial con el cual compararlos. Si las bases del lector difieren en filas o columnas, no son las mismas y ninguna comparación posterior tiene sentido hasta aclarar por qué.
Ficha del resultado · Estructura de las bases anuales, 2022 a 2025
- Periodo
- 2022-01 a 2025-12
- Ámbito
- Total nacional (archivo completo de cada año)
- Microdatos
- GEIH marco 2018, bases anuales apiladas 2022-2025 (geih_AAAA.csv)
- Variables
DIRECTORIO, SECUENCIA_P, HOGAR, ORDEN, MES, PER, PERIODO, REGIS, FEX_C18, P6040, P6050, PT, PET, FT, FFT, OCI, DSI, DPTO, AREA, CLASE, P3271- Filtros
- Ninguno: todas las filas de cada archivo.
- Universo
- Registros persona-mes de las bases públicas.
- Factor
- No aplica (conteos muestrales); FEX_C18 solo se usa en controles de constancia.
- Método
- Conteos sobre el archivo leído como texto con casteo explícito. Llave persona-mes DIRECTORIO + SECUENCIA_P + ORDEN; hogar-mes DIRECTORIO + SECUENCIA_P; vivienda DIRECTORIO. hogares_por_vivienda: número de hogares (k) por vivienda y cantidad de viviendas (n). personas_por_hogar: número de personas (k) por hogar-mes y cantidad de hogares (n). Los controles cuentan filas que violan cada regla (0 = se cumple en todo el archivo).
- Código
geih-lab/scripts/01_auditar_bases.py· commit 0864525
Advertencias
- Las bases anuales ya vienen con los módulos mensuales unidos; esta auditoría verifica el resultado del pegado, no el procedimiento.
- PER es el año de la encuesta, no un identificador de persona.
- La lectura de PERIODO como año, mes y semana es empírica: los caracteres 5 y 6 coinciden con MES.
- DPTO y AREA vienen sin cero a la izquierda en el archivo; aquí se rellenan a dos caracteres.
- Número de códigos distintos de AREA por año: 2022: 24, 2023: 32, 2024: 32, 2025: 32. En 2022 las filas de Amazonía y Orinoquía no tienen AREA; desde 2023 sí, así que una serie por ciudad cambia de dominio entre 2022 y 2023.
- Santa Marta se identifica con AREA = 47 (cabecera municipal); el control santa_marta_* comprueba que esas filas son del Magdalena (DPTO 47) y de cabecera (CLASE 1).
- P3271 (sexo al nacer) es la única variable de sexo en las cuatro bases; P6020 no existe en ellas.
Problemas frecuentes#
Todos los problemas de esta sección aparecieron al auditar las bases o al escribir el laboratorio, y casi todos producen cifras plausibles en lugar de errores visibles, que es lo que los vuelve peligrosos. Varios ya se trataron en el capítulo 3 desde el punto de vista de los datos; aquí interesa cómo los resuelve el código y qué hacer si reaparecen.
Codificación del texto#
Las bases son texto ASCII salvo en algunas columnas de respuesta abierta, donde aparecen bytes en Latin-1, como los de la Ñ, y una lectura en UTF-8 estricto falla en esas líneas. El laboratorio usa encoding="utf8-lossy", que reemplaza esos bytes por un carácter de sustitución; para las columnas numéricas es inocuo, y las de texto libre no se usan. Quien necesite texto libre debe leer esas columnas como Latin-1 (capítulo 3).
El segundo problema de codificación aparece en la consola de Windows y no afecta a los datos. Cuando la salida de un script se redirige a un archivo o a otro programa, los acentos de los mensajes pueden aparecer como M�dulos en lugar de Módulos. Ocurrió al preparar este capítulo y se corrige con el modo UTF-8 de Python, python -X utf8 run_all.py, o definiendo la variable PYTHONUTF8=1. Los archivos que escribe el laboratorio no se ven afectados: siempre se guardan en UTF-8.
Ceros a la izquierda#
Varias columnas que son códigos llegan al archivo como si fueran números y pierden el cero inicial. Les pasa a DPTO, AREA y RAMA2D_R4, que son de dos caracteres, y a OFICIO_C8, que es de cuatro y en el grupo de las Fuerzas Militares llega con tres. Sobre el texto original, el filtro DPTO == "05" no encuentra ninguna fila de Antioquia, y no da error. Por eso la lectura nunca deja que polars adivine tipos: lee todo como texto con infer_schema_length=0, trata el vacío y el punto aislado como faltantes y castea cada columna por su nombre.
lf = (
pl.scan_csv(
ruta,
encoding="utf8-lossy",
infer_schema_length=0,
null_values=["", "."],
)
.select(presentes)
.with_columns([_expr_tipo(c).alias(c) for c in presentes])
)
def _expr_tipo(col: str) -> pl.Expr:
c = pl.col(col)
if col in INDICADORAS:
return c.str.strip_chars().cast(pl.Int8).fill_null(0)
if col in _ENTEROS:
return c.str.strip_chars().cast(_ENTEROS[col])
if col in _DECIMALES:
return c.str.strip_chars().cast(pl.Float64)
if col in _RELLENO_2:
return c.str.strip_chars().str.zfill(2)
if col in _RELLENO_4:
return c.str.strip_chars().str.zfill(4)
return cEl caso de OFICIO_C8 muestra lo que cuesta equivocarse en una sola línea. En algunas ramas, la regla oficial de ocupación informaltoma los dos primeros dígitos del oficio, lo que supone un código de cuatro caracteres. Si se rellena a ocho, todos los códigos empiezan por "00" y la clasificación cambia para los trabajadores por cuenta propia y los patronos cuya situación la regla resuelve por el oficio. En lugar de confiar en que nadie cometa el error, el script de informalidad lo reproduce a propósito en cada año y guarda el contraste en el bloque oficio_c8 de informalidad.json; una prueba de test_informalidad.py comprueba además el efecto en 2025.
Resultado reproducido
Calculado con los microdatos de la GEIH por el laboratorio del libro.
Informalidad nacional de 2025 con el oficio mal rellenado
Con el relleno correcto a cuatro caracteres, la proporción de informalidad nacional de 2025 es 55,7188 %, la misma que se deriva del anexo oficial. Con el relleno a ocho baja a 55,4702 %, una diferencia de −0,2487 p.p. producida por 724 filas de ocupados que cambian de clasificación. Sin relleno alguno la proporción queda en 55,7188 %, igual a la correcta, por razones que el capítulo 6 explica y que no garantizan nada en otras bases. El bloque oficio_c8 no tiene referente oficial propio: es un contraste interno del laboratorio.
Ficha del resultado · Efecto del relleno de OFICIO_C8 en la proporción de informalidad, 2025
- Periodo
- 2022 a 2025 (enero a diciembre de cada año)
- Ámbito
- Total nacional; Magdalena (DPTO 47); Santa Marta (AREA 47, cabecera)
- Microdatos
- GEIH marco 2018, bases anuales 2022 a 2025 (12 meses cada una)
- Variables
OCI, FEX_C18, MES, DPTO, AREA, CLASE, P3271, P6430, P6100, P6110, P6450, P6920, P6930, P6940, RAMA2D_R4, P3045S1, P3046, P3069, P6765, P3065, P3066, P3067, P3067S1, P3067S2, P6775, P3068, OFICIO_C8, PER- Filtros
- OCI == 1. Magdalena: DPTO == '47' tras rellenar a 2 caracteres. Santa Marta: AREA == '47'.
- Universo
- Población ocupada
- Factor
- FEX_C18, promedio de los 12 totales mensuales
- Método
- Regla oficial: traducción línea a línea de la hoja Código_STATA del anexo anex-GEIHEISS-oct-dic2025.xlsx (actualizado el 12 de febrero de 2026); EI = 0 informal. Se verificó que la hoja Código_SAS clasifica igual. Proporción = informales / ocupados, con totales anuales como promedio de los totales mensuales. Oficial derivado: promedio de los cuatro trimestres calendario (ene-mar, abr-jun, jul-sep, oct-dic) de la serie de trimestres móviles del anexo. Regla no_cotiza (NO oficial): formal si P6100 == 1 y P6920 == 1, para todos los ocupados.
- Código
geih-lab/scripts/03_informalidad.py· commit 0864525
Advertencias
- La regla 'no_cotiza' no es la definición del DANE: mide ausencia de afiliación contributiva a salud y de cotización a pensión. Se publica solo como contraste.
- P6430: 7 = jornalero o peón y 8 = otro es una inferencia (el código DANE trata el 7 como asalariado y el 8 como informal; el anexo publica esas dos categorías). Ver bloque inferencia_p6430.
- Magdalena no tiene cifra oficial de informalidad publicada: es estimación propia con el factor mensual, sin la calibración anual departamental.
- Las cifras oficiales por posición y sexo son solo del total nacional; las de clase, del total nacional; Santa Marta solo en total.
- cv_cota_inf es la aproximación propia de geihlab.precision (vivienda como conglomerado, DPTO x CLASE como pseudoestrato), no el CV del DANE; solo se calcula para la regla oficial. Vale 0 cuando la categoría es informal por definición (familiares sin remuneración, otro) y queda vacío cuando la proporción es 0 (empleados del gobierno, formales por definición).
- La informalidad del rediseño solo es comparable desde 2021 (boletín oct-dic 2025, p. 3) y la serie anual 2021 y 2022 fue revisada entre ediciones del boletín.
- OFICIO_C8 llega con 4 caracteres en más del 99 % de los ocupados. Los de 3 caracteres son el grupo 0 sin su cero (110, 210 y 310, Fuerzas Militares), más un registro '612' en 2024 cuya clasificación no depende del oficio; hay además un código '0' (bloque oficio_c8_formato). La regla toma los dos primeros caracteres de un código de 4: hay que rellenar a 4. Rellenar a 8 vuelve '00' a todos los códigos y baja la proporción (bloque oficio_c8).
- La línea de la fila 65 del Código_STATA termina con un operador '|' suelto; se tradujo sin él, como la línea equivalente del Código_SAS (fila 57).
Un cuarto de punto no se explica por redondeo, pero se puede confundir con una revisión de la serie entre ediciones, como las que el capítulo 8 documenta para la informalidad. El capítulo 6 presenta el error dentro de la regla, el 8 lo usa para mostrar cómo se diagnostica una discrepancia pequeña y el ejercicio 10.3 lo retoma con los datos de 2022.
Faltantes, indicadoras y llaves#
Además del vacío, algunas columnas usan un punto aislado como valor faltante. Si no se declara como nulo, el casteo a número falla, y si nadie castea la columna, se queda como texto. Las indicadoras de la clasificación laboral, PT, PET, FT, FFT, OCI y DSI, valen 1 o vacío, y el vacío significa "no pertenece": la lectura lo convierte en 0 para que las sumas ponderadas funcionen.
Con las llaves el error típico es tomar PER por un identificador de persona, cuando es el año de la encuesta y toma un único valor en cada archivo. La persona y mes se identifica con DIRECTORIO, SECUENCIA_P y ORDEN; DIRECTORIO y ORDEN solos no bastan porque hay viviendas con más de un hogar, y en 2025 esa combinación tiene 3.705 duplicados. El laboratorio agrega MES a la llave como resguardo, aunque en estas bases no es necesario.
Queda el caso de los meses incompletos. Los totales anuales del libro son el promedio de doce totales mensuales, y ese promedio deja de ser válido si falta un mes. Por eso leer_geih falla si una base no trae los doce, en lugar de devolver un promedio de once que nadie notaría.
Memoria, tiempo y rutas largas#
Leer una base anual completa como texto, con sus más de quinientas columnas, es la forma más rápida de agotar la memoria de un portátil. Ningún script lo hace. leer_geih abre el archivo con scan_csv, que no lee nada todavía, selecciona solo las columnas pedidas y recién entonces castea y recoge. El script de indicadores, por ejemplo, pide catorce columnas. Quien escriba su propio análisis debería seguir el mismo patrón: declarar primero qué columnas necesita y pedir solo esas.
Las rutas de Windows son la otra fuente de problemas en este proyecto. El límite clásico de una ruta es de 260 caracteres, y en la máquina del autor, con el repositorio dentro de una carpeta sincronizada de nombre largo, el archivo más profundo del laboratorio llega a 227. Un entorno virtual creado dentro de geih-lab añade rutas todavía más largas. Si la instalación o la lectura fallan con un error de archivo no encontrado sobre una ruta que sí existe, lo primero es mover el repositorio a una carpeta corta o crear el entorno virtual fuera de él.
Anexos, hojas y citas#
Los anexos del DANE están hechos para leerse en pantalla, no para procesarse. Combinan series mensuales y de trimestres móviles, ponen llamadas de nota junto a los rótulos, tienen rótulos que empiezan igual, como "Fuerza de trabajo potencial" y "Fuerza de trabajo", y algunas hojas llevan el nombre con un espacio al final, como "Grandes dominios " en el anexo de informalidad. 06_oficiales.py no lee ninguna celda por posición fija: normaliza los nombres de hoja y los rótulos (tildes, espacios y notas), ubica la fila por su rótulo y la columna por su encabezado. Si un rótulo no aparece, falla con un mensaje que lo nombra, en lugar de leer una celda vecina. Cuando el lector descargue una edición más reciente y el script falle, ese mensaje es el punto de partida.
Con los boletines el riesgo es distinto. Las cifras que se toman de su texto están redondeadas a un decimal, y el script verifica que la frase citada aparezca en la página indicada. Como se dijo en la sección de requisitos, sin los textos extraídos ni pypdf esa verificación se omite y se anota; con cualquiera de los dos, una cita que no aparezca detiene el script. Lo segundo es preferible, porque una cifra de boletín sin verificar es justo el tipo de dato que este libro procura no publicar.
Adaptar el código a otro departamento#
El laboratorio está escrito para el total nacional, el Magdalena y Santa Marta, pero nada en él es específico del Caribe salvo los códigos. Los ámbitos se definen en geihlab/estimadores.py:
AMBITOS: dict[str, pl.Expr] = {
"Total nacional": pl.lit(True),
"Magdalena": pl.col("DPTO") == "47",
"Santa Marta": pl.col("AREA") == "47",
}Cambiar el departamento exige más que editar ese diccionario. El código "47" aparece también en los filtros propios de 02_indicadores_laborales.py (la base para la aproximación del CV y la comprobación de que la ciudad es cabecera del departamento), 03_informalidad.py, 04_ingresos.py, 05_factor_expansion.py, 08_magdalena_grupos.py y los controles de 01_auditar_bases.py. El nuevo código debe escribirse con sus dos caracteres, porque la lectura rellena DPTO a dos. Para una ciudad, AREA identifica el área y hay que comprobar, como hace el laboratorio con Santa Marta, que esas filas sean todas de cabecera del departamento correspondiente. La auditoría de las bases registra además que en 2022 las filas de las ciudades de la Amazonía y la Orinoquía no tienen AREA y desde 2023 sí, de modo que una serie de esas ciudades cambia de dominio entre los dos años.
La validación no se adapta sola. 06_oficiales.py extrae del anexo departamental las celdas del Magdalena; para otro departamento hay que agregar su extracción, y lo esperable es que las cifras anuales no coincidan, por una razón que la metodología deja escrita: el DANE hace dos calibraciones, una mensual por regiones y otra anual por departamentos (Metodología GEIH V11, p. 20), un pasaje que el capítulo 9 transcribe y discute.
Las bases públicas traen un solo factor, FEX_C18, que el laboratorio trata como el de la calibración mensual. Esa es la explicación que el libro adopta para que las cifras anuales propias de un departamento difieran de las publicadas, como ocurre con el Magdalena: en 2024 su tasa de desocupación propia es 9,5386 % frente a 9,6956 % en el anexo, una diferencia de −0,1570 p.p. que la tabla de validación clasifica como explicada por esa causa. El capítulo 9 documenta el caso completo.
Ficha del resultado · TD del Magdalena, 2024Diferencia explicada
- Periodo
- 2024
- Ámbito
- Magdalena
- Microdatos
- GEIH marco 2018, bases anuales apiladas 2022-2025 (geih_AAAA.csv)
- Variables
DIRECTORIO, MES, DPTO, AREA, CLASE, P3271, P6040, FEX_C18, PT, PET, FT, FFT, OCI, DSI- Filtros
- Ámbito: ninguno (nacional), DPTO == '47' (Magdalena), AREA == '47' (Santa Marta). Sexo: P3271 == 1 (hombres), P3271 == 2 (mujeres).
- Universo
- PT: todas las personas; PET: PET == 1 (15 años y más); FT: FT == 1; OC: OCI == 1; DS: DSI == 1; PFFT: FFT == 1. TGP y TO sobre la PET; TD sobre la FT.
- Factor
- FEX_C18 (factor mensual), promedio de 12 totales mensuales
- Método
- Mensual: suma de FEX_C18 por indicadora en el mes; tasas como cociente de totales. Anual, metodo_anual = promedio_de_totales para poblaciones y razon_de_promedios para tasas (cociente de los promedios anuales); metodo_anual = promedio_de_tasas: promedio simple de las 12 tasas mensuales. n: filas-mes del universo del indicador (denominador para tasas). cv_cota_inf: geihlab.precision.cv_aproximado; para PT no se calcula y para promedio_de_tasas tampoco.
- Anualización
- Cociente de los promedios anuales de numerador y denominador
- Muestra
- 15.324 filas-mes en el universo del indicador
- Código
geih-lab/scripts/02_indicadores_laborales.py· commit 0864525
Contraste con la cifra oficial
| Propio | 9,5386 % |
|---|---|
| Oficial | 9,6956 % (celda del anexo) |
| Diferencia | −0,1570 (tolerancia 0,0001) |
| CV aproximado | 3,33 % (cota inferior, sin el diseño completo) |
| Dentro del IC oficial | Sí |
Hay diferencia y su causa está documentada con fuente.
Causa: Las cifras departamentales del DANE se expanden con una segunda calibración, anual y por departamentos, que no viene en la base pública. El archivo solo trae FEX_C18, que según la evidencia de los datos corresponde a la calibración mensual por regiones (ningún documento revisado lo afirma), y con él la población total propia del Magdalena ya difiere de la publicada: los factores no son los mismos.
DANE, Metodología General GEIH, versión 11 (2023), p. 20: "se tienen dos calibraciones la primera se hace mensualmente y toma en cuenta las regiones y la segunda se hace una vez al año y toma en cuenta los departamentos".
Evidencia: Población total 2024: propia 1501.900 mil frente a 1489.368 mil publicada (+12.532 mil).
Queda la precisión. El diseño fija para la tasa de desocupación una publicación mensual del total del país y trimestral de cada una de las 23 ciudades, y para el índice de pobreza monetaria una estimación anual por departamento (Metodología GEIH V11, p. 15). El DANE no recomienda usar desagregaciones cuyos indicadores tengan coeficientes de variación superiores al 15 % (Metodología GEIH V11, p. 14). El script 08 aplica una regla del autor que parte de ese umbral: un grupo dentro del departamento se publica solo si la cota inferior aproximada de su CV no pasa de 15 % y existe precisión oficial para el dominio que lo contiene. Antes de publicar cualquier corte de otro departamento conviene correr esa misma regla y no publicar cifras mensuales departamentales, que el diseño no produce.
Lo que el laboratorio no resuelve#
La limitación principal ya se anunció al comienzo: la base anual a partir de los módulos de ANDA no se ha construido con datos reales, y el libro depende de bases recibidas ya pegadas cuyo procedimiento original no está disponible. Mientras nadie ejecute 00_construir_base_anual.py --comparar con los módulos descargados, la reproducción empieza en la base anual y no en la descarga.
La segunda tiene que ver con el diseño muestral. Las bases públicas no traen estrato, unidad primaria de muestreo ni segmento, y el DANE estima la varianza por el método del conglomerado último con esos elementos (Metodología GEIH V11, p. 21). El laboratorio no puede reproducir los errores estándar oficiales. Lo que ofrece es una aproximación propia, con la vivienda como conglomerado y el departamento por clase como pseudoestrato. En el Magdalena queda por debajo del CV oficial en todos los casos contrastados, pero en varios indicadores nacionales, como la tasa global de participación, la tasa de ocupación y los ocupados, queda por encima, de modo que allí no se comporta como cota inferior. El capítulo 8 presenta esa calibración.
Hay también decisiones que descansan en inferencias empíricas y no en el diccionario oficial de cada año, que no está en los materiales: que en P3271 el 1 sea hombre y el 2 mujer, y que en P6430 el 7 sea jornalero o peón y el 8 otro. Las dos lecturas reproducen cifras oficiales hasta el cuarto decimal, lo que las hace muy probables, pero siguen siendo inferencias. Del mismo lado están los textos de las metodologías que usa 09 para verificar citas, que el laboratorio no distribuye, y las cifras monetarias, que se dejan en pesos corrientes de cada año: ningún script deflacta.
Por último, el estado del manifiesto. La corrida publicada se hizo con cambios sin guardar en el commit que registra; los hashes permiten comprobar que el código versionado es el mismo, pero lo correcto es volver a correr el laboratorio completo desde un commit limpio para que el manifiesto lo diga por sí solo. La versión del laboratorio con la que se cierra cada edición del libro se anota en las notas de versión.
Ejercicios#
Ejercicio 10.1
Reproducir la informalidad nacional de 2023
Ejecute el laboratorio y obtenga la proporción de ocupados informales del total nacional en 2023. Compárela con la fila correspondiente de la tabla del libro y explique por qué el estado es "coincide" y no "coincide al redondeo", aunque el boletín publique la cifra con un solo decimal.
Ver una pista
Basta con 03_informalidad.py y 07_validacion.py, en ese orden. Busque luego la fila por su id en el CSV que generó.
Ver la respuesta
Con las bases y los anexos en sus carpetas, se corre python run_all.py --solo 03 07 (o la corrida completa) y se busca la fila informalidad_nacional.total-nacional.prop_inf.2023.oficial. El valor propio debe ser 56,3626 % y el referente 56,362624 %, una diferencia de −0,000024 puntos, menor que la tolerancia de 0,0001: estado "coincide".
El referente de esta fila es un valor derivado: el cociente entre los informales y los ocupados de los cuatro trimestres calendario de 2023, leídos en la hoja Grandes dominios del anexo de informalidad vigente (Anexo informalidad jun.-ago. 2026, hoja Grandes dominios, celdas Z16:AK16 sobre Z14:AK14). El boletín de octubre a diciembre de 2025 imprime para ese año 56,4 % (Boletín informalidad oct.-dic. 2025, p. 9), y contra esa cifra la fila correspondiente solo coincide al redondeo. Las dos filas existen en la tabla y conviene citarlas por separado.
Ficha del resultado · Proporción de informalidad, total nacional, 2023Coincide
- Periodo
- 2023
- Ámbito
- Total nacional
- Microdatos
- GEIH marco 2018, bases anuales 2022 a 2025 (12 meses cada una)
- Variables
OCI, FEX_C18, MES, DPTO, AREA, CLASE, P3271, P6430, P6100, P6110, P6450, P6920, P6930, P6940, RAMA2D_R4, P3045S1, P3046, P3069, P6765, P3065, P3066, P3067, P3067S1, P3067S2, P6775, P3068, OFICIO_C8, PER- Filtros
- OCI == 1. Magdalena: DPTO == '47' tras rellenar a 2 caracteres. Santa Marta: AREA == '47'.
- Universo
- Población ocupada
- Factor
- FEX_C18, promedio de los 12 totales mensuales
- Método
- Regla oficial: traducción línea a línea de la hoja Código_STATA del anexo anex-GEIHEISS-oct-dic2025.xlsx (actualizado el 12 de febrero de 2026); EI = 0 informal. Se verificó que la hoja Código_SAS clasifica igual. Proporción = informales / ocupados, con totales anuales como promedio de los totales mensuales. Oficial derivado: promedio de los cuatro trimestres calendario (ene-mar, abr-jun, jul-sep, oct-dic) de la serie de trimestres móviles del anexo. Regla no_cotiza (NO oficial): formal si P6100 == 1 y P6920 == 1, para todos los ocupados.
- Anualización
- Cociente de los promedios anuales de numerador y denominador
- Muestra
- 365.819 filas-mes en el universo del indicador
- Código
geih-lab/scripts/03_informalidad.py· commit 0864525
Contraste con la cifra oficial
| Propio | 56,3626 % |
|---|---|
| Oficial | 56,3626 % (derivado de celdas del anexo) |
| Diferencia | −0,0000 (tolerancia 0,0001) |
| CV aproximado | 0,28 % (cota inferior, sin el diseño completo) |
Diferencia menor que la tolerancia frente a un valor oficial con al menos cuatro decimales y el mismo método.
Ejercicio 10.2
La tasa de desocupación de Santa Marta en 2024
Obtenga la tasa de desocupación anual de Santa Marta en 2024 y compárela con la tabla del libro. Explique por qué la ciudad coincide con el anexo hasta el cuarto decimal mientras el departamento que la contiene no.
Ver una pista
La ciudad se identifica con AREA == "47". Compare luego el estado de esta fila con el de la TD del Magdalena del mismo año y busque en la metodología cómo se calibran las ciudades.
Ver la respuesta
La fila santa_marta_anual.santa-marta.td.2024.razon_de_promedios da 9,6252 % frente a 9,625234 % en la hoja Año_móvil_32_ciudades del anexo de agosto de 2026 (Anexo GEIH ago. 2026, hoja Año_móvil_32_ciudades, celda AL398), con una diferencia de −0,000034 puntos: estado "coincide". La del Magdalena, en cambio, difiere en −0,1570 p.p..
La metodología explica la asimetría: la calibración anual se hace por departamentos, mientras que "las ciudades, áreas metropolitanas y ciudades intermedias se calibran con el promedio anual" (Metodología GEIH V11, p. 20). La lectura del autor es que, para la ciudad, el promedio de los doce meses con el factor mensual equivale a lo que publica el DANE, y por eso la cifra se reproduce; para el departamento interviene un factor anual que la base no trae.
Ficha del resultado · TD de Santa Marta (cabecera), 2024Coincide
- Periodo
- 2024
- Ámbito
- Santa Marta
- Microdatos
- GEIH marco 2018, bases anuales apiladas 2022-2025 (geih_AAAA.csv)
- Variables
DIRECTORIO, MES, DPTO, AREA, CLASE, P3271, P6040, FEX_C18, PT, PET, FT, FFT, OCI, DSI- Filtros
- Ámbito: ninguno (nacional), DPTO == '47' (Magdalena), AREA == '47' (Santa Marta). Sexo: P3271 == 1 (hombres), P3271 == 2 (mujeres).
- Universo
- PT: todas las personas; PET: PET == 1 (15 años y más); FT: FT == 1; OC: OCI == 1; DS: DSI == 1; PFFT: FFT == 1. TGP y TO sobre la PET; TD sobre la FT.
- Factor
- FEX_C18 (factor mensual), promedio de 12 totales mensuales
- Método
- Mensual: suma de FEX_C18 por indicadora en el mes; tasas como cociente de totales. Anual, metodo_anual = promedio_de_totales para poblaciones y razon_de_promedios para tasas (cociente de los promedios anuales); metodo_anual = promedio_de_tasas: promedio simple de las 12 tasas mensuales. n: filas-mes del universo del indicador (denominador para tasas). cv_cota_inf: geihlab.precision.cv_aproximado; para PT no se calcula y para promedio_de_tasas tampoco.
- Anualización
- Cociente de los promedios anuales de numerador y denominador
- Muestra
- 10.510 filas-mes en el universo del indicador
- Código
geih-lab/scripts/02_indicadores_laborales.py· commit 0864525
Contraste con la cifra oficial
| Propio | 9,6252 % |
|---|---|
| Oficial | 9,6252 % (celda del anexo) |
| Diferencia | −0,0000 (tolerancia 0,0001) |
| CV aproximado | 3,14 % (cota inferior, sin el diseño completo) |
Diferencia menor que la tolerancia frente a un valor oficial con al menos cuatro decimales y el mismo método.
Ejercicio 10.3
Ceros de más en OFICIO_C8, con los datos de 2022
El error de relleno de la nota sobre 2025 también está calculado para 2022. Averigüe cuánto cambiaría la proporción de informalidad nacional de ese año si la lectura rellenara OFICIO_C8 a ocho caracteres en lugar de cuatro, qué ocupados cambiarían de clasificación y si la tabla de validación lo detectaría frente al derivado del anexo y frente al boletín.
Ver una pista
No hace falta modificar el código: el script de informalidad ya calcula el error en cada año. Busque el bloque oficio_c8 en el informalidad.json que generó, quédese con las filas de 2022 y lea su campo cambian_por_posicion. Para la detección, recuerde las tolerancias de la tabla de validación.
Ver la respuesta
En 2022, rellenar OFICIO_C8 a ocho caracteres baja la proporción de informalidad nacional de 57,9434 % a 57,3337 %, una diferencia de −0,6097 p.p., más del doble de la de 2025. Cambian de clasificación 2.080 filas de ocupados: 2.015 de trabajadores por cuenta propia (posición 4 de P6430) y 65 de patronos o empleadores (posición 5). Son las únicas posiciones en las que el código oficial, en algunas de sus ramas, decide por el oficio: si los dos primeros dígitos van de 00 a 20 la persona se clasifica como formal y si son 21 o más, como informal (Anexo informalidad oct.-dic. 2025, hoja Código_STATA). Con el relleno a ocho todos los códigos empiezan por "00" y caen en el primer grupo, de modo que ocupados que la regla clasificaba como informales pasan a formales y la proporción baja.
Si ese error entrara en el cálculo principal de 03_informalidad.py, la tabla lo detectaría por los dos lados. Frente al derivado del anexo, 57,943448 %, la diferencia sería de −0,6097 p.p., muy por encima de la tolerancia de 0,0001 puntos. Frente al boletín vigente, que publica 57,9 % para 2022 (Boletín informalidad oct.-dic. 2025, p. 9), sería de −0,57 p.p., también fuera de la tolerancia de 0,05 que admite una cifra con un decimal. Sin una causa documentada, 07_validacion.pymarcaría las dos filas como "diferencia sin explicar" y la prueba de test_validacion.py que exige que no haya ninguna fallaría.
El riesgo está en la lectura humana. Quien compare a ojo con la primera edición del boletín, que publicó 58,2 % para 2022 (Boletín informalidad oct.-dic. 2022, p. 8), verá una brecha de casi un punto y puede atribuirla a la revisión que la serie tuvo después, cuando el problema es suyo. Con errores más pequeños que la mitad del último decimal publicado, el boletín ya no sirve de control, y para esos casos se necesita el referente con cuatro decimales.
Ficha del resultado · Proporción de informalidad, total nacional, 2022Coincide
- Periodo
- 2022
- Ámbito
- Total nacional
- Microdatos
- GEIH marco 2018, bases anuales 2022 a 2025 (12 meses cada una)
- Variables
OCI, FEX_C18, MES, DPTO, AREA, CLASE, P3271, P6430, P6100, P6110, P6450, P6920, P6930, P6940, RAMA2D_R4, P3045S1, P3046, P3069, P6765, P3065, P3066, P3067, P3067S1, P3067S2, P6775, P3068, OFICIO_C8, PER- Filtros
- OCI == 1. Magdalena: DPTO == '47' tras rellenar a 2 caracteres. Santa Marta: AREA == '47'.
- Universo
- Población ocupada
- Factor
- FEX_C18, promedio de los 12 totales mensuales
- Método
- Regla oficial: traducción línea a línea de la hoja Código_STATA del anexo anex-GEIHEISS-oct-dic2025.xlsx (actualizado el 12 de febrero de 2026); EI = 0 informal. Se verificó que la hoja Código_SAS clasifica igual. Proporción = informales / ocupados, con totales anuales como promedio de los totales mensuales. Oficial derivado: promedio de los cuatro trimestres calendario (ene-mar, abr-jun, jul-sep, oct-dic) de la serie de trimestres móviles del anexo. Regla no_cotiza (NO oficial): formal si P6100 == 1 y P6920 == 1, para todos los ocupados.
- Anualización
- Cociente de los promedios anuales de numerador y denominador
- Muestra
- 382.461 filas-mes en el universo del indicador
- Código
geih-lab/scripts/03_informalidad.py· commit 0864525
Contraste con la cifra oficial
| Propio | 57,9434 % |
|---|---|
| Oficial | 57,9434 % (derivado de celdas del anexo) |
| Diferencia | −0,0000 (tolerancia 0,0001) |
| CV aproximado | 0,27 % (cota inferior, sin el diseño completo) |
Diferencia menor que la tolerancia frente a un valor oficial con al menos cuatro decimales y el mismo método.