Skip to content

Register weighted-microdata rules, with AST guards and #767 proofs - #3938

Draft
anth-volk wants to merge 1 commit into
masterfrom
microdata-weight-rules
Draft

anth-volk wants to merge 1 commit into
masterfrom
microdata-weight-rules

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3937

Summary

Phase 1 step 1.9: weighted-microdata rules. Stacked on #3936, which removes the unused economy comparison module.

A weighted MicroSeries or MicroDataFrame weights every sum, mean and count, so aggregating a weight column through one returns the sum of squared weights. The Simulation API published exactly that as budget.households until PolicyEngine/policyengine-sim-api#767.

This PR registers the rule that weights aren't values, plus three AST guards for the shapes a source check can recognise.

  • No current code is in scope. Since Remove the unused economy comparison module #3936, nothing in policyengine_api imports microdf. So all three guarded rules start enforced, with no findings and no baseline, and they apply to any new code that handles microdata.
  • No production code changes. The guards are tests. Everything else is:
    • the registry and its rendered views;
    • the proof fixtures;
    • a new skill doc, docs/engineering/skills/microdata-weights.md, with pointers from AGENTS.md and the skills index;
    • a changelog fragment.
  • One rule isn't registered here: the Simulation API's builder-tests-use-weighted-frames. This repository has no output builders.

Rules

Rule Status Findings
api.microdata.weights-not-values: weights, values, labels and counts each allow different operations doc-only —
api.microdata.weights-not-aggregated-as-values: a weight is never the value of a weighted MicroSeries, the variable of a weighted aggregator, or frame[<weight>] followed by sum, mean, median, quantile, std, count, groupby or cumsum enforced 0
api.microdata.weight-stripping-confined: raw values (np.array, np.asarray, .values, .to_numpy(), .tolist(), pd.DataFrame, pd.Series) are read from weighted microdata only in a designated stripping module; none is designated yet enforced 0
api.microdata.unweighted-series-are-labels: a MicroSeries built without weights= is never reduced enforced 0

The guards live in tests/unit/services/test_microdata_weight_boundaries.py, which make test runs in the Test job.

How the guards recognise a weight: a string or f-string ending in _weight or _weights, a name assigned from one, or a .weights attribute.

What counts as weighted microdata:

  • a MicroSeries or MicroDataFrame built with weights=;
  • names assigned from one;
  • anything indexed, filtered or computed from those;
  • in a module that imports microdf, a string-keyed read from a baseline or reform economy-output dict, whose microdata columns sit beside their weight columns.

The guards don't follow data through function calls or containers.

Proof

Each rule has a bad and a good fixture under rules/fixtures/<rule-id>/, and a test_rule_proof_* test asserting exactly which expressions are flagged.

  • Aggregation:
    • The bad fixture is the real pre-Update PolicyEngine US to 0.469.0 #767 Simulation API line, households=_sum_output_variable(baseline, "household_weight", entity="household"), plus the same mistake in this repository's MicroSeries idiom: MicroSeries(baseline["household_weight"], weights=baseline["household_weight"]).
    • The good fixture is Update PolicyEngine US to 0.469.0 #767's replacement, _count_output_entities(baseline, entity="household", weight="household_weight"), plus a plain sum of household_weight.
  • Stripping: np.array(baseline["household_net_income"]).sum() and income.values.sum() are unweighted sample totals. They're checked against weighted MicroSeries totals.
  • Labels: decile.mean() and (age < 18).sum() are checked against grouping and filtering weighted series by those labels.

Testing

  • pytest tests/unit: 2,733 passed, including the 6 guard and proof tests.
  • python scripts/run_quality_guards.py: migration-contracts, rules-registry and ci-scripts passed.
  • python -m scripts.rules guard --base-ref remove-economy-comparison passes, and python -m scripts.rules render leaves no diff.
  • python -m unittest discover -s scripts/rules/tests -t .: OK on Python 3.11 and 3.12.
  • uv run --frozen --extra dev mypy: no issues.
  • ruff check . and ruff format --check . (ruff 0.15.12, from uv.lock) are clean, with no new noqa.

🤖 Generated with Claude Code

@anth-volk
anth-volk added this pull request to stack #3917 October 14, 2026 21:56
@anth-volk anth-volk changed the title microdata weight rules Register weighted-microdata rules, with AST guards and #767 proofs Oct 14, 2026
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch 5 times, most recently from e90bb9d to f680d93 Compare October 15, 2026 15:21
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch 2 times, most recently from dff7b97 to ce635ba Compare October 15, 2026 17:20
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from ce635ba to 514eec9 Compare October 16, 2026 05:40
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from 514eec9 to 7f874be Compare October 16, 2026 06:00
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from 7f874be to 6d73b4e Compare October 16, 2026 06:40
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from 6d73b4e to 847dbb3 Compare October 16, 2026 06:45
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from 847dbb3 to 15b837f Compare October 16, 2026 06:59
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch 3 times, most recently from b1f2ad5 to 82a60a6 Compare October 19, 2026 13:39
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch 2 times, most recently from 139fd25 to cf58774 Compare October 19, 2026 16:32
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch 2 times, most recently from 7b23281 to 228f3c4 Compare October 19, 2026 21:14
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from 228f3c4 to e2395b7 Compare October 19, 2026 21:43
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from e2395b7 to e385a2b Compare October 20, 2026 12:37
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from e385a2b to a51f1ef Compare October 20, 2026 12:42
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from a51f1ef to 21a9d2d Compare October 20, 2026 14:00
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from 21a9d2d to e53e8b6 Compare October 20, 2026 16:31
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch from e53e8b6 to 546c2bd Compare October 20, 2026 18:01
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch 4 times, most recently from 0e0ec38 to 727b155 Compare October 21, 2026 15:49
@anth-volk
anth-volk force-pushed the microdata-weight-rules branch 3 times, most recently from 5f64b54 to 5581422 Compare October 21, 2026 22:31
Base automatically changed from remove-economy-comparison to master October 22, 2026 17:43
A weighted MicroSeries or MicroDataFrame weights every sum, mean and
count, so aggregating a weight column through one returns the sum of
squared weights. The Simulation API published exactly that as its
household count until #767. No module in the package handles microdata
since the unused economy comparison was removed, so four rules now
cover what new code may do with it:

- api.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- api.microdata.weights-not-aggregated-as-values: a weight is never the
  value of a weighted MicroSeries, the variable of a weighted
  aggregator, or frame[<weight>] followed by a weighted reduction.
- api.microdata.weight-stripping-confined: raw values are read from
  weighted microdata only in a designated stripping module, and none is
  designated yet.
- api.microdata.unweighted-series-are-labels: a MicroSeries built
  without weights= is only a grouping or filter key.

The three guarded rules are enforced with no findings and no baseline.
The AST guards run in make test. The aggregation proof uses the real
pre-#767 Simulation API line and the same mistake in this repository's
MicroSeries idiom. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.72%. Comparing base (615ec9e) to head (b9eab24).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff             @@
##           master    #3938       +/-   ##
===========================================
+ Coverage   51.11%   87.72%   +36.60%     
===========================================
  Files         242      258       +16     
  Lines       14441    15864     +1423     
  Branches     2307     2594      +287     
===========================================
+ Hits         7382    13917     +6535     
+ Misses       6436     1280     -5156     
- Partials      623      667       +44     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch was successfully deployed

1 active deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Register weighted-microdata rules: weights aren't values

1 participant