Skip to content

LSR and CG behavioral responses produce nonsensical results when combined #7785

@MaxGhenis

Description

@MaxGhenis

Summary

When both labor supply responses (LSR) and capital gains (CG) behavioral responses are enabled simultaneously, the simulation produces nonsensical results — e.g., $8.6 trillion in federal tax revenue impact for a single year, compared to $6.2B (LSR only) or $21.9B (CG only).

Root cause

LSR and CG behavioral responses each create simulation branches to measure marginal tax rates, but they don't neutralize each other's responses in their measurement branches. This creates a compounding feedback loop:

The branch nesting problem

  1. LSR creates lsr_measurement and baseline_lsr_measurement branches, neutralizing employment_income_behavioral_response and self_employment_income_behavioral_response
  2. Inside those branches, relative_income_change computes household_net_income, which depends on long_term_capital_gains
  3. long_term_capital_gains = long_term_capital_gains_before_response + capital_gains_behavioral_response
  4. capital_gains_behavioral_response is NOT neutralized in the LSR branches, so it fires
  5. capital_gains_behavioral_response calls relative_capital_gains_mtr_change, which creates its OWN sub-branches (baseline_cgr_measurement, cgr_measurement)
  6. Inside those, marginal_tax_rate_on_capital_gains creates FURTHER branches (adult_1_cg_rise, adult_2_cg_rise)

The result is deeply nested branches where each behavioral response computes MTRs in a context where the other response's variables have been partially neutralized, producing incorrect MTR measurements. These wrong MTRs feed into wrong behavioral responses, which compound into the $8.6T result.

Specifically

  • LSR measurement branches see CG-adjusted household_net_income (because CG response isn't neutralized), so the income change and wage change used for LSR are wrong
  • CG response inside LSR branches computes MTRs in a context where employment income responses are neutralized, getting wrong CG MTRs
  • The responses amplify each other instead of being independent

Reproduction

from policyengine_core.reforms import Reform
from policyengine_us import Microsimulation

DATE_RANGE = "2020-01-01.2100-12-31"

# Any reform that changes taxes
reform = Reform.from_dict({
    "gov.contrib.congress.watca.in_effect": {DATE_RANGE: True},
    "gov.contrib.congress.watca.surtax.in_effect": {DATE_RANGE: True},
}, country_id="us")

# Enable both LSR and CG responses
lsr = Reform.from_dict({
    "gov.simulation.labor_supply_responses.elasticities.income": {DATE_RANGE: -0.05},
    "gov.simulation.labor_supply_responses.elasticities.substitution.all": {DATE_RANGE: 0.25},
}, country_id="us")

cg = Reform.from_dict({
    "gov.simulation.capital_gains_responses.elasticity": {DATE_RANGE: -0.62},
}, country_id="us")

sim_baseline = Microsimulation()
sim_reform = Microsimulation(reform=(reform, lsr, cg))

# This produces ~$8.6T instead of a reasonable ~$15-25B
tax_baseline = sim_baseline.calculate("income_tax", period=2026, map_to="household")
tax_reform = sim_reform.calculate("income_tax", period=2026, map_to="household")
print(f"Impact: ${float((tax_reform - tax_baseline).sum()) / 1e9:.1f}B")

Individual responses work correctly:

  • LSR only: $6.2B (reasonable)
  • CG only: $21.9B (reasonable)
  • LSR + CG: $8,651.3B (broken)

Proposed fix

Each behavioral response's measurement branches should neutralize ALL other behavioral responses, not just their own. Specifically:

  1. In labor_supply_behavioral_response.formula, add capital_gains_behavioral_response to the list of neutralized variables in the measurement branches
  2. In relative_capital_gains_mtr_change.formula, add employment_income_behavioral_response and self_employment_income_behavioral_response to the list of neutralized variables in the measurement branches

This makes the responses additive (each measured against the static baseline) rather than compounding.

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions