You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The executor's entity frames are microdf MicroDataFrames weighted by <entity>_weight, so .sum(), .mean() and .count() on any column are already weighted.
In #766 (fixed by #767), build_budgetary_impact passed "household_weight" to _sum_output_variable, the helper it uses for ordinary values. That weighted the weight column by itself, giving the sum of squared weights.
Effect:budget.households was about 10^4 times too high on every run (New York 2026: 135.6 billion, against about 7.5 million households). Clients divide by it, so the average change per household was about 10^4 times too small.
Why it escaped: the builder test used plain pd.DataFrames, which carry no weights.
Nothing stops the same class of mistake from coming back.
Proposal
Phase 1 step 1.9. Register the rule that weights aren't values in rules/registry.toml, with AST guards and proofs:
sim.microdata.weights-not-values (doc-only): weights, values, labels and counts each allow different operations.
sim.microdata.weights-not-aggregated-as-values: a weight is never the variable of a weighted aggregator, and never frame[<weight>] followed by a weighted reduction.
sim.microdata.weight-stripping-confined: raw values are read from a weighted frame only in simulation_output_common.py.
Counting entities is always allowed there.
Any other read there is a recorded exception, in a list that may only shrink.
sim.microdata.builder-tests-use-weighted-frames: output-builder tests build entity frames through one shared weighted factory, never as plain pd.DataFrames.
Alongside, two cheap runtime checks that the AST guards can't replace:
change totals (reform minus baseline) log a warning when the two sides carry different weights;
an implausible published household count fails the run.
Decisions
UK local-authority handoff. The breakdown hands policyengine's longwise geography impact a plain household frame, and that function weights by household_weight itself. The handoff moves into simulation_output_common.py as the stripping rule's one recorded exception. Removing it is tracked separately.
Weight mismatch: warn first. It logs a warning and still publishes the total. It becomes an error once logs show that no legitimate run produces a mismatch.
Household count: fail the run. A failed run is better than a headline that's 10^4 times wrong.
Activity