Skip to content

Fix individual_eitc reform not responding to parameter changes#7780

Open
DTrim99 wants to merge 13 commits intoPolicyEngine:mainfrom
DTrim99:fix-individual-eitc-reform
Open

Fix individual_eitc reform not responding to parameter changes#7780
DTrim99 wants to merge 13 commits intoPolicyEngine:mainfrom
DTrim99:fix-individual-eitc-reform

Conversation

@DTrim99
Copy link
Collaborator

@DTrim99 DTrim99 commented Mar 13, 2026

Summary

Fixes the individual_eitc (Winship) reform which was not responding to parameter changes in the app.

Root cause: The reform checked enabled at creation time, not simulation time. Since structural reforms are created with base parameters before user modifications, setting enabled: true in the app had no effect.

Solution: Restructured the reform to follow modern patterns:

  • Check in_effect inside the formula using where() (like GA SB520, NJ budget reforms)
  • Renamed enabledin_effect for consistency
  • Added missing takes_up_eitc multiplier
  • Changed AGI limit behavior: 0 now means "no limit" instead of "no EITC"

Changes

  • policyengine_us/reforms/winship.py - Restructured to check in_effect inside formula
  • policyengine_us/parameters/gov/contrib/individual_eitc/in_effect.yaml - New parameter (replaces enabled.yaml)
  • policyengine_us/parameters/gov/contrib/individual_eitc/agi_eitc_limit.yaml - Updated description
  • policyengine_us/tests/policy/reform/winship.yaml - Updated tests with proper structure

Test plan

  • Tests set in_effect: true to verify reform activates
  • Tests verify baseline EITC when in_effect: false
  • Tests verify AGI limit = 0 means no limit
  • Tests verify AGI limit blocks EITC when exceeded

Closes #7779

🤖 Generated with Claude Code

- Restructure reform to check `in_effect` inside formula using `where()`,
  following modern reform patterns (like GA SB520, NJ budget reforms)
- Rename `enabled` parameter to `in_effect` for consistency
- Add `takes_up_eitc` multiplier that was missing from reform
- Change AGI limit behavior: 0 now means "no limit" instead of "no EITC"
- Update tests to set `in_effect: true` and use proper naming conventions
- Add backward compatibility aliases for existing code

Closes PolicyEngine#7779

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@DTrim99 DTrim99 force-pushed the fix-individual-eitc-reform branch from dc05f03 to a8b2b85 Compare March 13, 2026 16:00
Instead of using simulation branches (which had caching issues),
compute the EITC formula directly for each spouse's individual
earnings using the EITC parameters.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@DTrim99 DTrim99 force-pushed the fix-individual-eitc-reform branch from 722064f to a727a7a Compare March 13, 2026 16:12
@DTrim99 DTrim99 requested a review from PavelMakarchuk March 13, 2026 19:08
- Remove baseline EITC code from winship.py (handled by in_effect parameter)
- Fix agi_eitc_limit.yaml description format to single line

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 17, 2026

Addressed review comments:

  • winship.py: Removed baseline EITC code - the in_effect parameter handles defaulting to original behavior
  • agi_eitc_limit.yaml: Fixed description to single line format

@DTrim99 DTrim99 requested a review from PavelMakarchuk March 17, 2026 16:45
@PavelMakarchuk
Copy link
Collaborator

tests fail

The formula needs to return baseline EITC when in_effect is false,
since the reform replaces the eitc variable when loaded.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PavelMakarchuk and others added 2 commits March 17, 2026 16:43
The test loaded the reform but set in_effect: false, expecting baseline
behavior. This is not a supported use case - if baseline behavior is
desired, the reform should not be loaded.

Removes the baseline EITC logic from the reform formula per reviewer feedback.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@DTrim99 DTrim99 force-pushed the fix-individual-eitc-reform branch from bc20cc0 to 0438ad9 Compare March 17, 2026 21:22
@DTrim99 DTrim99 requested a review from PavelMakarchuk March 18, 2026 13:32
@PavelMakarchuk
Copy link
Collaborator

PR Review

Should Address

  1. Joint bonus always added regardless of filing status — The phase-out start computation always adds joint_bonus:

    phase_out_start = eitc_params.phase_out.start.calc(child_count) 
        + eitc_params.phase_out.joint_bonus.calc(child_count)

    The original branch approach inherited filing-status-dependent behavior from eitc_phase_out_start. This always adds the joint bonus even for single filers, which may overstate EITC for non-joint filers.

  2. Lost eligibility checks — The old branch approach computed EITC through standard intermediary variables that enforced investment income limits, age requirements for childless filers, etc. The new direct computation bypasses that machinery — it only gates on eitc_eligible at the top level but manually recomputes phase-in/phase-out without the additional constraints.

  3. uv.lock has a version regression — revision 3→2 and version 1.592.1→1.598.0. Looks like a stale lockfile from an older branch. Should rebase or regenerate.

  4. Test Case 5 doesn't actually test the "no limit" logic — The test sets agi_eitc_limit: 0 (meaning no limit) with 100k income, expecting 0 EITC. The comment says "Head with 100k income is past EITC phase-out, so 0." This passes because of the phase-out, not the no-limit behavior. A test with income within the EITC range and agi_eitc_limit: 0 would actually verify the no-limit logic works.


To auto-fix issues: /fix-pr 7780

DTrim99 and others added 4 commits March 18, 2026 09:44
- Apply joint bonus only to joint filers using is_joint check
- Update test case 5 to use income within EITC range (20k) to actually
  verify the no-limit behavior when agi_eitc_limit: 0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 18, 2026

Addressed Review Comments

All issues from the review have been fixed:

1. Joint bonus only applies to joint filers ✅

Added is_joint check so joint bonus is conditional on filing status:

is_joint = tax_unit("tax_unit_is_joint", period)
joint_bonus = eitc_params.phase_out.joint_bonus.calc(child_count)
phase_out_start = (
    eitc_params.phase_out.start.calc(child_count) + is_joint * joint_bonus
)

2. Eligibility checks preserved ✅

The reform uses defined_for = "eitc_eligible" which gates on eitc_eligible. This variable includes all baseline EITC eligibility checks:

  • eitc_investment_income_eligible - Investment income limit
  • eitc_demographic_eligible - Age requirements for childless filers, etc.
  • filer_meets_eitc_identification_requirements - SSN requirements
  • Separate filer limitation

3. uv.lock regression ✅

Reverted uv.lock - no longer included in PR changes.

4. Test Case 5 now properly tests no-limit logic ✅

Changed income from 100k (which was past phase-out) to 20k (within EITC range). Now the test verifies that agi_eitc_limit: 0 means no limit by expecting the same EITC as Case 3.

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.

Fix individual_eitc reform not responding to parameter changes

2 participants