Repository navigation
Break wealth-decile ties by a fixed hash of household_id - #2618
PavelMakarchuk wants to merge 2 commits into
Conversation
juaristi22
left a comment
There was a problem hiding this comment.
Reviewed at cba9e1ca81425f153c979c2367b36cadee54a283. One should-address finding is attached inline: fractional-weight normalization still depends on input row order, so the hash alone does not establish shuffle-invariant deciles.
Validation:17 existing/direct/oracle tests passed, including500 independent synthetic cases with exact binary denominators. A separate fractional-weight shuffle regression reproduces the remaining failure. The hash matches the original SplitMix64 source, and the documented endpoint/large-household ranking convention is otherwise consistent. No population or wealth-tax impact claim is made.
| ) | ||
| decile = weighted_wealth.decile_rank().values | ||
| # A zero-weight household at the start of the order would get 0. | ||
| decile = np.clip(weighted_wealth.decile_rank().values, 1, 10) |
There was a problem hiding this comment.
[P2] Normalize weights in a deterministic order as well as ranking them
The hash fixes sorting, but MicroSeries.rank(pct=True) still normalises by the sum of weights in input row order. Fractional weights can round to a different denominator after a shuffle and change the deciles this PR promises to keep invariant. With zero wealth/size1, IDs [3,1,2,0] and weights [0.1,0.2,0.3,0.4] yield [1,4,7,10]; permuting rows by [1,3,2,0] and restoring ID order yields [1,3,6,10] (the denominator becomes 1.0000000000000002). This reproduces with the locked microdf-python1.5.11. Compute normalization in the same deterministic order as ranking, restore the original row order, and add a fractional-weight shuffle regression.
Ties in household_wealth_decile split in row order, which carries information and can move when a dataset is rebuilt. They now split by a salt-free splitmix64 hash of household_id, then by row order, so tied households keep their deciles in any file order. Every household with non-negative wealth now gets a decile from 1 to 10, also one with zero weight at the start of the order, which used to get 0. Fixes #2617 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cba9e1c to
036eea6
Compare
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
Standard reform costings (change in gov_balance)
Standard reforms: change in each reform's target
Household net income by decileNo household's net income changes in any year. No change in any year: receipts by tax head; government balance, each component counted once; model totals; household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles. Checks failing on both base and head (1)
Counted-once checkEach of 86 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
The formula now reads household_id, so the identifier round-trip test computes it on generated frames, one of which has no weight at all. Count each person equally there instead of raising. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Rebased onto main after #2583 merged. One extra commit (47ee051): the formula now reads |
Fixes #2617
Stacked on #2583. The base is María's branch
fix-2579-wealth-decile-ties. After #2583 merges, retarget this PR tomain.What changes
policyengine_uk/variables/household/wealth/household_wealth_decile.py:household_id(a splitmix64 mix in numpy onuint64), then by row order. The result is the same on each run, and a dataset rebuild that reorders households does not move any tied household's decile. Python'shash()is not used, because it is salted per process.[0, 0, 5], weights[0, 1, 1]gave[0, 5, 10]). Negative wealth still gets-1.documentationnow describes the hash rule and the clip, in place of the row-order rule.household_income_deciledoes not change.Tests
Reproducer: policyengine_uk/tests/test_household_wealth_decile.py::test_tied_households_keep_their_decile_when_rows_are_shuffled, policyengine_uk/tests/test_household_wealth_decile.py::test_tie_order_does_not_follow_household_id_order, policyengine_uk/tests/test_household_wealth_decile.py::test_zero_weight_household_gets_a_decile_of_at_least_1
household_idorder or row order;e29046a4), all three new tests fail, and the 8 existing tests pass. With this change, all 11 pass.ids_in_hash_order, whose hash order is the row order. So every existing expected value and comment stays the same. The file's docstring now names the hash rule.test_household_wealth_decile.pyandtest_household_income_decile.py: 15 passed.reforms/parametric/wealth_tax.yaml: 33 passed.ruff format --check .andruff checkpass.I did not run the aggregate-impact report locally. CI runs it.
axiom: n/a: microsimulation wealth ranking; no legislative tax or benefit rule changes.
🤖 Generated with Claude Code