Skip to content

Add federal vs. state budgetary impact to economic_impact_analysis#296

Open
MaxGhenis wants to merge 2 commits intomainfrom
add-federal-state-budgetary-impact
Open

Add federal vs. state budgetary impact to economic_impact_analysis#296
MaxGhenis wants to merge 2 commits intomainfrom
add-federal-state-budgetary-impact

Conversation

@MaxGhenis
Copy link
Copy Markdown
Contributor

Summary

Partitions US reform budgetary impact into federal vs. state shares:

  • BudgetaryImpact (federal/state/total) model added to PolicyReformAnalysis
  • calculate_budgetary_impact(baseline_sim, reform_sim) -> BudgetaryImpact helper
  • Example output in examples/us_budgetary_impact.py

Closes #289.

Formulas

federal = Δ(income_tax) + Δ(payroll_tax) - Δ(federal_benefit_cost)
state   = Δ(state_income_tax)            - Δ(state_benefit_cost)
total   = federal + state

Where federal_benefit_cost and state_benefit_cost are the PE-US aggregates from PolicyEngine/policyengine-us#8076 that sum federal/state shares of Medicaid (FMAP) and CHIP (eFMAP). As more programs gain attribution (SNAP OBBBA FY2028, SSI, etc.), the aggregates grow automatically via the parameter-driven adds list — no changes needed here.

Why here vs. in policyengine-api

The arithmetic is thin sums over microsim outputs. Every consumer — API, notebooks, ad-hoc scripts — needs the same split; putting it behind the .py package means one implementation. The API follow-up (PolicyEngine/policyengine-api#3481) becomes a pass-through.

Test plan

  • pytest tests/test_budgetary_impact.py -v — 4 unit tests pass (federal-tax-cut, Medicaid expansion rollback fed/state split, mixed, zero-reform)
  • CI (will require PE-US pin bump to release containing #8076; policyengine-us >= 1.XXX.Y)

Scope notes

  • Programs that are 100% federal (SSI, LIHEAP, WIC, HCV, school meals) and 100% state (state supplements via household_state_benefits) are not folded in here — this exposes only the shared-funding programs in federal_benefit_cost / state_benefit_cost. Follow-up can add federal_only_benefit_cost and state_only_benefit_cost aggregates if useful; for now users can compute those directly.
  • Tax revenue split assumes state_income_tax is fully state (true) and income_tax + payroll_tax is fully federal (true). Not yet modeled: local income taxes, state-level tax credits that show up under federal variables.

Related

Adds BudgetaryImpact model with federal/state/total fields, exposed on
PolicyReformAnalysis and via a new standalone calculate_budgetary_impact
helper. Federal = change in income_tax + payroll_tax minus change in
federal_benefit_cost; state = change in state_income_tax minus change in
state_benefit_cost.

The arithmetic lives here (not in policyengine-api) so every consumer —
API, analysis notebooks, ad-hoc scripts — reuses a single implementation.

Requires policyengine-us with federal_benefit_cost / state_benefit_cost
aggregates (PolicyEngine/policyengine-us#8076). Pin bump is separate.

Closes #289.
Copy link
Copy Markdown

@vahid-ahmadi vahid-ahmadi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shape of this is right and the sign convention is consistent with the rest of the codebase, but I think there's a real bug in the federal-tax computation that the unit tests miss.

Blocking: payroll_tax is not a variable in policyengine-us.

Grepping the installed package: there is no class payroll_tax and no parameter-driven payroll_tax aggregate. Only employee_payroll_tax (TaxUnit) and payroll_tax_gross_wages (a base, not an aggregate) exist. This is the same rename that PR #327 just landed on the program-statistics side.

When economic_impact_analysis calls calculate_budgetary_impact_sum_change(..., "payroll_tax")ChangeAggregate.run()get_aggregate_variable(...), that helper (newly added in #327) will raise:

ValueError: ChangeAggregate.variable references missing variable 'payroll_tax' in policyengine_us version X.Y.Z. Did you mean: 'employee_payroll_tax'?

So this PR currently breaks economic_impact_analysis end-to-end on US. Suggest swapping to employee_payroll_tax to match #327.

The reason the test suite passes is that all four tests patch("...analysis._sum_change", side_effect=...) and feed in mocked deltas. The arithmetic is exercised but the variable names never touch policyengine-us. Worth one thin integration test that calls _sum_change against a real (tiny) simulation for each of the four variables — that's all it'd take to catch this and any future rename.

Also worth resolving before merge

  • The PE-US pin bump for federal_benefit_cost / state_benefit_cost is called out as TODO in the description but isn't in the diff. If this lands before the pin bumps, the same get_aggregate_variable error fires from inside calculate_budgetary_impact for any consumer running economic_impact_analysis. Either bump the pin in this PR, or guard the budgetary-impact computation: try the aggregates; if the variables are missing in the installed PE-US, return BudgetaryImpact(federal=federal_tax_change, state=state_tax_change, total=...) with a logged warning, and make budgetary_impact Optional[BudgetaryImpact] on PolicyReformAnalysis. Either path keeps economic_impact_analysis working across the supported PE-US version range.

Smaller comments

  • state_income_tax is at TaxUnit level (verified). ChangeAggregate(... aggregate_type=SUM) will weight by tax-unit weights, which is what you want — fine. PR #327 mentioned household_state_income_tax because it was specifically materializing per-household output for snapshots; that's a different use case from the national sum here. Keeping state_income_tax is correct.
  • Sign convention check: medicaid rollback test expects +90e9 federal when federal_benefit_cost drops by 90e9. 0 - (-90e9) = +90e9. Consistent with "positive = government saves money" elsewhere. Good.
  • BudgetaryImpact.total is recomputed at construction (federal + state) — would be slightly cleaner as a @computed_field so it can't be inconsistent if someone later constructs the model directly with mismatched fields. Optional.
  • The example script will inherit whichever bug above hits first. Worth re-running it after the variable swap to confirm the printed numbers move in the expected direction.

Verified correct

  • Sign convention matches the rest of the codebase.
  • Arithmetic matches all four test cases under the assumed deltas.
  • Scope notes are honest about which programs are omitted (100%-federal and 100%-state programs not in the shared-funding aggregates).
  • __init__.py exports BudgetaryImpact and calculate_budgetary_impact cleanly.

Happy to re-review once payroll_taxemployee_payroll_tax and the PE-US pin / fallback question are resolved.

@anth-volk
Copy link
Copy Markdown
Contributor

@vahid-ahmadi does this relate to .py's stronger coupling of region and dataset together?

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.

Add federal vs. state budgetary impact to economic_impact_analysis

3 participants