Skip to content

Emit PolicyEngineRunner output columns in one canonical order - #1438

Open
MaxGhenis wants to merge 4 commits into
mainfrom
canonical-output-column-order
Open

MaxGhenis wants to merge 4 commits into
mainfrom
canonical-output-column-order

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #1426. The order of columns in PolicyEngineRunner output depended on what else was in the batch. The same two records put frate in column 8 or column 40, depending only on which tax year carried idtl=0.

The runner simulates one tax year at a time. Each chunk's frame carries only the columns its rows' idtl levels ask for, and the chunks are joined with pd.concat, which keeps columns in order of first appearance. A chunk that asks only for the standard columns puts frate, srate ahead of the full-detail columns a later chunk adds.

Change

run() now puts the joined frame in one canonical order: the three identifiers, the implemented columns in the order of the policyengine_to_taxsim mapping, then the marginal rates. The order is derived from the mapping, not listed by hand.

That is the order a batch already had when all its chunks carried the same columns, so batches with a single idtl level normally keep their layout. Which columns appear still depends on what the batch asks for; their relative order no longer does. Per decision d1312 this keeps the runner's current layout and does not move to TAXSIM's header order.

What changes for users: only batches whose chunks carry different column sets. In practice that is batches that mix idtl levels across tax years, which now get the same layout as a full-detail batch, and the rare batch where one year's taxsimid values are all blank, which used to put taxsimid last and now puts it first. Values are unchanged.

The ordering is applied where run() returns, not at the pd.concat lines, to stay clear of #1406 and #1423, which edit those lines.

Invariants

  1. Batch independence. The output columns are the canonical order restricted to the columns the batch carries. Which rows accompany a record never moves a column relative to another.
  2. Batches with uniform chunks unchanged. When every chunk carries the same columns, the ordered frame has the same column order as the frame before ordering. That covers single-idtl batches unless one year's IDs are all blank.
  3. Nothing added, dropped or moved between names. The same set of columns comes back and every value stays under its own column name.
  4. Unknown columns survive. A column the mapping does not know is kept, after the known ones, in the order it arrived.

Invariants 1, 3 and 4 have Hypothesis property tests on the ordering function. Invariants 1, 2 and 3 are also checked on real runs.

Verification

Run on this branch (base main at 72fdc04), macOS, with the 11 tests in tests/test_output_column_layout.py.

Check Result
New tests on this branch 12 passed (after merging main at be55c81)
Same tests with the ordering turned into a no-op 4 fail: both property tests, the run() wiring test and the mixed-batch layout test. The single-idtl and value tests pass, as they should
ruff format --check . clean

The real-run tests take each batch's frame from _run_once, before ordering, and compare it with the ordered frame. So "unchanged" is measured against what the runner produced before this change, for idtl 0, 2 and 5. The two mixed batches are the ones from #1426. The no-op run used a copy of the package on PYTHONPATH; the branch itself was not edited.

Review

An independent review (GPT-6.1 Sol) approved with no blocking findings. It ran the tests, confirmed the no-op version fails them, and found one case the "unchanged" claim missed: a year whose taxsimid values are all blank. The third commit qualifies the claim, rewords "position" as "relative order", and tests that case. It found no positional use of runner columns in policyengine_taxsim/, tests/, r-package/ or dashboard/.

Not covered

  • Batches routed to TAXSIM. StitchedRunner sends pre-2021 rows to the TAXSIM binary. A batch with only such rows comes back in TAXSIM's own layout; a batch that also has later years uses PolicyEngine's layout with the TAXSIM-only columns after it. That difference stays, since d1312 keeps each current layout.
  • RemoteTaxsimRunner over 2,000 rows. It joins 2,000-row batches the same way (remote_taxsim_runner.py line 216). I read that code but did not run it against the service; tracked separately in RemoteTaxsimRunner: column positions may depend on batch order for inputs over 2,000 rows #1439.

axiom: n/a: emulator output layout, no policy change

🤖 Generated with Claude Code

Each chunk's frame carries only the columns its rows' idtl levels ask
for, and pd.concat keeps columns in order of first appearance. So a
column's position depended on which chunk first carried it: a 2021
idtl=0 record followed by a 2022 idtl=2 record put frate in column 8,
and the reverse put it in column 40.

run() now reorders the joined frame to one canonical order: the
identifiers, the implemented columns in mapping order, then the
marginal rates. That is the order a single-idtl batch already has, so
those batches are unchanged.

Refs #1426

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

vercel Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policyengine-taxsim Ready Ready Preview Oct 12, 2026 12:44pm UTC

Request Review

- Hypothesis properties of the ordering: same columns back, known ones
  in canonical order, unknown ones after in arrival order, values kept
  under their names; and one layout whichever chunk comes first.
- run() applies the ordering to what the simulation pass returns.
- Real runs: the two mixed batches from #1426 get the full-detail
  layout, each record's values match the full batch column by column,
  and batches with a single idtl level (0, 2, 5) are unchanged from the
  frame _run_once produced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 12, 2026 08:32
- Say "relative order" rather than "position": which columns appear
  still depends on what the batch asks for.
- Qualify "single-idtl batches keep their layout" to batches whose
  chunks carry the same columns. A year whose taxsimid values are all
  blank dropped that column, and pd.concat then put it last; it now
  comes first. Test that case with the layout _run_once returned for it.

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

This branch was successfully deployed

1 active deployment
Preview — 0f5f8adf Deployed Oct 12, 2026 by vercel[bot]
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.

Output column positions depend on which idtl levels and years are in the batch

1 participant