Fix size-less RNG draws that made 15 columns constant; add tests and an answer key - #8
Merged
Merged
Conversation
…an answer key
Twenty-two call sites across ten generators called numpy's RNG without a size
argument, so the draw returned a scalar that broadcast across the whole column:
owns_radio = rng.binomial(1, 0.55) # one draw, every row
months_displaced = np.where(displaced, rng.exponential(14), 0) # one draw, every row
Nothing raised, the row count was right and the file wrote. Fifteen columns were
constants, among them four asset variables in `targeting` (a proxy-means-test
dataset whose asset predictors did not vary) and the three dropout barriers in
`girls_education`, all identically zero. The neighbouring `barrier_cost` was
always fine because its probability is an array, which is what confirmed the
diagnosis.
None of it was visible because CI ran `python generate.py --list`, which imports
no generator. All 36 modules were unexecuted by any automated check.
Generators
- Add the missing size argument at all 22 sites. Scalar draws that are genuinely
parameters (per-arm compliance rate, per-country intercept, anything inside a
per-row loop) are left alone.
- rct_experiment: `spillover_risk` was 1 for every control and 0 for every
treated unit, an exact alias of the treatment dummy, because randomisation was
stratified within district so every district always held treated units. Add a
village level, randomise 70% of villages into treatment and 30% into pure
control, and give exposed controls a real +3% spillover. The flag now varies,
and using contaminated controls biases the ITT toward zero by ~0.011 log
points, which is the lesson the design should teach.
- public_health: rebuild PHQ-9 as a sum of nine 0-3 items. The previous
`9 * logistic(...) * 3` was bounded with no item-level variance, so the floor
sat at 2 and `depression_severe` was identically zero in every draw. Now spans
0-27 with 14.6% at or above the moderate threshold and 1.45% at severe.
- girls_education: schooling status is three-state. `dropped_out` was defined as
the complement of `enrolled`, which both collapsed never-enrolled girls into
dropouts and made the two columns perfect aliases. Also vectorise the distance
loop and remove a dead `2 if True else 5` conditional.
Tests and documentation
- tests/: 233 tests. Every generator runs, is reproducible under a seed, and
responds to a seed change; the registry matches the modules on disk; no column
is constant or a perfect alias of another, with the legitimate exceptions
(IRT item parameters, the poverty line) listed with reasons.
- TRUTH.md: the answer key. Records which parameters are in the data and which
estimand recovers each. The ITT is not a stable target because take-up is
drawn U(0.65, 0.85) per run and moved between +0.089 and +0.203 across five
seeds; the Wald LATE recovers theory to within 0.002.
- CLAUDE.md: new, so the next session does not re-derive this.
- CI: run the suite on 3.11 and 3.12, plus an end-to-end job writing every
dataset to CSV and Parquet, since serialisation fails differently.
- Pin requirements exactly. `pandas>=2.0` would pick up pandas 3, where text
columns carry a `str` dtype and a check keyed on `dtype == object` silently
skips every text column.
- README: correct the output directory (`output/`, not `data/`), the Python
floor (3.11), a dependency list naming `faker`, which nothing imports, and
claims that the data mirrors the distributions of real surveys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrvR2NXsJFVRCeJZFCPuNL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six months since the last commit. The substantive finding is a defect that made part of the data silently useless.
The bug
rng.binomial(1, 0.55)without a size argument returns a scalar, which numpy broadcasts across the whole column. Nothing raises, the row count is right, the CSV writes, and the variable is a constant.Twenty-two call sites across ten generators. Fifteen constant columns, including:
targeting(rooms,owns_radio,owns_mobile,owns_bicycle).targetingexists to teach proxy means testing. A PMT whose asset predictors do not vary is not a PMT.girls_education(barrier_marriage,barrier_pregnancy,barrier_household_chores), all identically zero.The tell that confirmed it is still in the file:
barrier_coston the neighbouring line takes an array probability (0.35 - 0.10 * receives_scholarship), so numpy returned a vector and that column was always fine.Why nothing caught it. CI ran
python generate.py --list, which imports no generator. All 36 modules were unexecuted by any automated check.Two further defects the new guard found
rct_experiment.spillover_riskwas an alias of the treatment dummy. 1 for every control, 0 for every treated unit, because randomisation was stratified within district so every district always contained treated units. Any regression including both dropped a collinear term.Fixed by adding a village level: 70% of villages are randomised into treatment, 30% into pure control, and exposed controls now receive a real +3% spillover. The flag varies, and the contamination lesson is now visible in the data — using all controls as the comparison biases every ITT toward zero by ~0.011 log points.
public_health.depression_severewas identically zero in every draw. PHQ-9 was built as9 * logistic(latent) * 3: a bounded transform of one normal with no item-level variance, so the floor sat at 2, the median at 8, and nothing reached the severe threshold of 20. Rebuilt as the sum of nine 0–3 items.Wealth and sex gradients run the right way (quintile 1 at 22.0% moderate vs quintile 5 at 10.4%; women roughly twice men on severe).
girls_educationschooling status is now three-state.dropped_outwas defined as the complement ofenrolled, which both made them perfect aliases and collapsed never-enrolled girls into dropouts. Those are different problems with different policy responses. Now 7.0% never enrolled / 80.6% enrolled / 12.4% dropped out.Tests
233 tests, about two minutes. The largest part is the degeneracy guard, which runs against every registered generator automatically.
TRUTH.mdare recoverable within toleranceTRUTH.md
A practice dataset you cannot check your answer against is a worked example with the answer torn off. This records which parameters are in the data and which estimand recovers each one.
The point worth reading: the ITT is not a stable target. Take-up is drawn
U(0.65, 0.85)per arm per run, so it moved between +0.089 and +0.203 log points across five seeds. Only the complier effect is stable, and only against pure-control villages. The Wald LATE recovers theory to within 0.002 log points across those seeds, which is what the test asserts.Maintenance
CLAUDE.mdadded, so the next session does not re-derive any of thispandas>=2.0would pick up pandas 3, where text columns carry astrdtype rather thanobject, so a check keyed ondtype == objectsilently skips every text columnoutput/notdata/; Python floor is 3.11 not 3.9; the dependency list namedfaker, which nothing imports; and the claim that the data mirrors the distributions of real surveys was removed, since it does not andTRUTH.mdsays soThe
840,000+ rowsclaim was checked rather than assumed. It is 841,247.Generated by Claude Code