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
@@ -0,0 +1,629 @@
1
+ """Labour supply participation (extensive margin) dynamics module.
2
+
3
+ This module handles the extensive margin of labour supply - how people decide
4
+ whether to work or not in response to policy changes. It implements the
5
+ methodology from the OBR's labour supply elasticity framework.
6
+
7
+ Reference: https://obr.uk/docs/dlm_uploads/NICS-Cut-Impact-on-Labour-Supply-Note.pdf
8
+ """
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ from policyengine_uk import Simulation
13
+ import warnings
14
+
15
+
16
+ def calculate_participation_elasticities(
17
+ sim: Simulation,
18
+ earnings_quintile: np.ndarray,
19
+ ) -> np.ndarray:
20
+ """Calculate labour force participation elasticities by demographic group.
21
+
22
+ Uses OBR elasticity estimates (Table A1) to assign participation elasticities
23
+ based on gender, marital status, presence/age of children, and earnings quintile.
24
+
25
+ Args:
26
+ sim: PolicyEngine simulation object
27
+ earnings_quintile: Array indicating earnings quintile (1-5) for each person
28
+
29
+ Returns:
30
+ Array of participation elasticities for each person
31
+ """
32
+ # Get demographic characteristics
33
+ gender = sim.calculate("gender")
34
+ is_married = sim.calculate("is_married", map_to="person")
35
+ has_children = sim.calculate("benunit_count_children", map_to="person") > 0
36
+ youngest_child_age = sim.calculate("youngest_child_age", map_to="person")
37
+ is_single = ~is_married
38
+
39
+ # Get partner employment status for married individuals
40
+ is_household_head = sim.calculate("is_household_head", map_to="person")
41
+ benunit_count_adults = sim.calculate(
42
+ "benunit_count_adults", map_to="person"
43
+ )
44
+ employment_income = sim.calculate("employment_income")
45
+ benunit_id = sim.calculate("benunit_id", map_to="person")
46
+ adult_index = sim.calculate("adult_index")
47
+
48
+ # Create DataFrame for efficient partner employment calculation
49
+ df = pd.DataFrame(
50
+ {
51
+ "benunit_id": benunit_id,
52
+ "is_adult": adult_index > 0,
53
+ "employed": employment_income > 0,
54
+ "benunit_count_adults": benunit_count_adults,
55
+ }
56
+ )
57
+
58
+ # Calculate total employed adults per benunit
59
+ benunit_employed = (
60
+ df[df["is_adult"]].groupby("benunit_id")["employed"].sum()
61
+ )
62
+
63
+ # Map back to individuals
64
+ employed_adults_in_benunit = (
65
+ df["benunit_id"].map(benunit_employed).fillna(0)
66
+ )
67
+
68
+ # Partner is employed if: 2-adult benunit and (both employed OR person not employed but other is)
69
+ partner_employed = (benunit_count_adults == 2) & (
70
+ (employed_adults_in_benunit == 2)
71
+ | ((employed_adults_in_benunit == 1) & (employment_income == 0))
72
+ )
73
+
74
+ # Initialize elasticity array
75
+ elasticities = np.zeros(gender.shape, dtype=float)
76
+
77
+ # Define elasticity values by quintile (from Table A1)
78
+ elasticity_matrices = {
79
+ # Men (except lone fathers)
80
+ "men": np.array([0.227, 0.182, 0.136, 0.091, 0.023]),
81
+ # Single women without children
82
+ "single_women_no_children": np.array(
83
+ [0.216, 0.173, 0.130, 0.086, 0.022]
84
+ ),
85
+ # Women without children, non-working partner
86
+ "women_no_children_nonworking_partner": np.array(
87
+ [0.216, 0.173, 0.130, 0.086, 0.022]
88
+ ),
89
+ # Women without children, working partner
90
+ "women_no_children_working_partner": np.array(
91
+ [0.432, 0.345, 0.259, 0.173, 0.043]
92
+ ),
93
+ # Lone parents by youngest child age
94
+ "lone_parent_0_2": np.array([1.195, 0.956, 0.717, 0.478, 0.120]),
95
+ "lone_parent_3_5": np.array([1.554, 1.243, 0.932, 0.621, 0.155]),
96
+ "lone_parent_6_10": np.array([1.195, 0.956, 0.717, 0.478, 0.120]),
97
+ "lone_parent_11_plus": np.array([0.797, 0.637, 0.478, 0.319, 0.080]),
98
+ # Women with non-working partner by youngest child age
99
+ "women_nonworking_partner_0_2": np.array(
100
+ [0.324, 0.259, 0.194, 0.129, 0.032]
101
+ ),
102
+ "women_nonworking_partner_3_5": np.array(
103
+ [0.421, 0.336, 0.253, 0.168, 0.042]
104
+ ),
105
+ "women_nonworking_partner_6_10": np.array(
106
+ [0.324, 0.259, 0.194, 0.130, 0.033]
107
+ ),
108
+ "women_nonworking_partner_11_plus": np.array(
109
+ [0.216, 0.173, 0.130, 0.086, 0.021]
110
+ ),
111
+ # Women with working partner by youngest child age
112
+ "women_working_partner_0_2": np.array(
113
+ [0.755, 0.604, 0.453, 0.302, 0.076]
114
+ ),
115
+ "women_working_partner_3_5": np.array(
116
+ [0.982, 0.786, 0.589, 0.393, 0.098]
117
+ ),
118
+ "women_working_partner_6_10": np.array(
119
+ [0.755, 0.604, 0.453, 0.302, 0.076]
120
+ ),
121
+ "women_working_partner_11_plus": np.array(
122
+ [0.504, 0.403, 0.302, 0.201, 0.051]
123
+ ),
124
+ }
125
+
126
+ # Vectorized assignment function
127
+ def assign_elasticities(mask, elasticity_key):
128
+ if mask.any():
129
+ # Ensure quintiles are in valid range
130
+ valid_quintiles = np.clip(
131
+ earnings_quintile[mask] - 1, 0, 4
132
+ ).astype(int)
133
+ elasticities[mask] = elasticity_matrices[elasticity_key][
134
+ valid_quintiles
135
+ ]
136
+
137
+ # Men (except lone fathers)
138
+ men_not_lone_parent = (gender == "MALE") & ~(is_single & has_children)
139
+ assign_elasticities(men_not_lone_parent, "men")
140
+
141
+ # Single women without children
142
+ single_women_no_children = (gender == "FEMALE") & is_single & ~has_children
143
+ assign_elasticities(single_women_no_children, "single_women_no_children")
144
+
145
+ # Women without children by partner employment status
146
+ women_no_children = (gender == "FEMALE") & ~has_children & is_married
147
+ assign_elasticities(
148
+ women_no_children & ~partner_employed,
149
+ "women_no_children_nonworking_partner",
150
+ )
151
+ assign_elasticities(
152
+ women_no_children & partner_employed,
153
+ "women_no_children_working_partner",
154
+ )
155
+
156
+ # Lone parents by youngest child age
157
+ lone_parents = (gender == "FEMALE") & is_single & has_children
158
+ assign_elasticities(
159
+ lone_parents & (youngest_child_age <= 2), "lone_parent_0_2"
160
+ )
161
+ assign_elasticities(
162
+ lone_parents & (youngest_child_age >= 3) & (youngest_child_age <= 5),
163
+ "lone_parent_3_5",
164
+ )
165
+ assign_elasticities(
166
+ lone_parents & (youngest_child_age >= 6) & (youngest_child_age <= 10),
167
+ "lone_parent_6_10",
168
+ )
169
+ assign_elasticities(
170
+ lone_parents & (youngest_child_age >= 11), "lone_parent_11_plus"
171
+ )
172
+
173
+ # Women with children by partner employment status and youngest child age
174
+ women_with_children = (gender == "FEMALE") & has_children & is_married
175
+
176
+ # Non-working partner
177
+ women_nonworking_partner = women_with_children & ~partner_employed
178
+ assign_elasticities(
179
+ women_nonworking_partner & (youngest_child_age <= 2),
180
+ "women_nonworking_partner_0_2",
181
+ )
182
+ assign_elasticities(
183
+ women_nonworking_partner
184
+ & (youngest_child_age >= 3)
185
+ & (youngest_child_age <= 5),
186
+ "women_nonworking_partner_3_5",
187
+ )
188
+ assign_elasticities(
189
+ women_nonworking_partner
190
+ & (youngest_child_age >= 6)
191
+ & (youngest_child_age <= 10),
192
+ "women_nonworking_partner_6_10",
193
+ )
194
+ assign_elasticities(
195
+ women_nonworking_partner & (youngest_child_age >= 11),
196
+ "women_nonworking_partner_11_plus",
197
+ )
198
+
199
+ # Working partner
200
+ women_working_partner = women_with_children & partner_employed
201
+ assign_elasticities(
202
+ women_working_partner & (youngest_child_age <= 2),
203
+ "women_working_partner_0_2",
204
+ )
205
+ assign_elasticities(
206
+ women_working_partner
207
+ & (youngest_child_age >= 3)
208
+ & (youngest_child_age <= 5),
209
+ "women_working_partner_3_5",
210
+ )
211
+ assign_elasticities(
212
+ women_working_partner
213
+ & (youngest_child_age >= 6)
214
+ & (youngest_child_age <= 10),
215
+ "women_working_partner_6_10",
216
+ )
217
+ assign_elasticities(
218
+ women_working_partner & (youngest_child_age >= 11),
219
+ "women_working_partner_11_plus",
220
+ )
221
+
222
+ return elasticities
223
+
224
+
225
+ def impute_wages_for_nonworkers(
226
+ sim: Simulation,
227
+ year: int = 2025,
228
+ hours_for_new_entrants: float = 18.8,
229
+ ) -> np.ndarray:
230
+ """Impute wages for non-workers based on their elasticity group.
231
+
232
+ Assumes non-workers would work 18.8 hours per week at the average wage
233
+ for their specific elasticity group.
234
+
235
+ Args:
236
+ sim: PolicyEngine simulation object
237
+ year: Year for calculation
238
+ hours_for_new_entrants: Weekly hours assumed for new entrants
239
+
240
+ Returns:
241
+ Array of imputed annual employment income for non-workers
242
+ """
243
+ employment_income = sim.calculate("employment_income", year)
244
+ hours_worked = sim.calculate("hours_worked", year)
245
+
246
+ # Calculate hourly wages for workers
247
+ working_mask = (employment_income > 0) & (hours_worked > 0)
248
+ hourly_wages = np.where(
249
+ working_mask, employment_income / (hours_worked * 52), 0
250
+ )
251
+
252
+ # Get elasticity groups for wage calculation
253
+ earnings_quintile = calculate_earnings_quintile(
254
+ sim, year, hours_for_new_entrants, random_seed=42
255
+ )
256
+ elasticities = calculate_participation_elasticities(sim, earnings_quintile)
257
+
258
+ # Create elasticity bins for grouping
259
+ unique_elasticities = np.unique(elasticities[elasticities > 0])
260
+
261
+ # Calculate average wage by elasticity group
262
+ imputed_wages = np.zeros_like(employment_income, dtype=float)
263
+
264
+ for elasticity_val in unique_elasticities:
265
+ elasticity_mask = (elasticities == elasticity_val) & working_mask
266
+ if elasticity_mask.any():
267
+ avg_hourly_wage = np.mean(hourly_wages[elasticity_mask])
268
+ # Apply to all non-workers in this elasticity group
269
+ nonworker_mask = (elasticities == elasticity_val) & ~working_mask
270
+ imputed_wages[nonworker_mask] = (
271
+ avg_hourly_wage * hours_for_new_entrants * 52
272
+ )
273
+
274
+ return imputed_wages
275
+
276
+
277
+ def calculate_gain_to_work(
278
+ sim: Simulation,
279
+ year: int = 2025,
280
+ hours_for_new_entrants: float = 18.8,
281
+ count_adults: int = 1,
282
+ impute_nonworker_wages: bool = True,
283
+ ) -> pd.DataFrame:
284
+ """Calculate gain-to-work metric for each individual.
285
+
286
+ The gain-to-work is the difference between income when working vs not working.
287
+ Uses adult_index to handle multi-adult benefit units correctly.
288
+ Optionally imputes wages for non-workers.
289
+
290
+ Args:
291
+ sim: PolicyEngine simulation object
292
+ year: Year for calculation
293
+ hours_for_new_entrants: Weekly hours for new labour market entrants
294
+ count_adults: Number of adults to calculate responses for
295
+ impute_nonworker_wages: Whether to impute wages for non-workers
296
+
297
+ Returns:
298
+ DataFrame with in-work income, out-of-work income, and gain-to-work
299
+ """
300
+ # Get current employment status and income
301
+ employment_income = sim.calculate("employment_income", year)
302
+ hours_worked = sim.calculate("hours_worked", year)
303
+ household_net_income = sim.calculate(
304
+ "household_net_income", year, map_to="person"
305
+ )
306
+ adult_index = sim.calculate("adult_index")
307
+
308
+ # Impute wages for non-workers if requested
309
+ if impute_nonworker_wages:
310
+ imputed_wages = impute_wages_for_nonworkers(
311
+ sim, year, hours_for_new_entrants
312
+ )
313
+ # For non-workers, use imputed wages; for workers, use actual income
314
+ working_mask = employment_income > 0
315
+ employment_income_with_imputation = np.where(
316
+ working_mask, employment_income, imputed_wages
317
+ )
318
+ else:
319
+ employment_income_with_imputation = employment_income
320
+
321
+ # Initialize arrays
322
+ out_of_work_income = household_net_income.copy()
323
+ in_work_income = household_net_income.copy()
324
+
325
+ # Calculate both in-work and out-of-work income for each adult group
326
+ original_employment = employment_income.copy()
327
+
328
+ for i in range(1, count_adults + 1):
329
+ is_adult_i = adult_index == i
330
+
331
+ if is_adult_i.any():
332
+ # Calculate out-of-work income (set employment to 0)
333
+ temp_employment_out = employment_income.copy()
334
+ temp_employment_out[is_adult_i] = 0
335
+
336
+ sim.reset_calculations()
337
+ sim.set_input("employment_income", year, temp_employment_out)
338
+ out_of_work_income[is_adult_i] = sim.calculate(
339
+ "household_net_income", year, map_to="person"
340
+ )[is_adult_i]
341
+
342
+ # Calculate in-work income (use imputed wages if applicable)
343
+ temp_employment_in = employment_income.copy()
344
+ temp_employment_in[is_adult_i] = employment_income_with_imputation[
345
+ is_adult_i
346
+ ]
347
+
348
+ sim.reset_calculations()
349
+ sim.set_input("employment_income", year, temp_employment_in)
350
+ in_work_income[is_adult_i] = sim.calculate(
351
+ "household_net_income", year, map_to="person"
352
+ )[is_adult_i]
353
+
354
+ # Reset to original state
355
+ sim.reset_calculations()
356
+ sim.set_input("employment_income", year, original_employment)
357
+
358
+ # Calculate gain to work
359
+ gain_to_work = in_work_income - out_of_work_income
360
+
361
+ return pd.DataFrame(
362
+ {
363
+ "in_work_income": in_work_income,
364
+ "out_of_work_income": out_of_work_income,
365
+ "gain_to_work": gain_to_work,
366
+ }
367
+ )
368
+
369
+
370
+ def calculate_earnings_quintile(
371
+ sim: Simulation,
372
+ year: int = 2025,
373
+ hours_for_new_entrants: float = 18.8,
374
+ random_seed: int = 42,
375
+ ) -> np.ndarray:
376
+ """Calculate earnings quintile for each person based on potential earnings.
377
+
378
+ For workers, uses actual earnings. For non-workers, uses imputed potential earnings.
379
+
380
+ Args:
381
+ sim: PolicyEngine simulation object
382
+ year: Year for calculation
383
+ hours_for_new_entrants: Weekly hours assumed for new entrants
384
+ random_seed: Seed for random number generation
385
+
386
+ Returns:
387
+ Array of quintiles (1-5) for each person
388
+ """
389
+ employment_income = sim.calculate("employment_income", year)
390
+
391
+ # Calculate quintiles
392
+ # Use pandas qcut for equal-sized bins
393
+ # Add random noise to avoid ties in quintile calculation
394
+ rng = np.random.RandomState(random_seed)
395
+ quintiles = pd.qcut(
396
+ employment_income + rng.random(employment_income.shape),
397
+ q=5,
398
+ labels=[1, 2, 3, 4, 5],
399
+ duplicates="drop",
400
+ )
401
+
402
+ if quintiles is not None:
403
+ return quintiles.astype(int)
404
+ else:
405
+ return np.ones(employment_income.shape, dtype=int)
406
+
407
+
408
+ def apply_participation_responses(
409
+ sim: Simulation,
410
+ year: int = 2025,
411
+ hours_for_new_entrants: float = 18.8,
412
+ count_adults: int = 2,
413
+ random_seed: int = 42,
414
+ ) -> pd.DataFrame:
415
+ """Apply participation responses to simulation at microdata level.
416
+
417
+ Stochastically applies participation responses to individual workers and
418
+ non-workers based on their calculated participation elasticities.
419
+
420
+ Args:
421
+ sim: PolicyEngine simulation object (must have baseline)
422
+ year: Year for calculation
423
+ hours_for_new_entrants: Weekly hours for new labour market entrants
424
+ count_adults: Number of adults to calculate responses for
425
+ random_seed: Seed for random number generation
426
+
427
+ Returns:
428
+ DataFrame with participation response information and updated employment
429
+ """
430
+ if sim.baseline is None:
431
+ return pd.DataFrame()
432
+
433
+ # Set random seed for reproducibility
434
+ np.random.seed(random_seed)
435
+
436
+ # Calculate excluded individuals
437
+ from .labour_supply import calculate_excluded_from_labour_supply_responses
438
+
439
+ excluded = calculate_excluded_from_labour_supply_responses(
440
+ sim, count_adults
441
+ )
442
+
443
+ # Get employment status
444
+ employment_income = sim.calculate("employment_income", year)
445
+ adult_index = sim.calculate("adult_index")
446
+ eligible = ~excluded & (adult_index > 0) & (adult_index <= count_adults)
447
+ currently_working = (employment_income > 0) & eligible
448
+ currently_not_working = (employment_income == 0) & eligible
449
+
450
+ # Calculate gain-to-work for baseline and reform (with wage imputation)
451
+ baseline_gtw = calculate_gain_to_work(
452
+ sim.baseline,
453
+ year,
454
+ hours_for_new_entrants,
455
+ count_adults,
456
+ impute_nonworker_wages=True,
457
+ )
458
+ reform_gtw = calculate_gain_to_work(
459
+ sim,
460
+ year,
461
+ hours_for_new_entrants,
462
+ count_adults,
463
+ impute_nonworker_wages=True,
464
+ )
465
+
466
+ # Calculate percentage change in gain-to-work
467
+ gtw_baseline = baseline_gtw["gain_to_work"].values
468
+ gtw_reform = reform_gtw["gain_to_work"].values
469
+
470
+ # Avoid division by zero
471
+ gtw_pct_change = np.zeros_like(gtw_baseline)
472
+ positive_baseline = gtw_baseline > 0
473
+ gtw_pct_change[positive_baseline] = (
474
+ gtw_reform[positive_baseline] - gtw_baseline[positive_baseline]
475
+ ) / gtw_baseline[positive_baseline]
476
+
477
+ # Get elasticities
478
+ earnings_quintile = calculate_earnings_quintile(
479
+ sim, year, hours_for_new_entrants, random_seed
480
+ )
481
+ elasticities_wrt_income = calculate_participation_elasticities(
482
+ sim, earnings_quintile
483
+ )
484
+
485
+ # Transform elasticities from w.r.t. in-work income to w.r.t. gain-to-work
486
+ # Following OBR Appendix E methodology: multiply by (1 - replacement_rate)
487
+ in_work_income = baseline_gtw["in_work_income"].values
488
+ out_of_work_income = baseline_gtw["out_of_work_income"].values
489
+
490
+ # Calculate replacement rates (out-of-work income / in-work income)
491
+ replacement_rate = np.zeros_like(in_work_income)
492
+ positive_in_work = in_work_income > 0
493
+ replacement_rate[positive_in_work] = (
494
+ out_of_work_income[positive_in_work] / in_work_income[positive_in_work]
495
+ )
496
+ replacement_rate = np.clip(
497
+ replacement_rate, 0, 1
498
+ ) # Ensure between 0 and 1
499
+
500
+ # Transform elasticities
501
+ elasticities = elasticities_wrt_income * (1 - replacement_rate)
502
+
503
+ # Calculate participation probability change for each person
504
+ # From OBR methodology: percentage change in participation = elasticity * percentage change in GTW
505
+ direct_participation_change = elasticities * gtw_pct_change
506
+
507
+ # Calculate surplus participation effects (OBR methodology)
508
+ # When workers have better incentives (positive GTW change), this creates a "surplus"
509
+ # that pulls non-workers into employment
510
+ # When workers have worse incentives (negative GTW change), this creates a "deficit"
511
+ # that pushes non-workers further away from employment
512
+
513
+ # Group people by elasticity group for surplus calculation
514
+ elasticity_groups = pd.cut(
515
+ elasticities, bins=20, labels=False, duplicates="drop"
516
+ )
517
+
518
+ surplus_participation_change = np.zeros_like(direct_participation_change)
519
+
520
+ for group in np.unique(elasticity_groups[~np.isnan(elasticity_groups)]):
521
+ group_mask = elasticity_groups == group
522
+ workers_in_group = group_mask & currently_working
523
+ nonworkers_in_group = group_mask & currently_not_working
524
+
525
+ if workers_in_group.any() and nonworkers_in_group.any():
526
+ # Calculate average GTW change for workers in this elasticity group
527
+ avg_worker_gtw_change = np.mean(gtw_pct_change[workers_in_group])
528
+
529
+ # Apply a fraction of this to non-workers in the same group
530
+ # Using spillover factor of 1 (100% of worker effect) to match OBR
531
+ spillover_factor = 1
532
+ surplus_participation_change[nonworkers_in_group] = (
533
+ elasticities[nonworkers_in_group]
534
+ * avg_worker_gtw_change
535
+ * spillover_factor
536
+ )
537
+
538
+ # Similarly, non-worker GTW changes affect workers (pushing them out if negative)
539
+ avg_nonworker_gtw_change = np.mean(
540
+ gtw_pct_change[nonworkers_in_group]
541
+ )
542
+ surplus_participation_change[workers_in_group] = (
543
+ elasticities[workers_in_group]
544
+ * avg_nonworker_gtw_change
545
+ * spillover_factor
546
+ )
547
+
548
+ # Total participation change includes both direct and surplus effects
549
+ participation_change = (
550
+ direct_participation_change + surplus_participation_change
551
+ )
552
+
553
+ # Apply stochastic participation responses
554
+ new_employment_income = employment_income.copy()
555
+ participation_response = np.zeros_like(employment_income, dtype=bool)
556
+
557
+ # For currently working individuals: chance of leaving work
558
+ for i in np.where(currently_working)[0]:
559
+ # Negative participation change means lower probability of working
560
+ exit_probability = max(
561
+ 0, -participation_change[i]
562
+ ) # Only consider negative changes
563
+ if np.random.random() < exit_probability:
564
+ new_employment_income[i] = 0
565
+ participation_response[i] = True # Exited work
566
+
567
+ # For currently non-working individuals: chance of entering work
568
+ imputed_wages = impute_wages_for_nonworkers(
569
+ sim, year, hours_for_new_entrants
570
+ )
571
+ for i in np.where(currently_not_working)[0]:
572
+ # Positive participation change means higher probability of working
573
+ entry_probability = max(
574
+ 0, participation_change[i]
575
+ ) # Only consider positive changes
576
+ if np.random.random() < entry_probability and imputed_wages[i] > 0:
577
+ new_employment_income[i] = imputed_wages[i]
578
+ participation_response[i] = True # Entered work
579
+
580
+ # Update simulation with new employment incomes
581
+ sim.set_input("employment_income", year, new_employment_income)
582
+
583
+ # Update hours worked for new entrants
584
+ hours_worked = sim.calculate("hours_worked", year)
585
+ new_hours_worked = hours_worked.copy()
586
+
587
+ # Set hours for new entrants
588
+ new_workers = currently_not_working & (new_employment_income > 0)
589
+ new_hours_worked[new_workers] = hours_for_new_entrants
590
+
591
+ # Set hours to 0 for those who left work
592
+ left_work = currently_working & (new_employment_income == 0)
593
+ new_hours_worked[left_work] = 0
594
+
595
+ sim.set_input("hours_worked", year, new_hours_worked)
596
+
597
+ # Create results DataFrame
598
+ results = pd.DataFrame(
599
+ {
600
+ "originally_working": currently_working,
601
+ "originally_not_working": currently_not_working,
602
+ "participation_elasticity": elasticities,
603
+ "elasticity_group": elasticity_groups,
604
+ "gtw_baseline": gtw_baseline,
605
+ "gtw_reform": gtw_reform,
606
+ "gtw_pct_change": gtw_pct_change,
607
+ "direct_participation_change": direct_participation_change,
608
+ "surplus_participation_change": surplus_participation_change,
609
+ "participation_change": participation_change,
610
+ "participation_response": participation_response,
611
+ "new_employment_income": new_employment_income,
612
+ "excluded": excluded,
613
+ }
614
+ )
615
+
616
+ results["participation_change_ftes"] = results["participation_change"] * (
617
+ hours_for_new_entrants / 37.5
618
+ ) # Convert to FTEs
619
+
620
+ weights = sim.calculate("household_weight", year, map_to="person")
621
+
622
+ # Weight and filter
623
+ from microdf import MicroDataFrame
624
+
625
+ weighted_results = MicroDataFrame(results, weights=weights)
626
+
627
+ return weighted_results[~weighted_results.excluded].drop(
628
+ columns=["excluded"]
629
+ )