How the panel was built and understood: pipeline, exploratory analysis, feature engineering, MLOps practices, repository structure and a star-schema warehouse.
Building the database
The first deliverable is already built: the multiseries panel of the U.S. Visa Bulletin —extracted, annotated, consolidated, and audited end to end— from the public repository VisaPredictAI. This section documents in detail how it was built and what was found along the way.
01 The multiseries panel
Each panel cell is the priority date of a combination (country , category , table ) in month , transformed into days since a base date ( January 1, 1975, earlier than the oldest observed priority date: November 1979 in Philippines F4). Each observation also retains its administrative status —Current (C), Final (F, specific date), or Unavailable (U)— as a descriptive annotation. The predictive target is trained exclusively on observations with status F.
since Dec-2001
since Oct-2015
since Dec-2001
since Oct-2015
02 Data pipeline architecture
From raw HTML to an audited panel in six reproducible stages, run by a GitHub Action whenever a new bulletin is published.
03 The five gaps resolved
The inherited base covered only four employment categories, one table, and lost the status information. Each gap was closed with a verified fix (audit before → after).
C/F/U annotation preserved
The code flattened Current→bulletin date and Unavailable→empty, erasing the regime. Now a status column preserves it and the model trains only on F.
Employment Dates for Filing
The scraper stopped after the first table (FAD only). Now it captures both employment-based tables labeled by type; DFF available since Oct-2015.
EB-5 and subcategories
The filter discarded everything except EB-1 through EB-4. A classifier normalizes 20 years of label drift into 16 canonical codes (EB-5, Other Workers, set-asides…).
Extension to the source floor
Robust column detection (category = col 0; country by normalized name) recovered 2001–2003 and repaired the residual series (All Charg. was truncated to 2016).
Empty value disambiguated
The old «empty» mixed Unavailable with unparsed cells. The status now distinguishes U from UNK (a sentinel chosen so it does not collide with pandas's coercion to NaN): in the current panel, a single UNK cell.
No silent losses
The audit detected one month (Dec-2007) lost to a transient HTTP failure. A retry with backoff and explicit reporting was added: no month is dropped silently.
04 Category taxonomy and the EB-5 odyssey
Normalizing the categories required absorbing 68 distinct label variants (line breaks, hard spaces, typos) accumulated over two decades. The extreme case is the fifth employment preference (EB-5), whose structure changed regime three times: the system captures each era under a stable canonical code.
05 Deep search for missing bulletins
Every expected month was enumerated and probed against both the constructed URL and the Wayback Machine. Bulletin-level coverage is 296 of 296 months (100 %). The five bulletins withdrawn from travel.state.gov (March, September, October, and November 2009; October 2012) were recovered by hand from the historical archive, frozen alongside the rest of the snapshots, and are part of the panel.
The Final Action Dates have been published since 1992 [14], but the structurally homogeneous monthly series that the module consolidates from the bulletin HTML spans continuously from December 2001 – 2026 (296 complete months per series). The 1992–2001 period exists in statistical and archival sources, reserved for later integration. The Dates for Filing exist only since October 2015 (130 observations).
06 Quality mega-audit (12 dimensions)
The panel undergoes an exhaustive audit: schema, completeness, series inventory, status distribution, gaps, key uniqueness, date validity, FAD vs DFF coherence, jump anomalies, source↔panel reconciliation, coverage matrix, and trainability. Verdict: PASS — zero critical findings, zero warnings.
Administrative status distribution
Two informative findings (real phenomena, not errors)
6 inversions DFF < FAD
The Department of State occasionally publishes filing dates a few days behind the final action. The raw values parse correctly: they are real data, not extraction failures.
14 strong month-to-month jumps
Real retrogressions and advances —the December 2022 EB-4 transition and the Mexico F1/F3 backlogs of the 1980s—. The predictive system must tolerate retrogressions as a legitimate phenomenon.
MLOps practices
The database is not a loose script: it is a reproducible, tested, and self-protecting engineering system that runs in an automated way. This section documents each MLOps practice in production in the VisaPredictAI repository.
01 Deterministic reproducibility
The development environment and the continuous integration environment are identical: same versions of Python and of each dependency, fixed with exact pins. Anyone reproduces the same data byte by byte.
02 Automated tests at four levels
The repository's test suite runs under pytest with an executable coverage floor (`fail_under=65`): coverage cannot erode without breaking CI (the exact count lives in CI, not in this prose).
Pure functions
State classifiers (C/F/U/UNK) and category classifiers (20 years of label drift, substring disambiguation). No I/O, deterministic.
Offline extraction
The actual parsing runs over 4 saved HTML fixtures (format eras 2002/2007/2020/2022) with no network: fast, deterministic, covers the logic where the bugs live.
Panel contract
Hard product invariants: unique key, 0 negative days, state domain, priority date only on `F`, month completeness (a month dropped by the network fails the gate and triggers the alert). Any regression blocks the commit.
Warehouse contract
The DuckDB star schema is loaded under live PK/FK/CHECK constraints: they verify that the panel is reconstructed without loss (v_panel_long), that each fact respects the contract, and that the views and marts reconcile.
03 Continuous integration: two pipelines
One pipeline validates the code on every change; the other refreshes the data when a new bulletin arrives, with a quality gate before publishing.
04 Robustness against the real world
The source is 20 years of live HTML with changing formats and transient failures. The pipeline is hardened against every observed failure mode.
Retry + backoff
A transient HTTP failure (5xx, timeout) retries with increasing wait; a permanent 4xx is no longer retried. A month is no longer lost silently (previously an `except: pass` swallowed them).
Deterministic ordering
Rows are sorted by their full key, not by an unstable sort: a dropped month no longer cascades a reorder across the rest of the file.
Abort on mass failure
If >10 bulletins fail (source redesign/outage), the scraper aborts without writing: a degraded panel is never published.
Alerts on failure
A red gate or a crash opens (or comments on) a tracking issue: the failure speaks up, it does not die silently in the Actions tab.
`UNK` sentinel
The «no data» state uses `UNK`, not `NA`: the string `"NA"` is coerced by pandas to NaN on read and erased the annotation. `UNK` is safe for any consumer.
Caught by the tests
The sentinel bug and a month lost to the network were caught by the tests and the failure report, not by a user: the safety net working.
05 Data contract, lineage, and versioning
The data validates itself
- Hard panel invariants as executable tests (the schema is the contract).
- Cell-level lineage: `raw_value` preserves the original cell exactly as published.
- Programmatic audit of 12 dimensions with a PASS verdict (0 critical findings).
- Pre-commit hooks: large-file blocking, end-of-line, YAML/TOML, ruff.
Each artifact where it belongs
- The open CSVs live in git: directly downloadable, with no DVC or credentials (they are the deliverable).
- The figures are not versioned (regenerable artifacts) so as not to bloat the history.
- DVC initialized and reserved for next semester's model artifacts (checkpoints, forecasts).
- `Makefile`: `make install · scrape · panel · test · check · all` (single-command operation).
Structure adapted from Cookiecutter Data Science
The repository does not organize its files at random: it follows its own structural contract adapted from Cookiecutter Data Science (CCDS v2, a template that itself encourages adaptation), with deliberate, documented deviations: two packages with layered import direction instead of a single src/, zero notebooks (everything is a reproducible script), and model binaries via DVC rather than git. Anyone who knows CCDS finds each file where they expect it —raw data kept apart from derived data, reports and documentation in their place, and an installable package with a single source of truth for the dependencies— in the VisaPredictAI repository.
01 The tree, at a glance
The CCDS convention separates by role, not by file type: the raw data (immutable source) is never mixed with the derived data (regenerable), and the code lives apart from the reports and documentation.
VisaPredictAI/ ├── data/ │ ├── raw/ # 10 CSVs per country, freshly scraped (source) │ └── processed/ # visa_panel_long.csv — the derived panel ├── reports/ # role-structured artifacts: campaign · eval · prospective · governance ├── docs/ # documentation (DVC.md…) ├── tests/ # pytest + offline HTML fixtures ├── vp_data/ # core package: config · visa_common · tracking ├── pipeline/ # data DAG: freeze → scrape_* (employment · family · DV) → build_panel → build_database → mega_audit ├── pyproject.toml # deps + tooling + package (single source) ├── Makefile # single-command operation ├── LICENSE # MIT └── .github/workflows/ # code CI + data cron
02 What each piece does
Immutable source
The 10 CSVs per country exactly as they come out of the scrapers. Never edited by hand: if something changes, the scraper is fixed, not the data.
Regenerable derived data
The multiseries panel produced by pipeline/build_panel.py. Keeping it apart from the raw data makes clear what is source and what is result.
Reports and docs
The quality audits (12 dimensions) are written to reports/; the operational documentation (e.g. the DVC strategy) lives in docs/.
Executable contract
pytest suite with HTML fixtures for offline extraction and hard panel invariants: the schema is the contract.
Shared core
The vp_data/ package centralizes paths, constants and scraping helpers (single source); pipeline/ chains the data-DAG steps, each invocable as python -m pipeline.<module>.
A single source of truth
Dependencies (runtime and dev), ruff/mypy/pytest configuration, and packaging, all in one file: the package is installable with pip install -e ..
03 How it is used day to day
The structure enables a single-command, reproducible flow: the data flows from raw/ to processed/, and the whole toolchain installs from a single source.
git clone https://github.com/UACJ-MIAAD/VisaPredictAI.git cd VisaPredictAI make install # = pip install -e ".[dev]" (runtime + tools) make scrape # writes data/raw/*.csv make panel # consolidates data/processed/visa_panel_long.csv make check # ruff + mypy + pytest (coverage gate) make audit # audits → reports/
04 Honest adaptations
CCDS is a starting point, not a dogma. What serves a data acquisition project is adopted, and what does not yet apply is deliberately left out —no filler structure.
Star-schema data warehouse
The flat panel is promoted to a normalized dimensional warehouse in DuckDB: central facts by grain, conformed dimensions around them, and the contract invariants raised to declarative constraints (PK/FK/CHECK) that reject any invalid row at load time. It covers every category of the Visa Bulletin —family, employment and Diversity Visa— with lineage, governance and marts ready for modeling. The definition lives in `schema.sql`; the model is rebuilt with `make db`.
Star schema · facts in gold, dimensions in blue, conformed dimensions highlighted, gold marts in green, governance in gray.
00 Why DuckDB: selection criteria
The engine choice follows the usage profile, not the trend. Access to the database is analytical and read-only (scans, aggregations and star joins over ~27 000 rows), it is regenerated from a versioned CSV, and it must run on any machine right after cloning: no server, no cloud and no cost. Against that profile, six criteria were set.
Zero server
It runs inside the process (Python/CLI), with no daemon to install nor port to open. Cloning and make db is enough: full dev↔CI reproducibility.
Columnar and vectorized
A column-oriented OLAP engine: the panel's scans and aggregations are its natural use case, not the row by row transactional one.
No cloud, no account
An explicit NO-AWS decision: no credentials, billing or network. The academic deliverable reproduces without barriers.
Native PK/FK/CHECK
Standard SQL with declarative constraints that reject any invalid row at load time: the schema is the data contract.
Parquet and pandas
It reads and writes Parquet and DataFrames natively; the bridge to modeling in Python is direct, with no intermediate ETL.
Regenerable
The database is a single derived .duckdb file (gitignored); if it is deleted or corrupted, make db rebuilds it in seconds.
State of the art: comparison framework
In the 2024–2025 landscape, DuckDB is the reference for an embedded analytical database (often described as «the SQLite of OLAP»). Compared with the alternatives considered for this component:
| Option | Model | Server | Verdict | Reason |
|---|---|---|---|---|
| DuckDB | Columnar OLAP, embedded | No | Chosen | Analytical, serverless, local and free, SQL with constraints, native Parquet |
| SQLite | Row-based OLTP, embedded | No | Partial | Embedded and free, but row-oriented: slow on scans and aggregations; no columnar nor native Parquet |
| PostgreSQL | Row-based OLTP, client–server | Yes | No | Excellent for concurrent apps, but it requires a running server — it breaks the «clone and run» |
| Parquet + pandas | File + memory | No | Partial | Fast and columnar, but with no SQL engine nor relational integrity (no PK/FK/CHECK = no contract) |
| BigQuery / Snowflake / Redshift | Cloud OLAP | Managed | No | Powerful at large scale, but paid, with account and network — incompatible with NO-AWS reproducibility |
For an analytical, small and read-only panel that must reproduce for free and without a server, DuckDB combines the embedded nature of SQLite with the columnar performance of a data warehouse and the integrity guarantees of SQL. The typed Parquet it exports leaves the door open to scaling to the cloud in the future without rewriting the model.
01 Catalog of tables, fields and keys
The warehouse has 12 tables — 7 dimensions, 2 facts and 3 governance and provenance — plus 6 views/marts. Each column carries its key: PK primary · FK foreign · UK unique. Types in parentheses.
Dimensions · 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 DV regions
dim_category_alias
alias_idPKcategory_idFK→dim_categoryraw_labelUK(cat,label)valid_from·valid_to·n_months
Facts · 2
fact_priority
- PK composite:
area_id·category_id·table_id·date_id(all 4 FK) statusFK→dim_statuspriority_date(date · F only)days_since_base= (int · F only) ·raw_value- CHECK: status domain · days ≥ 0 · defined only if F
fact_dv_rank
- PK composite:
region_id·date_id(FK) statusFK→dim_statusrank_cutoff(int · F only)raw_value·exceptions(by country)- CHECK: rank ≥ 0 · defined only if F
Governance · 2
etl_run
run_idPK ·built_at_utc(ts)schema_version·n_fact_priority·n_fact_dvn_trainable_f·pct_trainable(quality)panel_floor·panel_ceiling(date)
schema_version
version(int)description(str)- detects schema drift / evolution
Views and marts · 6: v_panel_long and v_dv_long reconstruct the panel/DV losslessly; v_category_alias (lineage), v_trainable_by_preference (roll-up by preference), and the modeling marts mart_training_F and mart_series_summary.
02 Every category, including the Diversity Visa
Family (F1–F4) and employment (EB-1 through EB-5 with 16 canonical subcodes) live in the dates fact fact_priority (). The Diversity Visa publishes a rank number by region, not a date — so it does not fit in the panel and gets its own fact fact_dv_rank (6 regions × month) and its dimension dim_region.
03 Hierarchy, conformed dimensions and lineage
Roll-ups by preference
dim_category carries parent_code, preference_level and INA statutory basis: subcategories fold into their preference (all EB-5 under EB-5) via the view v_trainable_by_preference.
Shared dimensions
dim_date and dim_status are conformed: both facts share them, so time and the C/F/U/UNK regime are defined once and join the same way across the whole warehouse.
20 years of drift, auditable
dim_category_alias moves label normalization out of code and into data: each published spelling → canonical category with its month window (48 aliases; EB5_TEA had 7 spellings 2001–2015).
04 Governance, quality and modeling marts
The schema rejects the invalid
Composite PK, FK to each dimension and CHECK (status domain, days ≥ 0, variable defined only if status='F'): the contract lives in the schema, not only in the tests.
Load audit + quality
etl_run records each build (schema version, counts, trainability score, temporal floor/ceiling); schema_version marks the evolution. Medallion architecture raw→panel→gold.
Ready for modeling
mart_training_F = clean trainable set (only F with the dependent variable and time features); mart_series_summary summarizes each series to filter the evaluable ones.
05 Declared limits (adversarial audit)
An independent audit re-derived the model from scratch. It passed strongly —0 orphan foreign keys, 48/48 aliases re-classify to their canonical form, the constraints reject every invalid row, reproducible content— and left these limits unvarnished:
2002–2003 partial
The DV floor reaches 2001-12, but ~20 months of 2002–2003 publish the Diversity Visa in non-tabular HTML, not yet parsed (future work). The advance notification (future month) is also out of scope.
DV coverage gate
Those gaps are no longer silent: test_dv_coverage_floor (in the data gate) requires floors of rows, months and complete months, and aborts the commit if a scrape degrades coverage.