Repository navigation
Elasticity-based labour supply responses compare a reform with a copy of itself #2268
Description
What happens
The elasticity-based labour supply response variables compare a reform with a copy of itself, so they return zero for a reform that changes marginal tax rates.
Reproduction
On main (2.125.2, policyengine-core 3.32.27):
import numpy as np
from policyengine_uk import Simulation
situation = {
"people": {"person": {"age": {2025: 30}, "employment_income": {2025: 40_000}, "hours_worked": {2025: 1800}}},
"benunits": {"benunit": {"members": ["person"]}},
"households": {"household": {"members": ["person"]}},
}
reform = {
"gov.hmrc.income_tax.rates.uk[0].rate": {"2025": 0.30},
"gov.simulation.labour_supply_responses.substitution_elasticity": {"2025": 0.25},
"gov.simulation.labour_supply_responses.income_elasticity": {"2025": -0.05},
}
sim = Simulation(situation=situation, reform=reform)
rate = lambda s: float(s.tax_benefit_system.parameters("2025").gov.hmrc.income_tax.rates.uk.rates[0])
print(rate(sim), rate(sim.baseline), rate(sim.get_branch("baseline"))) # 0.3 0.2 0.3
print("baseline" in sim.branches, sim.get_branch("baseline") is sim.baseline) # False False
sim = Simulation(situation=situation, reform=reform)
print(sim.calculate("income_tax", 2025), sim.baseline.calculate("income_tax", 2025)) # [8229.] [5486.]
for variable in ("relative_wage_change", "relative_income_change", "employment_income_behavioral_response"):
print(variable, sim.calculate(variable, 2025)) # [0.] eachIncome tax rises from £5,486 to £8,229 and the basic rate from 20% to 30%, yet the relative wage change, the relative income change and the response are all zero.
Cause, read in the code
relative_wage_change, relative_income_change and employment_income_behavioral_response (policyengine_uk/variables/gov/simulation/labour_supply_response/) take the baseline from simulation.get_branch("baseline"); the comment says it is "Already created by default". That is policyengine-core's convention, where a reform simulation registers its baseline as the branch baseline. policyengine_uk.Simulation.__init__ does not: it keeps the baseline as a separate simulation in simulation.baseline and leaves simulation.branches empty. So get_branch("baseline") makes a new branch, which is a clone of the reform simulation with the reform's parameters.
policyengine_uk/dynamics/ (the OBR labour supply responses behind apply_dynamics) reads sim.baseline directly and is not affected.
Suggested fix
Take the baseline from simulation.baseline (for example simulation.baseline.get_branch("baseline_lsr_measurement", ...)), and add tests with a reform that changes a marginal rate and expects a non-zero response of the right sign. No test covers a non-zero case today. State the change in aggregates for reforms that set these elasticities.
Activity