policyengine-uk 2.40.1__py3-none-any.whl → 2.65.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. policyengine_uk/__init__.py +5 -3
  2. policyengine_uk/data/__init__.py +1 -0
  3. policyengine_uk/data/dataset_schema.py +70 -18
  4. policyengine_uk/data/economic_assumptions.py +36 -10
  5. policyengine_uk/data/filter_dataset.py +52 -0
  6. policyengine_uk/dynamics/labour_supply.py +343 -0
  7. policyengine_uk/dynamics/participation.py +629 -0
  8. policyengine_uk/dynamics/progression.py +384 -0
  9. policyengine_uk/microsimulation.py +105 -0
  10. policyengine_uk/model_api.py +1 -0
  11. policyengine_uk/parameters/gov/boe/base_rate.yaml +34 -0
  12. policyengine_uk/parameters/gov/boe/index.yaml +2 -0
  13. policyengine_uk/parameters/gov/contrib/behavioral_responses/employee_salary_sacrifice_reduction_rate.yaml +14 -0
  14. policyengine_uk/parameters/gov/contrib/behavioral_responses/salary_sacrifice_broad_base_haircut_rate.yaml +22 -0
  15. policyengine_uk/parameters/gov/contrib/cec/state_pension_increase.yaml +1 -1
  16. policyengine_uk/parameters/gov/contrib/ubi_center/carbon_tax.yaml +2 -2
  17. policyengine_uk/parameters/gov/contrib/ubi_center/land_value_tax.yaml +3 -3
  18. policyengine_uk/parameters/gov/dcms/bbc/tv_licence/colour.yaml +5 -5
  19. policyengine_uk/parameters/gov/dfe/education_spending.yaml +1 -1
  20. policyengine_uk/parameters/gov/dft/rail/fare_index.yaml +32 -0
  21. policyengine_uk/parameters/gov/dft/rail/prior_law_fare_index.yaml +32 -0
  22. policyengine_uk/parameters/gov/dft/rail/ridership_index.yaml +30 -0
  23. policyengine_uk/parameters/gov/dft/spending.yaml +2 -2
  24. policyengine_uk/parameters/gov/dwp/ESA/income/earn_disregard.yaml +1 -1
  25. policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_couple.yaml +1 -1
  26. policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_lone_parent.yaml +1 -1
  27. policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_single.yaml +1 -1
  28. policyengine_uk/parameters/gov/dwp/ESA/income/pension_disregard.yaml +1 -1
  29. policyengine_uk/parameters/gov/dwp/IIDB/maximum.yaml +1 -1
  30. policyengine_uk/parameters/gov/dwp/JSA/contrib/amount_over_25.yaml +1 -1
  31. policyengine_uk/parameters/gov/dwp/JSA/contrib/earn_disregard.yaml +1 -1
  32. policyengine_uk/parameters/gov/dwp/JSA/contrib/pension_disregard.yaml +1 -1
  33. policyengine_uk/parameters/gov/dwp/JSA/income/amount_18_24.yaml +7 -7
  34. policyengine_uk/parameters/gov/dwp/JSA/income/amount_over_25.yaml +7 -7
  35. policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_couple.yaml +1 -1
  36. policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_lone_parent.yaml +1 -1
  37. policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_single.yaml +1 -1
  38. policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_age_threshold.yaml +12 -0
  39. policyengine_uk/parameters/gov/dwp/attendance_allowance/higher.yaml +7 -7
  40. policyengine_uk/parameters/gov/dwp/attendance_allowance/lower.yaml +7 -7
  41. policyengine_uk/parameters/gov/dwp/benefit_cap.yaml +3 -3
  42. policyengine_uk/parameters/gov/dwp/carer_premium/couple.yaml +2 -2
  43. policyengine_uk/parameters/gov/dwp/carer_premium/single.yaml +6 -6
  44. policyengine_uk/parameters/gov/dwp/carers_allowance/rate.yaml +7 -7
  45. policyengine_uk/parameters/gov/dwp/disability_premia/disability_couple.yaml +1 -1
  46. policyengine_uk/parameters/gov/dwp/disability_premia/enhanced_couple.yaml +1 -1
  47. policyengine_uk/parameters/gov/dwp/disability_premia/enhanced_single.yaml +1 -1
  48. policyengine_uk/parameters/gov/dwp/disability_premia/severe_couple.yaml +1 -1
  49. policyengine_uk/parameters/gov/dwp/dla/mobility/higher.yaml +4 -4
  50. policyengine_uk/parameters/gov/dwp/dla/mobility/lower.yaml +8 -8
  51. policyengine_uk/parameters/gov/dwp/dla/self_care/higher.yaml +7 -7
  52. policyengine_uk/parameters/gov/dwp/dla/self_care/lower.yaml +8 -8
  53. policyengine_uk/parameters/gov/dwp/dla/self_care/middle.yaml +7 -7
  54. policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged.yaml +1 -1
  55. policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/older.yaml +3 -3
  56. policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged.yaml +1 -1
  57. policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/older.yaml +3 -3
  58. policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/worker.yaml +1 -1
  59. policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/amount.yaml +1 -1
  60. policyengine_uk/parameters/gov/dwp/housing_benefit/takeup.yaml +7 -7
  61. policyengine_uk/parameters/gov/dwp/income_support/amounts/amount_16_24.yaml +1 -1
  62. policyengine_uk/parameters/gov/dwp/income_support/amounts/amount_couples_over_18.yaml +1 -1
  63. policyengine_uk/parameters/gov/dwp/income_support/means_test/earn_disregard.yaml +1 -1
  64. policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_couple.yaml +1 -1
  65. policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_lone_parent.yaml +1 -1
  66. policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_single.yaml +1 -1
  67. policyengine_uk/parameters/gov/dwp/income_support/means_test/pension_disregard.yaml +1 -1
  68. policyengine_uk/parameters/gov/dwp/income_support/takeup.yaml +7 -7
  69. policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/carer/addition.yaml +4 -4
  70. policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee.yaml +9 -9
  71. policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/severe_disability/addition.yaml +3 -3
  72. policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/threshold.yaml +5 -5
  73. policyengine_uk/parameters/gov/dwp/pip/daily_living/enhanced.yaml +7 -7
  74. policyengine_uk/parameters/gov/dwp/pip/daily_living/standard.yaml +8 -8
  75. policyengine_uk/parameters/gov/dwp/pip/mobility/enhanced.yaml +4 -4
  76. policyengine_uk/parameters/gov/dwp/pip/mobility/standard.yaml +9 -9
  77. policyengine_uk/parameters/gov/dwp/sda/maximum.yaml +7 -7
  78. policyengine_uk/parameters/gov/dwp/state_pension/basic_state_pension/amount.yaml +11 -11
  79. policyengine_uk/parameters/gov/dwp/state_pension/new_state_pension/amount.yaml +4 -4
  80. policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/limit/child_count.yaml +10 -1
  81. policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/takeup.yaml +7 -7
  82. policyengine_uk/parameters/gov/dwp/tax_credits/working_tax_credit/takeup.yaml +7 -7
  83. policyengine_uk/parameters/gov/dwp/universal_credit/elements/carer/amount.yaml +3 -3
  84. policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/amount.yaml +2 -4
  85. policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/disabled/amount.yaml +2 -4
  86. policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/first/higher_amount.yaml +6 -8
  87. policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/child_count.yaml +6 -1
  88. policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/severely_disabled/amount.yaml +3 -5
  89. policyengine_uk/parameters/gov/dwp/universal_credit/elements/childcare/cap.yaml +2 -6
  90. policyengine_uk/parameters/gov/dwp/universal_credit/elements/disabled/amount.yaml +4 -6
  91. policyengine_uk/parameters/gov/dwp/universal_credit/elements/housing/non_dep_deduction/amount.yaml +4 -1
  92. policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/active.yaml +9 -0
  93. policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/new_claimant_health_element.yaml +9 -0
  94. policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/standard_allowance_uplift.yaml +13 -0
  95. policyengine_uk/parameters/gov/dwp/universal_credit/standard_allowance/amount.yaml +5 -5
  96. policyengine_uk/parameters/gov/dwp/winter_fuel_payment/eligibility/taxable_income_test/maximum_taxable_income.yaml +2 -1
  97. policyengine_uk/parameters/gov/dwp/winter_fuel_payment/eligibility/taxable_income_test/use_maximum_taxable_income.yaml +1 -0
  98. policyengine_uk/parameters/gov/dynamic/obr_labour_supply_assumptions.yaml +9 -0
  99. policyengine_uk/parameters/gov/economic_assumptions/create_economic_assumption_indices.py +1 -1
  100. policyengine_uk/parameters/gov/economic_assumptions/yoy_growth.yaml +522 -153
  101. policyengine_uk/parameters/gov/hmrc/cgt/additional_rate.yaml +5 -0
  102. policyengine_uk/parameters/gov/hmrc/cgt/basic_rate.yaml +5 -0
  103. policyengine_uk/parameters/gov/hmrc/cgt/higher_rate.yaml +4 -0
  104. policyengine_uk/parameters/gov/hmrc/child_benefit/amount/additional.yaml +6 -6
  105. policyengine_uk/parameters/gov/hmrc/child_benefit/amount/eldest.yaml +8 -8
  106. policyengine_uk/parameters/gov/hmrc/child_benefit/takeup/by_age.yaml +1 -1
  107. policyengine_uk/parameters/gov/hmrc/fuel_duty/calculate_fuel_duty_rates.py +464 -0
  108. policyengine_uk/parameters/gov/hmrc/fuel_duty/petrol_and_diesel.yaml +86 -10
  109. policyengine_uk/parameters/gov/hmrc/income_tax/allowances/personal_allowance/amount.yaml +6 -0
  110. policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml +2 -1
  111. policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_additions.yaml +1 -0
  112. policyengine_uk/parameters/gov/hmrc/income_tax/rates/dividends.yaml +12 -0
  113. policyengine_uk/parameters/gov/hmrc/income_tax/rates/property.yaml +46 -0
  114. policyengine_uk/parameters/gov/hmrc/income_tax/rates/savings.yaml +46 -0
  115. policyengine_uk/parameters/gov/hmrc/income_tax/rates/scotland/rates.yaml +2 -2
  116. policyengine_uk/parameters/gov/hmrc/income_tax/rates/uk.yaml +14 -2
  117. policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/additional.yaml +4 -6
  118. policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml +3 -3
  119. policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml +14 -4
  120. policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/flat_rate.yaml +2 -2
  121. policyengine_uk/parameters/gov/hmrc/national_insurance/salary_sacrifice_pension_cap.yaml +16 -0
  122. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/index.yaml +12 -0
  123. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_1/boe_margin.yaml +11 -0
  124. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/additional_rate.yaml +27 -0
  125. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/index.yaml +16 -0
  126. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/upper_threshold.yaml +48 -0
  127. policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/postgraduate_additional_rate.yaml +11 -0
  128. policyengine_uk/parameters/gov/hmrc/student_loans/postgraduate_repayment_rate.yaml +9 -0
  129. policyengine_uk/parameters/gov/hmrc/student_loans/repayment_rate.yaml +9 -0
  130. policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_1.yaml +25 -0
  131. policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_2.yaml +58 -0
  132. policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_4.yaml +19 -0
  133. policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_5.yaml +16 -0
  134. policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/postgraduate.yaml +21 -0
  135. policyengine_uk/parameters/gov/hmrc/vat/reduced_rate_share.yaml +3 -3
  136. policyengine_uk/parameters/gov/indices/private_rent_index.yaml +9 -9
  137. policyengine_uk/parameters/gov/revenue_scotland/lbtt/non_residential.yaml +2 -2
  138. policyengine_uk/parameters/gov/revenue_scotland/lbtt/rent.yaml +2 -2
  139. policyengine_uk/parameters/gov/revenue_scotland/lbtt/residential/first_time_buyer_rate.yaml +2 -2
  140. policyengine_uk/parameters/gov/revenue_scotland/lbtt/residential/rate.yaml +2 -2
  141. policyengine_uk/parameters/gov/wra/land_transaction_tax/non_residential.yaml +2 -2
  142. policyengine_uk/parameters/gov/wra/land_transaction_tax/rent.yaml +2 -2
  143. policyengine_uk/parameters/gov/wra/land_transaction_tax/residential/higher_rate.yaml +1 -1
  144. policyengine_uk/parameters/gov/wra/land_transaction_tax/residential/primary.yaml +2 -2
  145. policyengine_uk/parameters/household/consumption/carbon/consumption.yaml +8 -6
  146. policyengine_uk/parameters/household/consumption/carbon/intensity.yaml +4 -1
  147. policyengine_uk/parameters/household/consumption/carbon/production.yaml +12 -7
  148. policyengine_uk/parameters/household/consumption/carbon/production_by_source.yaml +76 -41
  149. policyengine_uk/parameters/household/consumption/fuel/prices/petrol.yaml +1 -1
  150. policyengine_uk/parameters/household/poverty/absolute_poverty_threshold_bhc.yaml +1 -1
  151. policyengine_uk/reforms/policyengine/adjust_budgets.py +0 -1
  152. policyengine_uk/scenarios/__init__.py +4 -0
  153. policyengine_uk/scenarios/pip_reform.py +23 -0
  154. policyengine_uk/scenarios/reindex_benefit_cap.py +32 -0
  155. policyengine_uk/scenarios/repeal_two_child_limit.py +10 -0
  156. policyengine_uk/scenarios/uc_reform.py +50 -0
  157. policyengine_uk/simulation.py +619 -0
  158. policyengine_uk/system.py +3 -257
  159. policyengine_uk/tax_benefit_system.py +141 -0
  160. policyengine_uk/tests/behavioral_responses/test_labor_supply_responses.yaml +183 -0
  161. policyengine_uk/tests/microsimulation/reforms_config.yaml +8 -8
  162. policyengine_uk/tests/microsimulation/test_reform_impacts.py +2 -2
  163. policyengine_uk/tests/microsimulation/test_salary_sacrifice_cap_reform.py +401 -0
  164. policyengine_uk/tests/microsimulation/test_validity.py +2 -3
  165. policyengine_uk/tests/microsimulation/update_reform_impacts.py +104 -40
  166. policyengine_uk/tests/policy/baseline/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.yaml +105 -0
  167. policyengine_uk/tests/policy/baseline/finance/benefit/family/child_benefit.yaml +2 -0
  168. policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support.yaml +0 -23
  169. policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/tv_licence.yaml +3 -0
  170. policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml +17 -17
  171. policyengine_uk/tests/policy/baseline/gov/dwp/basic_state_pension.yaml +44 -0
  172. policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/gift_aid.yaml +71 -0
  173. policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/personal_allowance.yaml +161 -0
  174. policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.yaml +107 -0
  175. policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.yaml +95 -0
  176. policyengine_uk/tests/policy/baseline/gov/hmrc/student_loans/student_loan_interest_rate.yaml +153 -0
  177. policyengine_uk/tests/policy/baseline/gov/hmrc/student_loans/student_loan_repayment.yaml +130 -0
  178. policyengine_uk/tests/policy/baseline/household/wealth/vehicle.yaml +27 -0
  179. policyengine_uk/tests/policy/reforms/nov_2025_budget/income_source_tax_rates.yaml +235 -0
  180. policyengine_uk/tests/policy/reforms/nov_2025_budget/income_tax_freeze.yaml +83 -0
  181. policyengine_uk/tests/policy/reforms/parametric/basic_income/basic_income.yaml +1 -0
  182. policyengine_uk/tests/test_behavioral_responses.py +215 -0
  183. policyengine_uk/tests/test_fiscal_year_parameters.py +131 -0
  184. policyengine_uk/utils/__init__.py +1 -0
  185. policyengine_uk/utils/compare.py +28 -0
  186. policyengine_uk/utils/create_ahc_deflator.py +169 -0
  187. policyengine_uk/utils/create_triple_lock.py +1 -1
  188. policyengine_uk/utils/dependencies.py +259 -0
  189. policyengine_uk/utils/parameters.py +12 -1
  190. policyengine_uk/utils/scenario.py +225 -0
  191. policyengine_uk/utils/solve_private_school_attendance_factor.py +4 -6
  192. policyengine_uk/variables/contrib/policyengine/education_budget_change.py +0 -1
  193. policyengine_uk/variables/contrib/policyengine/employer_ni/baseline_employer_cost.py +5 -1
  194. policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.py +23 -23
  195. policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_capital_incidence.py +1 -1
  196. policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_consumer_incidence.py +1 -1
  197. policyengine_uk/variables/contrib/policyengine/other_public_spending_budget_change.py +0 -1
  198. policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py +0 -1
  199. policyengine_uk/variables/gov/dft/rail_subsidy_spending.py +16 -1
  200. policyengine_uk/variables/gov/dft/rail_usage.py +16 -0
  201. policyengine_uk/variables/gov/dwp/BRMA_LHA_rate.py +7 -2
  202. policyengine_uk/variables/gov/dwp/LHA_category.py +4 -2
  203. policyengine_uk/variables/gov/dwp/additional_state_pension.py +4 -2
  204. policyengine_uk/variables/gov/dwp/basic_state_pension.py +26 -8
  205. policyengine_uk/variables/gov/dwp/is_CTC_eligible.py +1 -1
  206. policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py +9 -13
  207. policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +66 -0
  208. policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +75 -0
  209. policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py +66 -0
  210. policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py +5 -4
  211. policyengine_uk/variables/gov/gov_tax.py +0 -2
  212. policyengine_uk/variables/gov/hmrc/household_tax.py +0 -1
  213. policyengine_uk/variables/gov/hmrc/income_tax/allowances/gift_aid.py +23 -0
  214. policyengine_uk/variables/gov/hmrc/income_tax/allowances/personal_allowance.py +9 -2
  215. policyengine_uk/variables/gov/hmrc/income_tax/income_tax_pre_charges.py +1 -0
  216. policyengine_uk/variables/gov/hmrc/income_tax/liability/property_income_tax.py +75 -0
  217. policyengine_uk/variables/gov/hmrc/income_tax/liability/savings_income_tax.py +4 -4
  218. policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_broad_base_haircut.py +43 -0
  219. policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.py +38 -0
  220. policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.py +27 -0
  221. policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_adjusted.py +31 -0
  222. policyengine_uk/variables/gov/hmrc/pensions/salary_sacrifice_returned_to_income.py +41 -0
  223. policyengine_uk/variables/gov/hmrc/student_loans/__init__.py +2 -0
  224. policyengine_uk/variables/gov/hmrc/student_loans/plan_1_interest_rate.py +23 -0
  225. policyengine_uk/variables/gov/hmrc/student_loans/plan_2_interest_rate.py +31 -0
  226. policyengine_uk/variables/gov/hmrc/student_loans/plan_4_interest_rate.py +22 -0
  227. policyengine_uk/variables/gov/hmrc/student_loans/plan_5_interest_rate.py +18 -0
  228. policyengine_uk/variables/gov/hmrc/student_loans/postgraduate_interest_rate.py +23 -0
  229. policyengine_uk/variables/gov/hmrc/student_loans/student_loan_plan.py +27 -0
  230. policyengine_uk/variables/gov/hmrc/student_loans/student_loan_repayment.py +91 -0
  231. policyengine_uk/variables/gov/hmrc/student_loans/student_loan_repayment_rate.py +31 -0
  232. policyengine_uk/variables/gov/hmrc/would_claim_child_benefit.py +5 -1
  233. policyengine_uk/variables/household/demographic/benunit/benunit_count_adults.py +11 -0
  234. policyengine_uk/variables/household/demographic/is_disabled_for_benefits.py +13 -1
  235. policyengine_uk/variables/household/income/hbai_household_net_income.py +29 -1
  236. policyengine_uk/variables/household/income/hbai_household_net_income_ahc.py +13 -0
  237. policyengine_uk/variables/household/income/household_net_income.py +5 -1
  238. policyengine_uk/variables/household/income/inflation_adjustment.py +24 -0
  239. policyengine_uk/variables/household/post_tax_income.py +12 -0
  240. policyengine_uk/variables/household/wealth/num_vehicles.py +9 -0
  241. policyengine_uk/variables/household/wealth/owns_vehicle.py +17 -0
  242. policyengine_uk/variables/input/consumption/property/council_tax.py +0 -35
  243. policyengine_uk/variables/input/consumption/property/employee_pension_contributions.py +8 -1
  244. policyengine_uk/variables/input/consumption/property/employee_pension_contributions_reported.py +16 -0
  245. policyengine_uk/variables/input/consumption/property/pension_contributions_via_salary_sacrifice.py +16 -0
  246. policyengine_uk/variables/input/employment_income.py +2 -0
  247. policyengine_uk/variables/input/rent.py +0 -40
  248. policyengine_uk/variables/input/savings_interest_income.py +3 -1
  249. {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/METADATA +17 -8
  250. {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/RECORD +252 -173
  251. {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/WHEEL +1 -1
  252. policyengine_uk/repo.py +0 -3
  253. policyengine_uk/tests/policy/baseline/gov/abolitions/abolition_parameters.yaml +0 -250
  254. policyengine_uk/variables/contrib/policyengine/high_income_incident_tax_change.py +0 -22
  255. policyengine_uk-2.40.1.data/data/share/openfisca/openfisca-country-template/CHANGELOG.md +0 -2285
  256. policyengine_uk-2.40.1.data/data/share/openfisca/openfisca-country-template/README.md +0 -37
  257. policyengine_uk-2.40.1.dist-info/licenses/LICENSE +0 -661
  258. {policyengine_uk-2.40.1.data/data/share/openfisca/openfisca-country-template → policyengine_uk-2.65.6.dist-info/licenses}/LICENSE +0 -0
@@ -1,6 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  import os
4
+ from pathlib import Path
5
+
6
+ from policyengine_core.taxbenefitsystems import TaxBenefitSystem
7
+
4
8
  from policyengine_uk import entities
5
9
  from policyengine_uk.system import (
6
10
  CountryTaxBenefitSystem,
@@ -10,8 +14,6 @@ from policyengine_uk.system import (
10
14
  parameters,
11
15
  variables,
12
16
  )
13
- from pathlib import Path
14
- import os
15
- from policyengine_core.taxbenefitsystems import TaxBenefitSystem
17
+ from .model_api import *
16
18
 
17
19
  REPO = Path(__file__).parent
@@ -2,3 +2,4 @@ from policyengine_uk.data.dataset_schema import (
2
2
  UKMultiYearDataset,
3
3
  UKSingleYearDataset,
4
4
  )
5
+ from .filter_dataset import filter_dataset
@@ -14,11 +14,17 @@ class UKSingleYearDataset:
14
14
  household: pd.DataFrame
15
15
 
16
16
  @staticmethod
17
- def validate_file_path(file_path: str):
17
+ def validate_file_path(file_path: str, raise_exception: bool = True):
18
18
  if not file_path.endswith(".h5"):
19
- raise ValueError("File path must end with '.h5' for UKDataset.")
19
+ if raise_exception:
20
+ raise ValueError(
21
+ "File path must end with '.h5' for UKDataset."
22
+ )
23
+ return False
20
24
  if not Path(file_path).exists():
21
- raise FileNotFoundError(f"File not found: {file_path}")
25
+ if raise_exception:
26
+ raise FileNotFoundError(f"File not found: {file_path}")
27
+ return False
22
28
 
23
29
  # Check if the file contains time_period, person, benunit, and household datasets
24
30
  with h5py.File(file_path, "r") as f:
@@ -30,9 +36,14 @@ class UKSingleYearDataset:
30
36
  ]
31
37
  for dataset in required_datasets:
32
38
  if dataset not in f:
33
- raise ValueError(
34
- f"Dataset '{dataset}' not found in the file: {file_path}"
35
- )
39
+ if raise_exception:
40
+ raise ValueError(
41
+ f"Dataset '{dataset}' not found in the file: {file_path}"
42
+ )
43
+ else:
44
+ return False
45
+
46
+ return True
36
47
 
37
48
  def __init__(
38
49
  self,
@@ -42,6 +53,7 @@ class UKSingleYearDataset:
42
53
  household: pd.DataFrame = None,
43
54
  fiscal_year: int = 2025,
44
55
  ):
56
+ file_path = str(file_path) if file_path else None
45
57
  if file_path is not None:
46
58
  self.validate_file_path(file_path)
47
59
  with pd.HDFStore(file_path) as f:
@@ -108,9 +120,12 @@ class UKSingleYearDataset:
108
120
  if simulation.tax_benefit_system.variables[variable].entity.key
109
121
  == entity
110
122
  ]
111
- entity_dfs[entity] = simulation.calculate_dataframe(
112
- input_variables, period=fiscal_year
113
- )
123
+ if len(input_variables) == 0:
124
+ entity_dfs[entity] = pd.DataFrame()
125
+ else:
126
+ entity_dfs[entity] = simulation.calculate_dataframe(
127
+ input_variables, period=fiscal_year
128
+ )
114
129
 
115
130
  return UKSingleYearDataset(
116
131
  person=entity_dfs["person"],
@@ -137,7 +152,7 @@ class UKMultiYearDataset:
137
152
  self.datasets[year] = dataset
138
153
 
139
154
  if file_path is not None:
140
- UKSingleYearDataset.validate_file_path(file_path)
155
+ UKMultiYearDataset.validate_file_path(file_path)
141
156
  with pd.HDFStore(file_path) as f:
142
157
  self.datasets = {}
143
158
  for year in f.keys():
@@ -162,6 +177,10 @@ class UKMultiYearDataset:
162
177
  else:
163
178
  raise ValueError(f"No dataset found for year {fiscal_year}.")
164
179
 
180
+ @property
181
+ def years(self):
182
+ return list(self.datasets.keys())
183
+
165
184
  def __getitem__(self, fiscal_year: int):
166
185
  return self.get_year(fiscal_year)
167
186
 
@@ -203,21 +222,49 @@ class UKMultiYearDataset:
203
222
  return UKMultiYearDataset(datasets=list(new_datasets.values()))
204
223
 
205
224
  @staticmethod
206
- def validate_file_path(file_path: str):
225
+ def validate_file_path(file_path: str, raise_exception: bool = False):
207
226
  if not file_path.endswith(".h5"):
208
- raise ValueError(
209
- "File path must end with '.h5' for UKMultiYearDataset."
210
- )
227
+ if raise_exception:
228
+ raise ValueError(
229
+ "File path must end with '.h5' for UKMultiYearDataset."
230
+ )
231
+ else:
232
+ return False
211
233
  if not Path(file_path).exists():
212
- raise FileNotFoundError(f"File not found: {file_path}")
234
+ if raise_exception:
235
+ raise FileNotFoundError(f"File not found: {file_path}")
236
+ else:
237
+ return False
213
238
 
214
239
  # Check if the file contains datasets for multiple years
215
240
  with h5py.File(file_path, "r") as f:
216
241
  for required_dataset in ["person", "benunit", "household"]:
217
242
  if not any(f"{required_dataset}" in key for key in f.keys()):
218
- raise ValueError(
219
- f"Dataset '{required_dataset}' not found in the file: {file_path}"
220
- )
243
+ if raise_exception:
244
+ raise ValueError(
245
+ f"Dataset '{required_dataset}' not found in the file: {file_path}"
246
+ )
247
+ else:
248
+ return False
249
+
250
+ # Check that there is at least one dataset year in the folder (keys include e.g. person/2025)
251
+
252
+ # Check that there is at least one dataset year in the folder
253
+ years_found = False
254
+ for key in f.keys():
255
+ parts = key.split("/")
256
+ if len(parts) >= 2 and required_dataset == parts[0]:
257
+ years_found = True
258
+ break
259
+
260
+ if not years_found:
261
+ if raise_exception:
262
+ raise ValueError(
263
+ f"No yearly data found for '{required_dataset}' in file: {file_path}"
264
+ )
265
+ else:
266
+ return False
267
+ return True
221
268
 
222
269
  def load(self):
223
270
  data = {}
@@ -228,3 +275,8 @@ class UKMultiYearDataset:
228
275
  data[col] = {}
229
276
  data[col][year] = df[col].values
230
277
  return data
278
+
279
+ def reset_uprating(self):
280
+ from policyengine_uk.data.economic_assumptions import reset_uprating
281
+
282
+ reset_uprating(self)
@@ -1,14 +1,37 @@
1
1
  from policyengine_uk.data import UKMultiYearDataset, UKSingleYearDataset
2
- from policyengine_uk.system import system
2
+
3
3
  import yaml
4
4
  from policyengine_core.parameters import ParameterNode
5
5
  from pathlib import Path
6
6
  import numpy as np
7
+ import logging
8
+
9
+
10
+ def extend_single_year_dataset(
11
+ dataset: UKSingleYearDataset,
12
+ tax_benefit_system_parameters: ParameterNode,
13
+ end_year: int = 2030,
14
+ ) -> UKMultiYearDataset:
15
+ # Extend years and uprate
16
+ start_year = int(dataset.time_period)
17
+ datasets = [dataset]
18
+ for year in range(start_year, end_year + 1):
19
+ next_year = dataset.copy()
20
+ next_year.time_period = str(year)
21
+ datasets.append(next_year)
22
+ multi_year_dataset = UKMultiYearDataset(datasets=datasets)
23
+ return apply_uprating(
24
+ multi_year_dataset,
25
+ tax_benefit_system_parameters=tax_benefit_system_parameters,
26
+ )
7
27
 
8
28
 
9
29
  def apply_uprating(
10
30
  dataset: UKMultiYearDataset,
31
+ tax_benefit_system_parameters: ParameterNode = None,
11
32
  ):
33
+ from policyengine_uk.system import system
34
+
12
35
  # Apply uprating to the dataset.
13
36
  dataset = dataset.copy()
14
37
 
@@ -20,7 +43,9 @@ def apply_uprating(
20
43
  continue # Don't uprate the first year
21
44
  current_year = dataset.datasets[year]
22
45
  prev_year = dataset.datasets[year - 1]
23
- apply_single_year_uprating(current_year, prev_year, system.parameters)
46
+ apply_single_year_uprating(
47
+ current_year, prev_year, tax_benefit_system_parameters
48
+ )
24
49
 
25
50
  return dataset
26
51
 
@@ -48,7 +73,9 @@ def apply_single_year_uprating(
48
73
  prev_year_value = getattr(previous_year, table_name)[
49
74
  variable
50
75
  ]
51
- current_year_value = prev_year_value * index_rel_change
76
+ current_year_value = prev_year_value * (
77
+ 1 + index_rel_change
78
+ )
52
79
  getattr(current_year, table_name)[
53
80
  variable
54
81
  ] = current_year_value
@@ -128,9 +155,10 @@ def uprate_rent(
128
155
  social_rent_growth = growth.obr.social_rent(year)
129
156
 
130
157
  if year < 2022:
131
- raise ValueError(
132
- "Rent uprating is not supported for years before 2022."
158
+ logging.warning(
159
+ "Rent uprating is not supported for years before 2022. Not applying uprating."
133
160
  )
161
+ pass
134
162
  elif year < 2025:
135
163
  # We have regional growth rates for private rent.
136
164
  regional_growth_rate = growth.ons.private_rental_prices(year)[
@@ -158,11 +186,6 @@ def uprate_rent(
158
186
  private_rent_growth = (
159
187
  aggregate_growth - social_weight * social_rent_growth
160
188
  ) / private_weight
161
- print(
162
- f"Backed out private rent growth: {private_rent_growth:.1%} in {year}"
163
- )
164
- print(f"OBR aggregate rent growth: {aggregate_growth:.1%} in {year}")
165
- print(f"Social rent growth: {social_rent_growth:.1%} in {year}")
166
189
 
167
190
  current_year.household["rent"] = np.where(
168
191
  is_private_rented,
@@ -182,3 +205,6 @@ def reset_uprating(
182
205
  for year in dataset.datasets:
183
206
  if year != first_year:
184
207
  dataset.datasets[year] = dataset.datasets[first_year].copy()
208
+ dataset.datasets[year].time_period = str(year)
209
+
210
+ return dataset
@@ -0,0 +1,52 @@
1
+ import typing
2
+
3
+ if typing.TYPE_CHECKING:
4
+ from policyengine_uk import Microsimulation
5
+ from policyengine_uk.data import UKSingleYearDataset
6
+
7
+
8
+ def filter_dataset(
9
+ sim: "Microsimulation", household_id: int, year: int = 2026
10
+ ) -> "UKSingleYearDataset":
11
+ from policyengine_uk import Microsimulation
12
+ from policyengine_uk.data import UKSingleYearDataset
13
+
14
+ """
15
+ Extract a single household from a simulation dataset.
16
+
17
+ This function creates a new dataset containing only the specified household
18
+ and the associated benefit units and people within that household.
19
+
20
+ Parameters
21
+ ----------
22
+ sim : Microsimulation
23
+ The microsimulation object containing the dataset.
24
+ household_id : int
25
+ The ID of the household to extract.
26
+ year : int, default 2026
27
+ The dataset year to filter.
28
+
29
+ Returns
30
+ -------
31
+ UKSingleYearDataset
32
+ A new dataset containing only data for the specified household.
33
+ """
34
+ dataset: UKSingleYearDataset = sim.dataset[year]
35
+ new_dataset = dataset.copy()
36
+ new_dataset.person = new_dataset.person[
37
+ new_dataset.person.person_household_id == household_id
38
+ ]
39
+ new_dataset.household = new_dataset.household[
40
+ new_dataset.household.household_id == household_id
41
+ ]
42
+ benunits = new_dataset.person.person_benunit_id.unique()
43
+ new_dataset.benunit = new_dataset.benunit[
44
+ new_dataset.benunit.benunit_id.isin(benunits)
45
+ ]
46
+
47
+ return UKSingleYearDataset(
48
+ person=new_dataset.person,
49
+ household=new_dataset.household,
50
+ benunit=new_dataset.benunit,
51
+ fiscal_year=year,
52
+ )
@@ -0,0 +1,343 @@
1
+ """Labour supply dynamics module for PolicyEngine UK.
2
+
3
+ This module coordinates labour supply responses by combining progression (intensive margin)
4
+ and participation (extensive margin) models. It implements the methodology from the OBR's
5
+ labour supply elasticity framework to estimate how changes in tax and benefit policies
6
+ affect employment and working hours.
7
+
8
+ Reference: https://obr.uk/docs/dlm_uploads/NICS-Cut-Impact-on-Labour-Supply-Note.pdf
9
+ """
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+ from policyengine_uk import Simulation
14
+ from microdf import MicroDataFrame
15
+ from pydantic import BaseModel
16
+ from typing import Optional
17
+
18
+ from .progression import (
19
+ calculate_derivative_change,
20
+ calculate_relative_income_change,
21
+ calculate_labour_substitution_elasticities,
22
+ calculate_labour_net_income_elasticities,
23
+ calculate_employment_income_change,
24
+ )
25
+ from .participation import (
26
+ apply_participation_responses,
27
+ )
28
+
29
+
30
+ def calculate_excluded_from_labour_supply_responses(
31
+ sim: Simulation, count_adults: int = 2
32
+ ):
33
+ """Calculate which individuals are excluded from labour supply responses.
34
+
35
+ Excludes self-employed, full-time students, aged 60+, and individuals
36
+ outside the specified adult range.
37
+
38
+ Args:
39
+ sim: PolicyEngine simulation object
40
+ count_adults: Number of adults to include in calculations
41
+
42
+ Returns:
43
+ Boolean array indicating which individuals are excluded
44
+ """
45
+ # Exclude self-employed, full-time students, aged 60+, and adult_index == (0, >= count_adults + 1)
46
+ employment_status = sim.calculate("employment_status")
47
+ self_employed = np.isin(
48
+ employment_status, ["FT_SELF_EMPLOYED", "PT_SELF_EMPLOYED"]
49
+ )
50
+ student = employment_status == "STUDENT"
51
+ age = sim.calculate("age")
52
+ age_60_plus = age >= 60
53
+ adult_index = sim.calculate("adult_index")
54
+ excluded = (
55
+ self_employed
56
+ | student
57
+ | age_60_plus
58
+ | (adult_index == 0)
59
+ | (adult_index >= count_adults + 1)
60
+ )
61
+ return excluded.values
62
+
63
+
64
+ class FTEImpacts(BaseModel):
65
+ """Data model for FTE impacts of labour supply responses."""
66
+
67
+ substitution_response_ftes: float
68
+ """FTE impact from substitution effects."""
69
+ income_response_ftes: float
70
+ """FTE impact from income effects."""
71
+ total_response_ftes: float
72
+ """Total FTE impact from both substitution and income effects."""
73
+
74
+ participation_response_employment: Optional[float] = None
75
+ """FTE impact from participation responses, if available."""
76
+ participation_response_ftes: Optional[float] = None
77
+ """FTE impact from participation responses, if available."""
78
+
79
+ ftes: Optional[float] = None
80
+ """Total FTE impact across all responses."""
81
+
82
+
83
+ class LabourSupplyResponseData(BaseModel):
84
+ progression: pd.DataFrame
85
+ """DataFrame containing intensive margin (progression) responses."""
86
+ participation: Optional[pd.DataFrame] = None
87
+ """DataFrame containing extensive margin (participation) responses, if available."""
88
+
89
+ # Specific outputs for comparison with OBR outputs
90
+ fte_impacts: FTEImpacts
91
+ """FTE impacts of labour supply responses, including both progression and participation."""
92
+
93
+ model_config = {
94
+ "arbitrary_types_allowed": True,
95
+ }
96
+
97
+
98
+ def apply_labour_supply_responses(
99
+ sim: Simulation,
100
+ target_variable: str = "hbai_household_net_income",
101
+ input_variable: str = "employment_income",
102
+ year: int = 2025,
103
+ count_adults: int = 2,
104
+ delta: float = 1_000,
105
+ ) -> pd.DataFrame:
106
+ """Apply labour supply responses to simulation and return the response vector.
107
+
108
+ This is the main function for applying dynamic labour supply responses to
109
+ a PolicyEngine simulation. It coordinates both intensive margin (progression)
110
+ and extensive margin (participation) responses to policy changes.
111
+
112
+ Args:
113
+ sim: PolicyEngine simulation object (should have baseline attribute)
114
+ target_variable: Variable that drives labour supply decisions
115
+ input_variable: Variable representing labour supply (typically employment_income)
116
+ year: Year for calculation
117
+ count_adults: Number of adults to calculate responses for
118
+ delta: Size of change for marginal rate calculation (£)
119
+
120
+ Returns:
121
+ DataFrame with labour supply response information
122
+ """
123
+ follow_obr = sim.tax_benefit_system.parameters.gov.dynamic.obr_labour_supply_assumptions(
124
+ year
125
+ )
126
+ if (not follow_obr) or (sim.baseline is None):
127
+ return
128
+
129
+ # Calculate income changes using household_net_income
130
+ baseline_income = sim.baseline.calculate(
131
+ target_variable, year, map_to="person"
132
+ )
133
+ reform_income = sim.calculate(target_variable, year, map_to="person")
134
+
135
+ baseline_income = baseline_income
136
+ reform_income = reform_income
137
+ baseline_income = pd.Series(baseline_income, index=reform_income.index)
138
+
139
+ # Calculate relative changes
140
+ income_rel_change = np.where(
141
+ baseline_income != 0,
142
+ (reform_income - baseline_income) / baseline_income,
143
+ 0,
144
+ )
145
+
146
+ # Apply intensive margin responses (progression model)
147
+ progression_responses = apply_progression_responses(
148
+ sim=sim,
149
+ target_variable=target_variable,
150
+ input_variable=input_variable,
151
+ year=year,
152
+ count_adults=count_adults,
153
+ delta=delta,
154
+ pre_calculated_income_rel_change=income_rel_change,
155
+ )
156
+
157
+ # Apply extensive margin responses (participation model)
158
+ participation_responses = (
159
+ None # = apply_participation_responses(sim=sim, year=year)
160
+ )
161
+
162
+ # Add FTE impacts to the response data
163
+ fte_impacts = FTEImpacts(
164
+ substitution_response_ftes=progression_responses[
165
+ "substitution_response_ftes"
166
+ ].sum(),
167
+ income_response_ftes=progression_responses[
168
+ "income_response_ftes"
169
+ ].sum(),
170
+ total_response_ftes=progression_responses["total_response_ftes"].sum(),
171
+ participation_response_employment=(
172
+ participation_responses["participation_change"].sum()
173
+ if participation_responses is not None
174
+ else None
175
+ ),
176
+ participation_response_ftes=(
177
+ participation_responses["participation_change_ftes"].sum()
178
+ if participation_responses is not None
179
+ else None
180
+ ),
181
+ )
182
+
183
+ fte_impacts.ftes = fte_impacts.total_response_ftes + (
184
+ fte_impacts.participation_response_ftes
185
+ if fte_impacts.participation_response_ftes is not None
186
+ else 0
187
+ )
188
+
189
+ # For now, return only progression responses since participation is placeholder
190
+ # TODO: Combine progression and participation responses when participation model is implemented
191
+ return LabourSupplyResponseData(
192
+ progression=progression_responses,
193
+ participation=participation_responses,
194
+ fte_impacts=fte_impacts,
195
+ )
196
+
197
+
198
+ def apply_progression_responses(
199
+ sim: Simulation,
200
+ target_variable: str = "hbai_household_net_income",
201
+ input_variable: str = "employment_income",
202
+ year: int = 2025,
203
+ count_adults: int = 2,
204
+ delta: float = 1_000,
205
+ pre_calculated_income_rel_change: np.ndarray = None,
206
+ ) -> pd.DataFrame:
207
+ """Apply progression (intensive margin) labour supply responses.
208
+
209
+ This function handles the intensive margin of labour supply by calculating
210
+ how individuals adjust their working hours in response to policy changes.
211
+
212
+ Args:
213
+ sim: PolicyEngine simulation object (should have baseline attribute)
214
+ target_variable: Variable that drives labour supply decisions
215
+ input_variable: Variable representing labour supply (typically employment_income)
216
+ year: Year for calculation
217
+ count_adults: Number of adults to calculate responses for
218
+ delta: Size of change for marginal rate calculation (£)
219
+
220
+ Returns:
221
+ DataFrame with progression response information
222
+ """
223
+ # Calculate changes in marginal rates (drives substitution effects)
224
+ derivative_changes = calculate_derivative_change(
225
+ sim=sim,
226
+ target_variable=target_variable,
227
+ input_variable=input_variable,
228
+ year=year,
229
+ count_adults=count_adults,
230
+ delta=delta,
231
+ )
232
+
233
+ derivative_changes = derivative_changes.rename(
234
+ columns={col: f"deriv_{col}" for col in derivative_changes.columns}
235
+ )
236
+ derivative_changes["person_id"] = sim.calculate("person_id", year).values
237
+
238
+ # Add in actual implied wages
239
+ gross_wage = sim.calculate("employment_income", year) / sim.calculate(
240
+ "hours_worked", year
241
+ )
242
+ gross_wage = gross_wage.fillna(0).replace([np.inf, -np.inf], 0)
243
+ derivative_changes["wage_gross"] = gross_wage
244
+ derivative_changes["wage_baseline"] = (
245
+ gross_wage * derivative_changes["deriv_baseline"]
246
+ )
247
+ derivative_changes["wage_scenario"] = (
248
+ gross_wage * derivative_changes["deriv_scenario"]
249
+ )
250
+ derivative_changes["wage_rel_change"] = (
251
+ (
252
+ derivative_changes["wage_scenario"]
253
+ / derivative_changes["wage_baseline"]
254
+ - 1
255
+ )
256
+ .replace([np.inf, -np.inf, np.nan], 0)
257
+ .fillna(0)
258
+ )
259
+ derivative_changes["wage_abs_change"] = (
260
+ derivative_changes["wage_scenario"]
261
+ - derivative_changes["wage_baseline"]
262
+ )
263
+
264
+ # Calculate changes in income levels (drives income effects)
265
+ if pre_calculated_income_rel_change is not None:
266
+ n_people = len(sim.calculate("person_id", year))
267
+ income_changes = pd.DataFrame(
268
+ {
269
+ "baseline": np.zeros(n_people),
270
+ "scenario": np.zeros(n_people),
271
+ "rel_change": pre_calculated_income_rel_change,
272
+ "abs_change": np.zeros(n_people),
273
+ }
274
+ )
275
+ else:
276
+ income_changes = calculate_relative_income_change(
277
+ sim, target_variable, year
278
+ )
279
+
280
+ income_changes = income_changes.rename(
281
+ columns={col: f"income_{col}" for col in income_changes.columns}
282
+ )
283
+
284
+ df = pd.concat([derivative_changes, income_changes], axis=1).fillna(0)
285
+
286
+ # Get elasticity parameters by demographic group
287
+ substitution_elasticities = calculate_labour_substitution_elasticities(sim)
288
+ income_elasticities = calculate_labour_net_income_elasticities(sim)
289
+
290
+ df["income_elasticity"] = income_elasticities
291
+ df["substitution_elasticity"] = substitution_elasticities
292
+
293
+ # Get baseline employment income levels
294
+ employment_income = sim.calculate(input_variable, year)
295
+
296
+ df["employment_income"] = employment_income
297
+ df["hours_per_week"] = sim.calculate("hours_worked", year) / 52
298
+
299
+ # Calculate total labour supply response
300
+ response_df = calculate_employment_income_change(
301
+ employment_income=employment_income,
302
+ derivative_changes=derivative_changes,
303
+ income_changes=income_changes,
304
+ substitution_elasticities=substitution_elasticities,
305
+ income_elasticities=income_elasticities,
306
+ )
307
+
308
+ df = pd.concat([df, response_df], axis=1)
309
+
310
+ # Apply relative {substitution, income, total} changes to hours as well
311
+ # Apply relative changes to hours using the same factor for all response types
312
+ for response_type in [
313
+ "substitution_response",
314
+ "income_response",
315
+ "total_response",
316
+ ]:
317
+ df[f"{response_type}_ftes"] = (
318
+ df[response_type]
319
+ / df["employment_income"]
320
+ * df["hours_per_week"]
321
+ / 37.5
322
+ )
323
+
324
+ excluded = calculate_excluded_from_labour_supply_responses(
325
+ sim, count_adults=count_adults
326
+ )
327
+
328
+ for col in df.columns:
329
+ df.loc[excluded, col] = 0
330
+
331
+ df["excluded"] = excluded
332
+
333
+ response = response_df["total_response"].values
334
+
335
+ # Apply the labour supply response to the simulation
336
+ sim.reset_calculations()
337
+ sim.set_input(input_variable, year, employment_income + response)
338
+
339
+ weight = sim.calculate("household_weight", year, map_to="person").values
340
+
341
+ result = MicroDataFrame(df, weights=weight)
342
+
343
+ return result[~result.excluded].drop(columns=["excluded"])