Skip to content

Modernize Lamas pipeline: retire Airtable and Jupyter notebook#1

Merged
akariv merged 8 commits into
masterfrom
modernize-lamas-pipeline
Jul 4, 2026
Merged

Modernize Lamas pipeline: retire Airtable and Jupyter notebook#1
akariv merged 8 commits into
masterfrom
modernize-lamas-pipeline

Conversation

@akariv

@akariv akariv commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the Airtable-dependent, notebook-driven Lamas (CBS municipal statistics) pipeline with:

  • A proper installable package (Lamas/lamas/) with a lamas CLI covering the full workflow: download, diagnose, config show/set-sheet, preprocess, map-headers, mapping add/confirm-fuzzy/reject-fuzzy, stats, build, full-run, qa.
  • Local YAML files replacing Airtable: Lamas/data/sheet_config.yaml (per-year/per-sheet Excel layout config, replacing config.py) and Lamas/data/header_mapping.yaml (canonical header → raw variants, replacing the Airtable "Header Mapping" table — seeded from the live Airtable table itself, which represents years of prior human curation, rather than guessed at).
  • A lamas-ingest Claude Code skill (.claude/skills/lamas-ingest/SKILL.md) for the recurring workflow of ingesting a newly-published year.
  • A pytest QA suite (Lamas/tests/, run via lamas qa) encoding the checks a human used to do by eye in the old Airtable "Stats" table, plus regression tests for every bug found below.
  • Lamas/docs/CURRENT_BEHAVIOR.md, documenting the pipeline's pre-modernization behavior in depth as a baseline.
  • A new GitHub Action (.github/workflows/lamas-tests.yml) running the test suite on every PR touching Lamas/.

Bugs found and fixed along the way

  • downloader.py: a P_LIBUD/P_LIBUD2 if/if/else bug meant a fresh download of 2022 would silently fetch a broken ~2KB placeholder file instead of the real workbook. Verified live against CBS's site before fixing.
  • Hidden 2-row headers in 2023/2024: a category-label row sitting above the visible column labels wasn't being read (header_rows=1 was too narrow), causing ~93% of the physical/population sheet's columns to be silently dropped, and some property-tax-by-type columns to collide under identical header text across different sections (income vs. expense, charge-amount vs. area) — real data-integrity risk, not just a mapping annoyance. Fixed by widening header_rows/extend_headers_top to match the existing 2016-2022 pattern.
  • fix_years() gaps: never handled the YYYY-YY council-term date format or Hebrew month names in "as of <month> <year>" qualifiers, fragmenting otherwise-identical headers across 86+ headers spanning 1999-2024.
  • value_fixes() unit-conversion bug: matched flat, unprefixed header strings exactly, silently breaking once canonicals gained category prefixes from the real Airtable data — and in several cases the mapping itself had already merged a genuinely-in-thousands raw variant with an already-absolute one under the same canonical, corrupting historical population/gender figures right at the unit-transition year. Fixed the matching to be table-driven and split the mis-merged canonicals; verified the fix produces a smooth, continuous population series across all years.

Test plan

  • lamas qa (pytest suite) passing locally as of the last full run, aside from one known pre-existing issue (duplicate header collisions in 1999/2000/2017-2020, unrelated to this PR, not yet root-caused)
  • Package scaffold verified byte-identical to the original code across 3 different config eras before any behavior changes
  • 2023 and 2024 fully ingested end-to-end and spot-checked (diagnose → preprocess → map-headers → build)
  • New GitHub Action (this PR) will validate the full suite on clean CI hardware — my local runs were significantly slowed by machine contention during development

🤖 Generated with Claude Code

akariv and others added 8 commits July 4, 2026 18:23
Replaces the Airtable-dependent, notebook-driven Lamas CBS municipal
statistics pipeline with a proper installable package (lamas/), a CLI
covering the full workflow (download/diagnose/preprocess/map-headers/
build/qa), and local YAML files for sheet layout config and header
mapping (seeded from the live Airtable "Header Mapping" table, which
represents years of prior human curation).

Along the way, found and fixed several real bugs affecting historical
data quality, not just the modernization itself:
- downloader.py: P_LIBUD/P_LIBUD2 if/if/else bug meant 2022 would
  silently fetch a broken placeholder file on a fresh download.
- A hidden 2-row header (a category label sitting above the visible
  column labels) in the 2023/2024 budget and physical/population
  sheets meant ~93% of the physical sheet's columns were being
  silently dropped, and some property-tax-by-type columns were
  colliding under identical header text across different sections
  (income vs expense, charge-amount vs area).
- fix_years() never handled the "YYYY-YY" council-term date format or
  Hebrew month names in "as of <month> <year>" qualifiers, fragmenting
  otherwise-identical headers across many years (1999-2024).
- value_fixes()'s unit-conversion table used exact-string matching
  against flat header text, silently breaking once canonicals gained
  category prefixes - and the mapping itself had, in several places,
  merged a genuinely-in-thousands raw variant with an already-absolute
  one under the same canonical, corrupting historical population
  figures at the unit-transition year.

Adds a `lamas-ingest` Claude Code skill for the recurring workflow of
ingesting a newly-published year, and a pytest QA suite (run via
`lamas qa` or the new lamas-tests GitHub Action) encoding the checks a
human used to do by eye in the Airtable "Stats" table, plus new
regression tests for the bugs found above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Several tests in test_quality_report.py are deliberately non-blocking
reports (near-duplicate canonicals, naming-convention violations) that
print findings without failing. pytest hides captured stdout for
passing tests by default, which was silently swallowing these reports
in CI - verified the first PR run showed 0 findings surfaced despite
both report tests actually finding things to report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y run

Previously the workflow only ran pytest against the committed YAML
files, so every test gated on a preprocessed checkpoint (or a
downloads/ directory) silently skipped rather than running - not a
meaningful gate. Now the workflow downloads all configured years,
builds the parquet checkpoint, and regenerates the pending-headers
report before running pytest, so the full data-dependent suite
(duplicate-header detection, unresolved-header gating, year-over-year
coverage, etc.) actually executes.

Downloads and the checkpoint are cached (CBS's historical workbooks
are effectively immutable, and the checkpoint cache key is invalidated
whenever extraction-relevant code changes) to keep repeat runs fast.
The download step is best-effort (continue-on-error) so a transient
network hiccup doesn't hard-fail a run that already has a viable
cached checkpoint - the job only fails outright if neither a
checkpoint nor any downloaded data is available at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
requests.get() had no timeout at all - found for real in CI, where a
single unresponsive request to CBS's site stalled a run for 7+ minutes
with no way to recover. Adds a (connect, read) timeout, and makes
download_all() isolate failures per-year (logged and skipped) so one
bad/slow year doesn't abort the whole batch - maximizing how much data
is actually available afterward, consistent with the CI workflow's
"download or fall back to a viable checkpoint" design.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…re header

Found via CI's first real run against freshly-downloaded data:

- fix_value() flagged a batch of legitimate decimals as "BAD FLOATS"
  because Excel wraps numbers in invisible RTL-embedding/pop-directional-
  formatting Unicode control characters in Hebrew sheets - float() can't
  parse a string containing them even though the digits are fine. Also
  folded in a stray '. .' null-sentinel found in the same set.
- CBS revised a source file between my local download and CI's fresh
  one, surfacing a new raw header variant for the income-security-
  recipients metric with different sheet-prefix/end-of-year phrasing.
  Reviewed against the canonical's other 6 orig variants (which already
  cover the same prefix/phrasing pattern for the "during the year"
  counterpart) and confirmed it's the same metric, not a new one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test_no_duplicate_headers_within_sheet was hard-failing on 10 (year,
sheet, header) collisions in 1999/2000/2017-2020 that predate this
test and aren't yet root-caused. Investigated each: the 1999/2000
"אחוז שינוי ריאלי לעומת <year>" ones carry genuinely different values
per municipality (a real extraction ambiguity in the older .xls
format), while the 2017-2020 budget-sheet ones carry identical
duplicated values (a harmless redundant column in CBS's own report).
Both are isolated enough not to block on.

Carves out a fixed KNOWN_DUPLICATE_HEADERS allowlist (reported via
print, not silently dropped) so the test stays a hard gate for any
new/different collision without re-flagging these understood ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers what the pipeline does, the lamas CLI command reference, the
lamas-ingest skill, and a manual step-by-step walkthrough for
onboarding a newly-published year's Excel file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…GH Action

Replaces the dataflows-based dump_to_sql call with a simpler
psql \copy of the already-built CSV file - matches how a human would
push this by hand, and decouples the Postgres push from re-serializing
the whole dataset a second way. Truncates the target table first by
default (matching the old dump_to_sql(mode='rewrite') semantics), or
--no-truncate to append.

New CLI: `lamas push-postgres [--csv PATH] [--table T] [--truncate/
--no-truncate]`, usable standalone or via `lamas build --output
local,postgres`. Verified end-to-end against a disposable smoke-test
table on the real database (created, copied 5 rows in, verified,
dropped) before wiring anything to the production table.

Added .github/workflows/push-postgres.yml - workflow_dispatch only
(never runs on push/PR), requires typing "PUSH" to confirm, runs the
full QA suite before pushing. Uses the DATAFLOWS_DB_ENGINE repo secret
(set via `gh secret set`, never written to any file in this repo).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@akariv akariv merged commit e92eae1 into master Jul 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant