docs(#637): EHCVM people_last7days (t, i, pid) key review -- no key is broken#638
Conversation
…_last7days sites Key-soundness review of the six copy-pasted EHCVM `people_last7days` `.groupby(['t','i','pid'], dropna=False).first()` sites (Benin, Burkina Faso, CotedIvoire, Guinea-Bissau, Senegal, Togo). Verdict: no key is broken. The collapse is a strict NO-OP in all six countries -- there are zero duplicate (t, i, pid) groups, so `.first()`'s per-column skipna=True can form no composite row at all. This commit adds only the evidence: a comment at each site recording that the key was reviewed, what was checked, and the counts. No behaviour change. Measured on the SOURCE directly rather than by instrumenting the build. These tables are `materialize: make`, so they run out-of-process via subprocess; the earlier in-process `DataFrameGroupBy.first` monkeypatch probe never observed them, which is why it reported a meaningless "0" (see the #637 correction thread). country s04 rows s01 rows dup key groups merge fan-out Benin 35,042 42,343 0 / 0 0 Burkina Faso 45,612 45,612 0 / 0 0 CotedIvoire 61,116 61,116 0 / 0 0 Guinea-Bissau 42,839 42,839 0 / 0 0 Senegal 66,120 66,119 0 / 0 0 Togo 25,282 27,480 0 / 0 0 Every country's household-id formatter is injective over the full roster (8,012 / 7,010 / 12,992 / 5,351 / 7,156 / 6,171 distinct (grappe, menage) pairs -> the same number of distinct ids), so no two households -- and hence no two persons -- can be merged onto one key. Cold rebuilds confirm end to end: the wave parquets carry exactly the pre-groupby row counts (Togo 25,282; Benin 35,042; Senegal 66,120) with zero duplicate index entries. Deliberately NOT changed: * `.first()` is left as-is. `.first(skipna=False)` was withdrawn on #637 as a regression -- NaN is absence, not contradiction, and it would return <NA> for values the survey recorded. * No `aggregation:` YAML key (GH #323 D1 forbids it). Also recorded at the sites, so they are not rediscovered as defects: * `dropna=False` governs NA *group keys* only. Rows with NA i/pid are already filtered out immediately above and `t` is a literal, so the kwarg is inert at all six sites. * Senegal: exactly one s04 person-key (grappe 481, menage 9, line 17) has no s01 roster row, so that person's Age/working_age is NA. A one-row roster gap, not a key defect. * Senegal's API-level row count is 65,723, not the parquet's 66,120. The 397-row difference is NOT the v-join and NOT id_walk (both were checked and drop nothing): it is the universal hollow-row safety net `df.dropna(how='all')` at `country.py:2243` removing 397 persons whose every declared column is NA. Unrelated to the key. Refs #637. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c4773f1 to
c7dee21
Compare
Adversarial review — APPROVE_WITH_NOTES. I re-derived every headline number independently and could not break the claim.I did not take a single figure on trust. I rebuilt the measurement from the
Matches the PR table cell for cell, including Attacks I ran and what they found1. "Duplicate counts falling to zero is not evidence." Agreed as a general rule, but it does not bite here: this is not a proposed new key scored against duplicates, it is a demonstration that the existing key is already unique. The two ways a zero can be an artefact were both excluded:
2. "No key is broken" under the wider reading — stranded households. The PR proves injectivity of the 3. End-to-end, cold. Isolated Reproduces the PR exactly, including the 397-row Senegal gap. I re-derived the 397 straight from source rather than accepting the attribution: rows where the age is unknown and all three dummies are outside {1,2} number exactly 397 in Senegal, and 66,120 − 397 = 65,723. Confirmed independently, so the 4. Methodology claim. 5. Scope / house rules. Diff is six files, all under Notes (none blocking)
What I tried and could not breakRe-derived all 6×9 source counts cold and independently; excluded invariance-by-missingness (pre-filter == post-filter rows); excluded a hidden |
Key-soundness review of the six copy-pasted EHCVM
people_last7dayssites named in #637.Verdict: no key is broken. No fix warranted.
The
.groupby(['t', 'i', 'pid'], dropna=False, as_index=False).first()line is a strict no-op in all six countries — there are zero duplicate(t, i, pid)groups, so.first()'s per-columnskipna=Truecan form no composite row at all. Not "the composites it forms are benign": it forms none.This PR therefore adds only the evidence — a comment at each site recording that the key was reviewed, what was checked, and the counts. No behaviour change.
How it was measured
Directly on the source, not by instrumenting the build. These tables are
materialize: make, so they run out-of-process viasubprocess.run(make_cmd, …); the earlier in-processDataFrameGroupBy.firstmonkeypatch never entered those subprocesses, which is why it reported a meaningless0for these sites (per the #637 correction thread, all 38 country-script sites were unmeasured).For each country: load
s04_me_*(7-day module) ands01_me_*(roster) withget_dataframe(), reproduce the merge and thei()/pidconstruction exactly as the site does, then count duplicate groups on the built key and classify each.Per-country verdict
(grappe, menage, s01q00a)in s04 / s01(t,i,pid)groups == rowsin every row of that table. There is no conflicting group to quote evidence for, because there is no duplicate group of any kind.Why the key cannot merge two people
Three independent facts, each checked:
s04is already one row per person. Zero duplicate(grappe, menage, s01q00a)groups in the 7-day module itself.s01also has zero duplicate person-key groups, so thehow='left'merge adds zero rows in all six countries (merged == len(s04)exactly).s04:(grappe, menage)pairsigrappe + zeropad3(menage)ehcvm_i:grappe + '0' + zeropad2(menage)grappe + zeropad3(menage)grappe + '0' + zeropad2(menage)grappe + '0' + zeropad2(menage)grappe + '0' + zeropad2(menage)Two of these are worth naming because they are the ones that could have collided:
menageruns to 527 — the three-digit case that broke the olderi()(the GAP-4 livestock i-key bug documented inehcvm_i's docstring).ehcvm_i's'0'separator survives it with zero collisions.menagetops out at 30, sozeropadding=3always yields exactly three characters and the split point is unambiguous. A future wave withmenage > 999would break that. Recorded in the comment at the site.grappeis a wide code (max 27,711,109), not a 3-digit cluster number as elsewhere; the separated form still collides on none of it.End-to-end confirmation
Cold rebuilds under an isolated
LSMS_DATA_DIR(dvc-cachesymlinked; nodvcCLI invoked anywhere in this work). The wave parquets carry exactly the pre-groupby row counts, with zero duplicate index entries:Deliberately NOT changed
.first()is left as-is..first(skipna=False)was withdrawn on bug: groupby().first() is not first_row() — skipna=True fabricates composite rows (49 sites) #637 as a regression — NaN is absence, not contradiction, and it would return<NA>for values the survey recorded. Each site comment says so, so the attractive-but-wrong fix is not re-derived here.aggregation:YAML key (GH bug: framework silently drops rows on duplicate canonical index (groupby().first()) #323 D1 forbids it).lsms_library/*.pyis touched.Three side observations, recorded so they are not rediscovered as defects
dropna=Falseis inert at all six sites. It governs NA group keys only. Rows with NAi/pidare already filtered out on the line immediately above, andtis a literal. If the author believed it preserved NAs in the values, that belief was mistaken — and harmless here.s04person-key —(grappe 481, menage 9, line 17)— has nos01roster row, so that person'sAge(henceworking_age) is NA. A one-row roster gap, not a key defect.sample()att='2018-19'with zero NAv,_join_v_from_sampledrops nothing, andid_walkdrops nothing (Senegal'supdated_ids['2018-19']is empty). It is the universal hollow-row safety netdf.dropna(how='all')atcountry.py:2243, removing exactly 397 persons whose every declared column is NA (unknown age and non-1/2 codes on all three dummies). Benin and Togo have 0 such rows. Working as designed; unrelated to bug: groupby().first() is not first_row() — skipna=True fabricates composite rows (49 sites) #637.Refs #637.
🤖 Generated with Claude Code