Skip to content

Commit cd25c59

Browse files
akarivclaude
andcommitted
Add Lamas/README.md for human operators
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>
1 parent 1b9c298 commit cd25c59

1 file changed

Lines changed: 158 additions & 0 deletions

File tree

Lamas/README.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Lamas
2+
3+
Downloads, parses, and normalizes the Israeli Central Bureau of Statistics ("Lamas") yearly
4+
municipal statistics workbooks (1999-2024, one Excel file per year) into tidy
5+
`(year, name, header, value, filename)` rows, ready to load locally or into Postgres.
6+
7+
There is no Jupyter notebook and no Airtable dependency - everything runs through the `lamas` CLI,
8+
backed by two local YAML config files that are the only state that needs to persist between runs:
9+
10+
- `data/sheet_config.yaml` - per-year/per-sheet Excel layout overrides (header row counts, which
11+
sheets to skip, etc.)
12+
- `data/header_mapping.yaml` - canonical header name -> list of raw header text variants seen
13+
across 26 years of shifting CBS spreadsheet layouts
14+
15+
If you're looking for the deep history of *why* the pipeline works the way it does (the original
16+
pre-modernization behavior, bugs found along the way, etc.), see `docs/CURRENT_BEHAVIOR.md`.
17+
18+
## Setup
19+
20+
```bash
21+
pip install -e ".[dev]" # from inside Lamas/; [dev] adds pytest + the one-time Airtable seed script
22+
```
23+
24+
This registers the `lamas` console script. A `.env` file (not committed) holds
25+
`DATAFLOWS_DB_ENGINE` (a Postgres connection string) if you intend to push output there, and
26+
`AIRTABLE_API_KEY`, only needed for `scripts/seed_mapping_from_airtable.py` (a one-time migration
27+
helper, not part of normal operation).
28+
29+
## The `lamas` CLI
30+
31+
Every step of the pipeline is its own subcommand, so you can run the whole thing (`full-run`) or
32+
drop into just the step you need while iterating on a new year.
33+
34+
| Command | What it does |
35+
|---|---|
36+
| `lamas download [--year Y]` | Download one year, or every year configured in `downloader.py` (idempotent - skips files already on disk). |
37+
| `lamas diagnose --year Y [--sheet NAME]` | Dry-run parse of a workbook against the *current* `sheet_config.yaml`, without writing anything. Prints, per sheet: row/column counts, detected name-column index, a preview of the first extracted headers, or the exact parse error. This is the tool for iteratively tuning `sheet_config.yaml` for a new or misbehaving year. |
38+
| `lamas config show --year Y` | Print the effective sheet config (defaults + overrides) for that year. |
39+
| `lamas config set-sheet --year Y --sheet NAME [--header-rows N] [--extend-top N] [--extend-bottom N] [--skip/--no-skip]` | Create/update one sheet's config entry; rewrites `sheet_config.yaml` deterministically. |
40+
| `lamas preprocess [--year Y] [--checkpoint PATH]` | Parse the downloaded workbook(s) and write a parquet checkpoint (default `.cache/preprocessed.parquet`) - this is the expensive step (~2-3 min for the full 1999-2024 corpus). |
41+
| `lamas map-headers [--year Y] [--strict]` | Resolve every row's raw header against `header_mapping.yaml` and (re)write `reports/pending_headers.csv` for anything that didn't resolve cleanly. `--strict` exits non-zero if any row is fully `unresolved` (useful in CI). |
42+
| `lamas mapping add --canonical "..." --orig "..."` | Add a raw header as a variant of a canonical (existing or brand new). |
43+
| `lamas mapping confirm-fuzzy --orig "..."` | Accept the fuzzy-match suggestion already recorded for a pending row in `pending_headers.csv`. |
44+
| `lamas mapping reject-fuzzy --orig "..." [--canonical "..."]` | Override a fuzzy suggestion: map to a different canonical, or omit `--canonical` to make it a brand-new one. |
45+
| `lamas stats [--year Y]` | Regenerate `reports/header_stats.{csv,md}` - per-header row counts and year coverage, the local replacement for eyeballing the old Airtable "Stats" table. |
46+
| `lamas build [--strict] [--output local,postgres] [--output-dir PATH]` | Apply `specific_fixes`/`value_fixes` and write the final output. `--strict` refuses to build while any header is unresolved. Defaults to local-only output. |
47+
| `lamas full-run [--year Y] [--strict] [--output ...]` | `download` -> `preprocess` -> `map-headers` -> `stats` -> `build`, one shot. |
48+
| `lamas qa` | Runs the full pytest suite under `tests/` - the automated quality gate (see below). |
49+
50+
Run `lamas <command> --help` for the full flag list on anything above.
51+
52+
### Quality assurance
53+
54+
`lamas qa` (or plain `pytest tests/`) runs the same checks a human used to do by eye in Airtable's
55+
"Stats" table, now automated: no unresolved headers, no near-duplicate or unit-mixed canonicals in
56+
`header_mapping.yaml`, no duplicate header collisions within a sheet, sane per-year/sheet row
57+
counts, and year-over-year header coverage (a header reported in 5 straight years must still be
58+
reported in the 6th, catching silently broken extractions - see `tests/test_quality_report.py` for
59+
the full list and the reasoning behind each check, including the small set of already-investigated
60+
exceptions that are allowed to stay).
61+
62+
CI (`.github/workflows/lamas-tests.yml`) runs this same suite on every PR touching `Lamas/`,
63+
downloading and preprocessing the real data first (cached between runs) so the data-dependent
64+
checks actually execute rather than skip.
65+
66+
## The `lamas-ingest` skill
67+
68+
`.claude/skills/lamas-ingest/SKILL.md` is a Claude Code skill that walks through ingesting one
69+
year end-to-end, orchestrating the CLI above rather than reimplementing any parsing logic. Invoke
70+
it (or just ask Claude to "ingest year X") when a newly-published CBS workbook needs onboarding, or
71+
when an existing year has unresolved headers or config gaps.
72+
73+
It encodes one hard rule worth knowing even if you're doing this by hand: **every unmapped header
74+
gets resolved one by one, by a tight heuristic or explicit semantic review - never in bulk, and
75+
never just because "no better candidate was found."** An earlier version of this process got this
76+
wrong once (bulk-accepting ~800 headers with no cross-checking), which fragmented what should have
77+
been single metrics into near-duplicate canonicals. The skill file explains what a real per-header
78+
review looks like, with worked examples of fuzzy-match suggestions that looked right but weren't.
79+
80+
## Walkthrough: ingesting a new year's Excel file
81+
82+
This is what the skill above automates, spelled out as a manual sequence of CLI calls:
83+
84+
1. **Download it.**
85+
```bash
86+
lamas download --year 2025
87+
```
88+
Confirms the file lands in `downloads/`. If CBS has changed their URL/filename pattern (rare,
89+
but check `lamas/downloader.py`'s `P_LIBUD`/`P_LIBUD2` special cases if this fails), fix the
90+
pattern there first.
91+
92+
2. **Get it parsing correctly.**
93+
```bash
94+
lamas diagnose --year 2025
95+
```
96+
Look at the header preview for every sheet. A new year usually parses fine using the previous
97+
year's layout, but watch for: sheets with suspiciously few headers, obviously truncated/mis-joined
98+
header text, or an outright parse error. For any sheet that looks wrong, adjust its config and
99+
re-check:
100+
```bash
101+
lamas config set-sheet --year 2025 --sheet "נתוני תקציב" --header-rows 4 --extend-top 2
102+
lamas diagnose --year 2025 --sheet "נתוני תקציב"
103+
```
104+
Repeat until every sheet's header preview looks like real column labels, or mark a sheet
105+
`--skip` if it's legitimately irrelevant (matches the historical pattern of skipped
106+
social-survey/labor-force sheets).
107+
108+
3. **Build a checkpoint and find unmapped headers.**
109+
```bash
110+
lamas preprocess
111+
lamas map-headers --year 2025
112+
```
113+
Check the summary line (`N unresolved, M auto-resolved needing confirmation`) and open
114+
`reports/pending_headers.csv`.
115+
116+
4. **Resolve every pending header - one at a time.**
117+
- For each `auto_resolved_needs_confirmation` row: look at the `suggested_canonical` and
118+
`suggested_score`, compare against the raw header text and a few `sample_values`, and either:
119+
```bash
120+
lamas mapping confirm-fuzzy --orig "<orig_header>" # suggestion is correct
121+
lamas mapping reject-fuzzy --orig "<orig_header>" --canonical "<correct one>" # it's wrong
122+
lamas mapping reject-fuzzy --orig "<orig_header>" # it's actually a new metric
123+
```
124+
- For each `unresolved` row (no fuzzy candidate cleared the threshold): decide whether it's a
125+
variant of an existing canonical (search `data/header_mapping.yaml` for similar text) or a
126+
genuinely new metric, then:
127+
```bash
128+
lamas mapping add --canonical "<existing or new canonical>" --orig "<orig_header>"
129+
```
130+
Never accept a fuzzy suggestion, and never create a new canonical, purely because "nothing
131+
better turned up" - a wrong merge or an unnecessary new canonical is exactly the kind of
132+
fragmentation this whole mapping system exists to prevent.
133+
134+
5. **Confirm the year is clean.**
135+
```bash
136+
lamas map-headers --year 2025 --strict
137+
```
138+
Exits non-zero if anything is still unresolved - go back to step 4 if so.
139+
140+
6. **Regenerate stats and build the final output.**
141+
```bash
142+
lamas stats
143+
lamas build --strict
144+
```
145+
146+
7. **Run the full QA suite.**
147+
```bash
148+
lamas qa
149+
```
150+
This checks the whole historical dataset, not just the new year - a bad `sheet_config.yaml`
151+
tweak or a wrong mapping decision can regress older years too.
152+
153+
8. **Hand off to Postgres - only when asked.**
154+
```bash
155+
lamas build --output local,postgres
156+
```
157+
This is a shared-system write. Treat it as a suggestion to make to whoever's driving, not
158+
something to run automatically once QA passes.

0 commit comments

Comments
 (0)