Repository navigation
Add marginal_tax_rate_computed and fix cliff_evaluated ranking - #9653
Merged
Merged
Conversation
The marginal tax rate variables are simulated only for the top simulation.marginal_tax_rate_adults adults per household by market income; everyone else gets a placeholder zero that looks like a real one. Add a flag for exactly the simulated people, define cliff_evaluated as that flag (it ranked adults by age, while cliff_gap comes from the earnings-ranked marginal_tax_rate), and document the cap on every affected variable and the parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 tasks
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9653 +/- ##
===========================================
- Coverage 100.00% 92.82% -7.18%
===========================================
Files 4 8 +4
Lines 76 223 +147
Branches 2 8 +6
===========================================
+ Hits 76 207 +131
- Misses 0 15 +15
- Partials 0 1 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Describe the ranking as market income rather than earnings, name the five variables the flag covers and exclude the separately ranked capital gains MTR, add federal and FICA positive oracles for the component helper, add negative market income households to the property grid, and scope the cap-monotonicity invariant to simulations without behavioral responses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The batched YAML runner caches one tax-benefit system copy per distinct parameter override for the life of each subprocess. Four new marginal_tax_rate_adults overrides raised household batch 4's peak RSS from 17.6 GB to 22.7 GB and killed the 16 GB CI runner. Keep YAML cases on the default cap and check the cap-3 roommate rates in pytest, where simulations are freed per test; batch 4 now peaks at 17.7 GB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Merged by the US + core hub on the merge gates:
|
This was referenced Oct 4, 2026
MaxGhenis
added a commit
that referenced
this pull request
Oct 6, 2026
Conflict: tests/policy/baseline/household/marginal_tax_rate.yaml. Both sides appended cases at the end of the file. Kept main's comment fix and its three-roommate case from #9653, then this PR's six Ohio base/alt/rate cases. The rate variables' docstrings from #9653 merged cleanly next to this PR's formula changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 6, 2026
… moving inputs Rates measured in a reformed simulation with labor supply or capital gains elasticities now hold the responses at their simulated values. Since the branches raise pre-response inputs (employment_income_before_lsr, long_term_capital_gains_before_response), recomputing the responses there scaled them with the raise: a Texas worker's $1,000 raise became $971 of wages (rate 0.3166, fresh 0.2965), and a 20% capital gains rate read 0.3307. create_perturbed_branch, which every earnings and capital gains rate uses, now sets the three earnings responses, the capital gains response and the weekly hours response to their values in the simulation; neutralized ones, as in the behavioral response measurement branches, stay zero. Core branches a reformed simulation's baseline while constructing it, before the wrappers move inputs. That branch kept employment_income and weekly_hours_worked as inputs with empty pre-response variables and the stale input_variables, so its TANF counted no earnings and the baseline rate that labor supply responses compare against erased pre-response wages: an Ohio Works First parent got a -$416 response to a top-rate reform that does not touch them. Both wrappers now branch the baseline again after the moves, as core's subsample does. Adds tests for both (invariants 5 and 6) and corrects the #9653 comment on the component helper's adult loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Refs #9652. This makes the placeholder zeros identifiable and fixes
cliff_evaluated. It does not change which people get a simulated marginal tax rate; the cap default and the downstream follow-ups stay open on the issue.Summary
The marginal tax rate variables are simulated only for the top
simulation.marginal_tax_rate_adults(default 2) adults per household, ranked by market income. Everyone else, including every earner under 18, gets a0.0that is indistinguishable from a real zero. On the certifiedpopulace_us_2024that covers 18.1M working-age wage earners and $436B of wages; the issue has the evidence.marginal_tax_rate_computed(bool, Person):is_adult & 1 <= adult_earnings_index <= simulation.marginal_tax_rate_adults. For formula-derived ranks it is exactly the set of people the MTR loops perturb, so consumers can filter placeholder zeros out of distributions, averages and cliff shares. (The loops test onlyadult_earnings_index == k; the flag also requiresis_adult, which differs only if someone overridesadult_earnings_indexas an input for a child.) Stored-household API calculations return it with every other yearly variable. It does not covermarginal_tax_rate_on_capital_gains, which ranks adults separately.cliff_evaluated. It ranked adults by age (adult_index), butcliff_gaphas come from the earnings-rankedmarginal_tax_ratesince Fix cliff variables #5322. It is now defined asmarginal_tax_rate_computed. On the certified file, the old definition disagreed with the people actually simulated for 14,042 records (16.6M weighted in each direction). Nothing in pe-us, policyengine.py, API v1 or API v2 reads it; I checked each by grep.marginal_tax_rate,marginal_tax_rate_including_health_benefits,federal_marginal_tax_rate,state_marginal_tax_rate,fica_marginal_tax_rate, the parameter, andmarginal_tax_rate_on_capital_gains, whose loop is separately hardcoded to two adults ranked by capital gains. The API serves the variable strings in/us/metadata;simulation.*parameters are not served there.No marginal tax rate formula changes, so no MTR, cliff gap or labor supply number changes.
Invariants
These hold for every input whose ranks come from the model's own formulas (not an overridden
adult_earnings_index), and are tested intests/core/test_marginal_tax_rate_coverage.py, using the repo's grid pattern: 66 households varying adult count (1-5), earnings order, ties, zero earners, negative market income (self-employment losses), teen earners and children, all in one simulation per cap. The selection rule is restated there withoutadult_earnings_index.min(cap, adults)people. All of them are adults, and each flagged adult has at least the market income of every unflagged adult.cliff_evaluated == marginal_tax_rate_computed.marginal_tax_rate,marginal_tax_rate_including_health_benefitsandfederal_marginal_tax_rateare exactly 0 wherever the flag is false. These cover the three loop implementations: the net-income loop, its health-benefit copy, andcompute_component_mtr.Mutation check: restoring the old age-ranked
cliff_evaluatedmakes invariant 2 (the equality assertion) fail at caps 1, 2 and 3. It passes at caps 0 and 5, as it should, because there every adult or no adult is flagged.Tests
The YAML cases all use the default cap. The batched YAML runner caches one full copy of the tax-benefit system per distinct parameter override for the life of each subprocess. An earlier version of this PR added four new cap overrides to the household shard, and they raised batch 4's peak RSS from 17.6 GB to 22.7 GB (measured locally). That killed the 16 GB runner. Every case that varies the cap now lives in pytest, where each simulation is freed after its test.
marginal_tax_rate_computed.yaml:marginal_tax_rate.yaml: three single roommates in Texas in 2024 with wages of $90k, $60k and $30k. After the $14,600 standard deduction they fall in the 22%, 12% and 12% brackets, plus 7.65% employee FICA. Under the default cap the third earner is a placeholder 0 inmarginal_tax_rate,federal_marginal_tax_rateandfica_marginal_tax_rate.cliff_evaluated.yaml: the 70/40/38/22 household. The old code returns [T, T, F, F]; the fix returns [F, T, T, F].test_marginal_tax_rate_coverage.py::test_roommates_match_hand_computed_rates: the roommate household at caps 2 and 3. At cap 3 the third earner getsmarginal_tax_rate0.1965,federal_marginal_tax_rate0.12,fica_marginal_tax_rate0.0765 andstate_marginal_tax_rate0. These hand-computed values are positive oracles for both the net-income loop andcompute_component_mtr.Options considered
NaN for uncomputed people was rejected. Under any labor supply reform it propagates into
employment_incomeandhousehold_net_income(0 × NaN is NaN), which makes policyengine.pyeconomic_impact_analysisraise. It also puts bare NaN tokens in API v1 JSON and fails the simulation API's strict contract. The issue has the full comparison.axiom: n/a: a simulation-coverage flag and documentation; no statutory provision changes.
Reviewed independently by GPT-6 Astra (subfleet
review, tier hard): APPROVE with documentation and test-precision nits, all addressed in the second commit, which Astra re-reviewed and approved at dd8db7d. A third commit moves the cap-override cases from YAML to pytest to fit the household shard's memory budget. It adds no new assertions beyond the roommate cap-3 check, which was previously in YAML. It independently re-derived the roommate arithmetic and the labor supply example.🤖 Generated with Claude Code