Cómo se construyó y entendió el panel: pipeline, análisis exploratorio, ingeniería de características, prácticas MLOps, estructura del repositorio y almacén en esquema estrella.
Construcción de la base de datos
El primer entregable ya está construido: el panel multiserie del U.S. Visa Bulletin —extraído, anotado, consolidado y auditado de extremo a extremo— desde el repositorio público VisaPredictAI. Esta sección documenta con detalle cómo se levantó y qué se halló en el camino.
01 El panel multiserie
Cada celda del panel es la fecha de prioridad de una combinación (país , categoría , tabla ) en el mes , transformada a días desde una fecha base ( 1 de enero de 1975, anterior a la prioridad más antigua observada: noviembre de 1979 en Filipinas F4). Cada observación conserva además su estado administrativo —Current (C), Final (F, fecha específica) o Unavailable (U)— como anotación descriptiva. El objetivo predictivo se entrena exclusivamente sobre las observaciones con estado F.
desde dic-2001
desde oct-2015
desde dic-2001
desde oct-2015
02 Arquitectura del pipeline de datos
De HTML crudo a panel auditado en seis etapas reproducibles, ejecutadas por una GitHub Action al publicarse un nuevo boletín.
03 Las cinco brechas resueltas
La base heredada cubría sólo cuatro categorías de empleo, una tabla y perdía la información de estado. Cada brecha se cerró con una corrección verificada (auditoría antes → después).
Anotación C/F/U preservada
El código aplanaba Current→fecha del boletín y Unavailable→vacío, borrando el régimen. Ahora una columna status lo conserva y el modelo entrena sólo sobre F.
Dates for Filing de empleo
El scraper cortaba tras la primera tabla (sólo FAD). Ahora captura las dos tablas employment-based etiquetadas por tipo; DFF disponible desde oct-2015.
EB-5 y subcategorías
El filtro descartaba todo salvo EB-1 a EB-4. Un clasificador normaliza 20 años de deriva de etiquetas a 16 códigos canónicos (EB-5, Other Workers, set-asides…).
Extensión al piso de la fuente
Detección robusta de columnas (categoría = col 0; país por nombre normalizado) recuperó 2001–2003 y reparó la serie residual (All Charg. estaba truncada a 2016).
Vacío desambiguado
El antiguo «vacío» mezclaba Unavailable con celdas no parseadas. El estado distingue ahora U de UNK (centinela elegido para no colisionar con la coerción a NaN de pandas): en el panel actual, una sola celda UNK.
Sin pérdidas silenciosas
La auditoría detectó un mes (dic-2007) perdido por un fallo HTTP transitorio. Se añadió retry con backoff y reporte explícito: ningún mes se descarta en silencio.
04 Taxonomía de categorías y la odisea del EB-5
Normalizar las categorías exigió absorber 68 variantes de etiqueta distintas (saltos de línea, espacios duros, errores tipográficos) acumuladas en dos décadas. El caso extremo es la quinta preferencia de empleo (EB-5), cuya estructura cambió de régimen tres veces: el sistema captura cada época bajo un código canónico estable.
05 Búsqueda profunda de boletines faltantes
Se enumeró cada mes esperado y se sondeó tanto la URL construida como el Wayback Machine. La cobertura a nivel boletín es de 296 de 296 meses (100 %). Los cinco boletines retirados de travel.state.gov (marzo, septiembre, octubre y noviembre de 2009; octubre de 2012) se recuperaron manualmente del archivo histórico, se congelaron junto al resto de instantáneas y forman parte del panel.
Las Final Action Dates se publican desde 1992 [14], pero la serie mensual estructuralmente homogénea que el módulo consolida desde el HTML del boletín abarca de forma continua diciembre de 2001 – 2026 (296 meses completos por serie). El periodo 1992–2001 existe en fuentes estadísticas y de archivo, reservado a integración posterior. Las Dates for Filing sólo existen desde octubre de 2015 (130 observaciones).
06 Mega-auditoría de calidad (12 dimensiones)
El panel se somete a una auditoría exhaustiva: esquema, completitud, inventario de series, distribución de estado, huecos, unicidad de clave, validez de fechas, coherencia FAD vs DFF, anomalías de salto, reconciliación fuente↔panel, matriz de cobertura y entrenabilidad. Veredicto: APTO — cero hallazgos críticos, cero advertencias.
Distribución de estado administrativo
Dos hallazgos informativos (fenómenos reales, no errores)
6 inversiones DFF < FAD
El Departamento de Estado publica ocasionalmente fechas de presentación pocos días por detrás de la acción final. Los valores crudos parsean correctamente: son datos reales, no fallos de extracción.
14 saltos fuertes mes a mes
Retrogresiones y avances reales —la transición EB-4 de diciembre de 2022 y los rezagos de México F1/F3 de los años 80—. El sistema predictivo deberá tolerar las retrogresiones como fenómeno legítimo.
Prácticas MLOps
La base de datos no es un script suelto: es un sistema de ingeniería reproducible, probado y auto-protegido que se ejecuta de forma automatizada. Esta sección documenta cada práctica MLOps en producción en el repositorio VisaPredictAI.
01 Reproducibilidad determinista
El entorno de desarrollo y el de integración continua son idénticos: mismas versiones de Python y de cada dependencia, fijadas con pin exacto. Cualquier persona reproduce el mismo dato byte por byte.
02 Pruebas automatizadas en cuatro niveles
La suite de pruebas del repositorio corre bajo pytest con un piso de cobertura ejecutable (`fail_under=65`): la cobertura no puede erosionarse sin romper el CI (el conteo exacto vive en el CI, no en esta prosa).
Funciones puras
Clasificadores de estado (C/F/U/UNK) y de categoría (20 años de deriva de etiquetas, desambiguación de subcadenas). Sin I/O, deterministas.
Extracción offline
El parseo real corre sobre 4 fixtures HTML guardados (eras de formato 2002/2007/2020/2022) sin red: rápido, determinista, cubre la lógica donde viven los bugs.
Contrato del panel
Invariantes duras del producto: clave única, 0 días negativos, dominio de estado, fecha de prioridad sólo en `F`, completitud de meses (un mes caído por la red falla el gate y dispara la alerta). Cualquier regresión bloquea el commit.
Contrato del almacén
El esquema estrella DuckDB se carga bajo constraints PK/FK/CHECK vivos: verifican que el panel se reconstruye sin pérdida (v_panel_long), que cada hecho respeta el contrato y que las vistas y marts cuadran.
03 Integración continua: dos pipelines
Un pipeline valida el código en cada cambio; otro refresca el dato cuando llega un boletín nuevo, con un gate de calidad antes de publicar.
04 Robustez ante el mundo real
La fuente es HTML vivo de 20 años con formatos cambiantes y fallos transitorios. El pipeline está blindado contra cada modo de fallo observado.
Retry + backoff
Un fallo HTTP transitorio (5xx, timeout) reintenta con espera creciente; un 4xx permanente ya no se reintenta. Ya no se pierde un mes en silencio (antes un `except: pass` los tragaba).
Orden determinista
Las filas se ordenan por clave completa, no por un sort inestable: un mes caído ya no reordena en cascada el resto del archivo.
Abort en fallo masivo
Si fallan >10 boletines (rediseño/caída de la fuente), el scraper aborta sin escribir: nunca se publica un panel degradado.
Alertas en fallo
Un gate rojo o un crash abren (o comentan) un issue de seguimiento: el fallo avisa, no muere callado en la pestaña de Actions.
Centinela `UNK`
El estado «sin dato» usa `UNK`, no `NA`: el string `"NA"` lo coerciona pandas a NaN al leer y borraba la anotación. `UNK` es seguro para cualquier consumidor.
Hallado por los tests
El bug del centinela y un mes perdido por la red los cazaron las pruebas y el reporte de fallos, no un usuario: la red de seguridad funcionando.
05 Contrato de datos, linaje y versionado
El dato se valida solo
- Invariantes duras del panel como pruebas ejecutables (el esquema es el contrato).
- Linaje a nivel celda: `raw_value` conserva la celda original tal cual se publicó.
- Auditoría programática de 12 dimensiones con veredicto APTO (0 hallazgos críticos).
- Hooks de pre-commit: bloqueo de archivos grandes, fin de línea, YAML/TOML, ruff.
Cada artefacto donde corresponde
- Los CSV abiertos viven en git: descargables directo, sin DVC ni credenciales (son el entregable).
- Las figuras no se versionan (artefactos regenerables) para no inflar el historial.
- DVC inicializado y reservado para los artefactos de modelo del próximo semestre (checkpoints, pronósticos).
- `Makefile`: `make install · scrape · panel · test · check · all` (operación de un comando).
Estructura adaptada de Cookiecutter Data Science
El repositorio no organiza sus archivos al azar: sigue un contrato estructural propio adaptado de Cookiecutter Data Science (CCDS v2, una plantilla que fomenta justamente la adaptación), con desviaciones deliberadas y documentadas: dos paquetes con dirección de capas en lugar de un src/ único, cero notebooks (todo es script reproducible) y binarios de modelos por DVC, no en git. Quien conozca CCDS encuentra cada archivo donde espera —datos crudos separados de los derivados, reportes y documentación en su lugar, y un paquete instalable con una sola fuente de verdad para las dependencias— en el repositorio VisaPredictAI.
01 El árbol, de un vistazo
La convención CCDS separa por rol, no por tipo de archivo: el dato crudo (fuente inmutable) nunca se mezcla con el derivado (regenerable), y el código vive aparte de los reportes y la documentación.
VisaPredictAI/ ├── data/ │ ├── raw/ # 10 CSV por país, recién scrapeados (fuente) │ └── processed/ # visa_panel_long.csv — el panel derivado ├── reports/ # artefactos por rol: campaign · eval · prospective · governance ├── docs/ # documentación (DVC.md…) ├── tests/ # pytest + fixtures HTML offline ├── vp_data/ # paquete núcleo: config · visa_common · tracking ├── pipeline/ # DAG: freeze → scrape_* (empleo · familiar · DV) → build_panel → build_database → mega_audit ├── pyproject.toml # deps + tooling + paquete (fuente única) ├── Makefile # operación de un comando ├── LICENSE # MIT └── .github/workflows/ # CI de código + cron de datos
02 Qué hace cada pieza
Fuente inmutable
Los 10 CSV por país tal como salen de los scrapers. Nunca se editan a mano: si algo cambia, se corrige el scraper, no el dato.
Derivado regenerable
El panel multiserie que produce pipeline/build_panel.py. Separarlo del crudo deja claro qué es fuente y qué es resultado.
Reportes y docs
Las auditorías de calidad (12 dimensiones) se escriben en reports/; la documentación operativa (p. ej. la estrategia DVC) vive en docs/.
Contrato ejecutable
Suite pytest con fixtures HTML para extracción offline e invariantes duras del panel: el esquema es el contrato.
Núcleo compartido
El paquete vp_data/ centraliza rutas, constantes y helpers de scraping (fuente única); pipeline/ encadena los pasos del DAG de datos, cada uno invocable como python -m pipeline.<módulo>.
Una sola fuente de verdad
Dependencias (runtime y dev), configuración de ruff/mypy/pytest y el empaquetado, todo en un archivo: el paquete es instalable con pip install -e ..
03 Cómo se usa en el día a día
La estructura habilita un flujo de un solo comando y reproducible: el dato fluye de raw/ a processed/, y todo el toolchain se instala desde una sola fuente.
git clone https://github.com/UACJ-MIAAD/VisaPredictAI.git cd VisaPredictAI make install # = pip install -e ".[dev]" (runtime + herramientas) make scrape # escribe data/raw/*.csv make panel # consolida data/processed/visa_panel_long.csv make check # ruff + mypy + pytest (gate de cobertura) make audit # auditorías → reports/
04 Adaptaciones honestas
CCDS es un punto de partida, no un dogma. Se adopta lo que sirve a un proyecto de adquisición de datos y se omite, a conciencia, lo que aún no aplica —sin estructura de relleno.
Almacén de datos en esquema estrella
El panel plano se eleva a un almacén dimensional normalizado en DuckDB: hechos centrales por grano, dimensiones conformes alrededor, y las invariantes del contrato promovidas a constraints declarativas (PK/FK/CHECK) que rechazan en la carga cualquier fila inválida. Cubre todas las categorías del Visa Bulletin —familia, empleo y Diversity Visa— con linaje, gobernanza y marts listos para el modelado. La definición vive en `schema.sql`; el modelo se reconstruye con `make db`.
Esquema estrella · hechos en dorado, dimensiones en azul, dimensiones conformes resaltadas, marts gold en verde, gobernanza en gris.
00 Por qué DuckDB: criterios de selección
La elección del motor responde al perfil de uso, no a la moda. El acceso a la base es analítico y de solo-lectura (escaneos, agregaciones y joins en estrella sobre ~27 000 filas), se regenera de un CSV versionado y debe correr en cualquier máquina al clonar: sin servidor, sin nube y sin costo. Sobre ese perfil se fijaron seis criterios.
Cero servidor
Corre dentro del proceso (Python/CLI), sin demonio que instalar ni puerto que abrir. Clonar y make db basta: reproducibilidad total dev↔CI.
Columnar y vectorizado
Motor OLAP orientado a columnas: los escaneos y agregaciones del panel son su caso de uso natural, no el transaccional fila a fila.
Sin nube, sin cuenta
Decisión NO-AWS explícita: nada de credenciales, facturación ni red. El entregable académico se reproduce sin barreras.
PK/FK/CHECK nativas
SQL estándar con constraints declarativas que rechazan en la carga cualquier fila inválida: el esquema es el contrato de datos.
Parquet y pandas
Lee y escribe Parquet y DataFrames de forma nativa; el puente al modelado en Python es directo, sin un ETL intermedio.
Regenerable
La base es un solo archivo .duckdb derivado (gitignored); si se borra o corrompe, make db lo reconstruye en segundos.
Estado del arte: marco comparativo
En el panorama 2024–2025, DuckDB es el referente de base de datos analítica embebida (a menudo descrito como «el SQLite del OLAP»). Frente a las alternativas consideradas para este componente:
| Opción | Modelo | Servidor | Veredicto | Razón |
|---|---|---|---|---|
| DuckDB | OLAP columnar, embebido | No | Elegido | Analítico, sin servidor, local y gratis, SQL con constraints, Parquet nativo |
| SQLite | OLTP por filas, embebido | No | Parcial | Embebido y gratis, pero orientado a filas: lento en escaneos y agregaciones; sin columnar ni Parquet nativo |
| PostgreSQL | OLTP por filas, cliente–servidor | Sí | No | Excelente para apps concurrentes, pero exige un servidor corriendo — rompe el «clonar y correr» |
| Parquet + pandas | Archivo + memoria | No | Parcial | Rápido y columnar, pero sin motor SQL ni integridad relacional (sin PK/FK/CHECK = sin contrato) |
| BigQuery / Snowflake / Redshift | OLAP en la nube | Gestionado | No | Potentes a gran escala, pero de pago, con cuenta y red — incompatibles con la reproducibilidad NO-AWS |
Para un panel analítico, pequeño y de solo-lectura que debe reproducirse gratis y sin servidor, DuckDB combina lo embebido de SQLite con el rendimiento columnar de un almacén de datos y las garantías de integridad de SQL. El Parquet tipado que exporta deja abierta la puerta a escalar a la nube en el futuro sin reescribir el modelo.
01 Catálogo de tablas, campos y llaves
El almacén tiene 12 tablas — 7 dimensiones, 2 hechos y 3 de gobernanza y procedencia — más 6 vistas/marts. Cada columna lleva su llave: PK primaria · FK foránea · UK única. Tipos entre paréntesis.
Dimensiones · 7
dim_area
area_idPK (int)slugUK ·name(str)is_residual_group(bool)
dim_category
category_idPKblock·codeUK(block,code)parent_code·preference_levelis_subcategory·ina_basis
dim_date
date_idPKbulletin_dateUK (date)year·month·quarterus_fiscal_year(int)
dim_status
statusPK (C/F/U/UNK)label·descriptionis_predictable(bool · F)
dim_table
table_idPKcodeUK (FAD/DFF)name(str)
dim_region
region_idPKslugUK ·name- 6 regiones DV
dim_category_alias
alias_idPKcategory_idFK→dim_categoryraw_labelUK(cat,label)valid_from·valid_to·n_months
Hechos · 2
fact_priority
- PK compuesta:
area_id·category_id·table_id·date_id(las 4 FK) statusFK→dim_statuspriority_date(date · sólo F)days_since_base= (int · sólo F) ·raw_value- CHECK: dominio status · días ≥ 0 · definido sólo si F
fact_dv_rank
- PK compuesta:
region_id·date_id(FK) statusFK→dim_statusrank_cutoff(int · sólo F)raw_value·exceptions(por país)- CHECK: rango ≥ 0 · definido sólo si F
Gobernanza · 2
etl_run
run_idPK ·built_at_utc(ts)schema_version·n_fact_priority·n_fact_dvn_trainable_f·pct_trainable(calidad)panel_floor·panel_ceiling(date)
schema_version
version(int)description(str)- detecta drift / evolución del esquema
Vistas y marts · 6: v_panel_long y v_dv_long reconstruyen el panel/DV sin pérdida; v_category_alias (linaje), v_trainable_by_preference (roll-up por preferencia), y los marts de modelado mart_training_F y mart_series_summary.
02 Todas las categorías, incluida la Diversity Visa
Familia (F1–F4) y empleo (EB-1 a EB-5 con 16 subcódigos canónicos) viven en el hecho de fechas fact_priority (). La Diversity Visa publica un número de rango por región, no una fecha — así que no cabe en el panel y obtiene su propio hecho fact_dv_rank (6 regiones × mes) y su dimensión dim_region.
03 Jerarquía, dimensiones conformes y linaje
Roll-ups por preferencia
dim_category lleva parent_code, preference_level y base estatutaria INA: las subcategorías se pliegan a su preferencia (todas las EB-5 bajo EB-5) vía la vista v_trainable_by_preference.
Dimensiones compartidas
dim_date y dim_status son conformes: ambos hechos las comparten, así que el tiempo y el régimen C/F/U/UNK se definen una sola vez y se cruzan igual en todo el almacén.
20 años de deriva, auditables
dim_category_alias saca la normalización de etiquetas del código a datos: cada grafía publicada → categoría canónica con su ventana de meses (48 alias; EB5_TEA tuvo 7 grafías 2001–2015).
04 Gobernanza, calidad y marts de modelado
El esquema rechaza lo inválido
PK compuesta, FK a cada dimensión y CHECK (dominio de estado, días ≥ 0, variable definida sólo si status='F'): el contrato vive en el esquema, no sólo en los tests.
Auditoría de carga + calidad
etl_run registra cada build (versión de esquema, conteos, score de entrenabilidad, piso/techo temporal); schema_version marca la evolución. Arquitectura medallón raw→panel→gold.
Listo para el modelado
mart_training_F = conjunto entrenable limpio (sólo F con la variable dependiente y features de tiempo); mart_series_summary resume cada serie para filtrar las evaluables.
05 Límites declarados (auditoría adversarial)
Una auditoría independiente re-derivó el modelo desde cero. Pasó fuerte —0 claves foráneas huérfanas, 48/48 alias re-clasifican a su canónico, las constraints rechazan toda fila inválida, contenido reproducible— y dejó estos límites sin barnizar:
2002–2003 parcial
El piso DV llega a 2001-12, pero ~20 meses de 2002–2003 publican la Diversity Visa en HTML no-tabular, aún no parseado (trabajo futuro). La advance notification (mes futuro) también queda fuera de alcance.
Gate de cobertura DV
Esos huecos ya no son silenciosos: test_dv_coverage_floor (en el gate de datos) exige pisos de filas, meses y meses completos, y aborta el commit si un scrape degrada la cobertura.