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.
- policyengine_uk/__init__.py +5 -3
- policyengine_uk/data/__init__.py +1 -0
- policyengine_uk/data/dataset_schema.py +70 -18
- policyengine_uk/data/economic_assumptions.py +36 -10
- policyengine_uk/data/filter_dataset.py +52 -0
- policyengine_uk/dynamics/labour_supply.py +343 -0
- policyengine_uk/dynamics/participation.py +629 -0
- policyengine_uk/dynamics/progression.py +384 -0
- policyengine_uk/microsimulation.py +105 -0
- policyengine_uk/model_api.py +1 -0
- policyengine_uk/parameters/gov/boe/base_rate.yaml +34 -0
- policyengine_uk/parameters/gov/boe/index.yaml +2 -0
- policyengine_uk/parameters/gov/contrib/behavioral_responses/employee_salary_sacrifice_reduction_rate.yaml +14 -0
- policyengine_uk/parameters/gov/contrib/behavioral_responses/salary_sacrifice_broad_base_haircut_rate.yaml +22 -0
- policyengine_uk/parameters/gov/contrib/cec/state_pension_increase.yaml +1 -1
- policyengine_uk/parameters/gov/contrib/ubi_center/carbon_tax.yaml +2 -2
- policyengine_uk/parameters/gov/contrib/ubi_center/land_value_tax.yaml +3 -3
- policyengine_uk/parameters/gov/dcms/bbc/tv_licence/colour.yaml +5 -5
- policyengine_uk/parameters/gov/dfe/education_spending.yaml +1 -1
- policyengine_uk/parameters/gov/dft/rail/fare_index.yaml +32 -0
- policyengine_uk/parameters/gov/dft/rail/prior_law_fare_index.yaml +32 -0
- policyengine_uk/parameters/gov/dft/rail/ridership_index.yaml +30 -0
- policyengine_uk/parameters/gov/dft/spending.yaml +2 -2
- policyengine_uk/parameters/gov/dwp/ESA/income/earn_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_lone_parent.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/ESA/income/income_disregard_single.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/ESA/income/pension_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/IIDB/maximum.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/contrib/amount_over_25.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/contrib/earn_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/contrib/pension_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/income/amount_18_24.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/JSA/income/amount_over_25.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_lone_parent.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/JSA/income/income_disregard_single.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_age_threshold.yaml +12 -0
- policyengine_uk/parameters/gov/dwp/attendance_allowance/higher.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/attendance_allowance/lower.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/benefit_cap.yaml +3 -3
- policyengine_uk/parameters/gov/dwp/carer_premium/couple.yaml +2 -2
- policyengine_uk/parameters/gov/dwp/carer_premium/single.yaml +6 -6
- policyengine_uk/parameters/gov/dwp/carers_allowance/rate.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/disability_premia/disability_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/disability_premia/enhanced_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/disability_premia/enhanced_single.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/disability_premia/severe_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/dla/mobility/higher.yaml +4 -4
- policyengine_uk/parameters/gov/dwp/dla/mobility/lower.yaml +8 -8
- policyengine_uk/parameters/gov/dwp/dla/self_care/higher.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/dla/self_care/lower.yaml +8 -8
- policyengine_uk/parameters/gov/dwp/dla/self_care/middle.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/older.yaml +3 -3
- policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/older.yaml +3 -3
- policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/worker.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/amount.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/housing_benefit/takeup.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/income_support/amounts/amount_16_24.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/amounts/amount_couples_over_18.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/means_test/earn_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_couple.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_lone_parent.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/means_test/income_disregard_single.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/means_test/pension_disregard.yaml +1 -1
- policyengine_uk/parameters/gov/dwp/income_support/takeup.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/carer/addition.yaml +4 -4
- policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee.yaml +9 -9
- policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/severe_disability/addition.yaml +3 -3
- policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/threshold.yaml +5 -5
- policyengine_uk/parameters/gov/dwp/pip/daily_living/enhanced.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/pip/daily_living/standard.yaml +8 -8
- policyengine_uk/parameters/gov/dwp/pip/mobility/enhanced.yaml +4 -4
- policyengine_uk/parameters/gov/dwp/pip/mobility/standard.yaml +9 -9
- policyengine_uk/parameters/gov/dwp/sda/maximum.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/state_pension/basic_state_pension/amount.yaml +11 -11
- policyengine_uk/parameters/gov/dwp/state_pension/new_state_pension/amount.yaml +4 -4
- policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/limit/child_count.yaml +10 -1
- policyengine_uk/parameters/gov/dwp/tax_credits/child_tax_credit/takeup.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/tax_credits/working_tax_credit/takeup.yaml +7 -7
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/carer/amount.yaml +3 -3
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/amount.yaml +2 -4
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/disabled/amount.yaml +2 -4
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/first/higher_amount.yaml +6 -8
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/limit/child_count.yaml +6 -1
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/severely_disabled/amount.yaml +3 -5
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/childcare/cap.yaml +2 -6
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/disabled/amount.yaml +4 -6
- policyengine_uk/parameters/gov/dwp/universal_credit/elements/housing/non_dep_deduction/amount.yaml +4 -1
- policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/active.yaml +9 -0
- policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/new_claimant_health_element.yaml +9 -0
- policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/standard_allowance_uplift.yaml +13 -0
- policyengine_uk/parameters/gov/dwp/universal_credit/standard_allowance/amount.yaml +5 -5
- policyengine_uk/parameters/gov/dwp/winter_fuel_payment/eligibility/taxable_income_test/maximum_taxable_income.yaml +2 -1
- policyengine_uk/parameters/gov/dwp/winter_fuel_payment/eligibility/taxable_income_test/use_maximum_taxable_income.yaml +1 -0
- policyengine_uk/parameters/gov/dynamic/obr_labour_supply_assumptions.yaml +9 -0
- policyengine_uk/parameters/gov/economic_assumptions/create_economic_assumption_indices.py +1 -1
- policyengine_uk/parameters/gov/economic_assumptions/yoy_growth.yaml +522 -153
- policyengine_uk/parameters/gov/hmrc/cgt/additional_rate.yaml +5 -0
- policyengine_uk/parameters/gov/hmrc/cgt/basic_rate.yaml +5 -0
- policyengine_uk/parameters/gov/hmrc/cgt/higher_rate.yaml +4 -0
- policyengine_uk/parameters/gov/hmrc/child_benefit/amount/additional.yaml +6 -6
- policyengine_uk/parameters/gov/hmrc/child_benefit/amount/eldest.yaml +8 -8
- policyengine_uk/parameters/gov/hmrc/child_benefit/takeup/by_age.yaml +1 -1
- policyengine_uk/parameters/gov/hmrc/fuel_duty/calculate_fuel_duty_rates.py +464 -0
- policyengine_uk/parameters/gov/hmrc/fuel_duty/petrol_and_diesel.yaml +86 -10
- policyengine_uk/parameters/gov/hmrc/income_tax/allowances/personal_allowance/amount.yaml +6 -0
- policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml +2 -1
- policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_additions.yaml +1 -0
- policyengine_uk/parameters/gov/hmrc/income_tax/rates/dividends.yaml +12 -0
- policyengine_uk/parameters/gov/hmrc/income_tax/rates/property.yaml +46 -0
- policyengine_uk/parameters/gov/hmrc/income_tax/rates/savings.yaml +46 -0
- policyengine_uk/parameters/gov/hmrc/income_tax/rates/scotland/rates.yaml +2 -2
- policyengine_uk/parameters/gov/hmrc/income_tax/rates/uk.yaml +14 -2
- policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/additional.yaml +4 -6
- policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml +3 -3
- policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml +14 -4
- policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/flat_rate.yaml +2 -2
- policyengine_uk/parameters/gov/hmrc/national_insurance/salary_sacrifice_pension_cap.yaml +16 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/index.yaml +12 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_1/boe_margin.yaml +11 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/additional_rate.yaml +27 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/index.yaml +16 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/plan_2/upper_threshold.yaml +48 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/interest_rates/postgraduate_additional_rate.yaml +11 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/postgraduate_repayment_rate.yaml +9 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/repayment_rate.yaml +9 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_1.yaml +25 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_2.yaml +58 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_4.yaml +19 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/plan_5.yaml +16 -0
- policyengine_uk/parameters/gov/hmrc/student_loans/thresholds/postgraduate.yaml +21 -0
- policyengine_uk/parameters/gov/hmrc/vat/reduced_rate_share.yaml +3 -3
- policyengine_uk/parameters/gov/indices/private_rent_index.yaml +9 -9
- policyengine_uk/parameters/gov/revenue_scotland/lbtt/non_residential.yaml +2 -2
- policyengine_uk/parameters/gov/revenue_scotland/lbtt/rent.yaml +2 -2
- policyengine_uk/parameters/gov/revenue_scotland/lbtt/residential/first_time_buyer_rate.yaml +2 -2
- policyengine_uk/parameters/gov/revenue_scotland/lbtt/residential/rate.yaml +2 -2
- policyengine_uk/parameters/gov/wra/land_transaction_tax/non_residential.yaml +2 -2
- policyengine_uk/parameters/gov/wra/land_transaction_tax/rent.yaml +2 -2
- policyengine_uk/parameters/gov/wra/land_transaction_tax/residential/higher_rate.yaml +1 -1
- policyengine_uk/parameters/gov/wra/land_transaction_tax/residential/primary.yaml +2 -2
- policyengine_uk/parameters/household/consumption/carbon/consumption.yaml +8 -6
- policyengine_uk/parameters/household/consumption/carbon/intensity.yaml +4 -1
- policyengine_uk/parameters/household/consumption/carbon/production.yaml +12 -7
- policyengine_uk/parameters/household/consumption/carbon/production_by_source.yaml +76 -41
- policyengine_uk/parameters/household/consumption/fuel/prices/petrol.yaml +1 -1
- policyengine_uk/parameters/household/poverty/absolute_poverty_threshold_bhc.yaml +1 -1
- policyengine_uk/reforms/policyengine/adjust_budgets.py +0 -1
- policyengine_uk/scenarios/__init__.py +4 -0
- policyengine_uk/scenarios/pip_reform.py +23 -0
- policyengine_uk/scenarios/reindex_benefit_cap.py +32 -0
- policyengine_uk/scenarios/repeal_two_child_limit.py +10 -0
- policyengine_uk/scenarios/uc_reform.py +50 -0
- policyengine_uk/simulation.py +619 -0
- policyengine_uk/system.py +3 -257
- policyengine_uk/tax_benefit_system.py +141 -0
- policyengine_uk/tests/behavioral_responses/test_labor_supply_responses.yaml +183 -0
- policyengine_uk/tests/microsimulation/reforms_config.yaml +8 -8
- policyengine_uk/tests/microsimulation/test_reform_impacts.py +2 -2
- policyengine_uk/tests/microsimulation/test_salary_sacrifice_cap_reform.py +401 -0
- policyengine_uk/tests/microsimulation/test_validity.py +2 -3
- policyengine_uk/tests/microsimulation/update_reform_impacts.py +104 -40
- policyengine_uk/tests/policy/baseline/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.yaml +105 -0
- policyengine_uk/tests/policy/baseline/finance/benefit/family/child_benefit.yaml +2 -0
- policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support.yaml +0 -23
- policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/tv_licence.yaml +3 -0
- policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml +17 -17
- policyengine_uk/tests/policy/baseline/gov/dwp/basic_state_pension.yaml +44 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/gift_aid.yaml +71 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/personal_allowance.yaml +161 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.yaml +107 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.yaml +95 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/student_loans/student_loan_interest_rate.yaml +153 -0
- policyengine_uk/tests/policy/baseline/gov/hmrc/student_loans/student_loan_repayment.yaml +130 -0
- policyengine_uk/tests/policy/baseline/household/wealth/vehicle.yaml +27 -0
- policyengine_uk/tests/policy/reforms/nov_2025_budget/income_source_tax_rates.yaml +235 -0
- policyengine_uk/tests/policy/reforms/nov_2025_budget/income_tax_freeze.yaml +83 -0
- policyengine_uk/tests/policy/reforms/parametric/basic_income/basic_income.yaml +1 -0
- policyengine_uk/tests/test_behavioral_responses.py +215 -0
- policyengine_uk/tests/test_fiscal_year_parameters.py +131 -0
- policyengine_uk/utils/__init__.py +1 -0
- policyengine_uk/utils/compare.py +28 -0
- policyengine_uk/utils/create_ahc_deflator.py +169 -0
- policyengine_uk/utils/create_triple_lock.py +1 -1
- policyengine_uk/utils/dependencies.py +259 -0
- policyengine_uk/utils/parameters.py +12 -1
- policyengine_uk/utils/scenario.py +225 -0
- policyengine_uk/utils/solve_private_school_attendance_factor.py +4 -6
- policyengine_uk/variables/contrib/policyengine/education_budget_change.py +0 -1
- policyengine_uk/variables/contrib/policyengine/employer_ni/baseline_employer_cost.py +5 -1
- policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.py +23 -23
- policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_capital_incidence.py +1 -1
- policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_response_consumer_incidence.py +1 -1
- policyengine_uk/variables/contrib/policyengine/other_public_spending_budget_change.py +0 -1
- policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py +0 -1
- policyengine_uk/variables/gov/dft/rail_subsidy_spending.py +16 -1
- policyengine_uk/variables/gov/dft/rail_usage.py +16 -0
- policyengine_uk/variables/gov/dwp/BRMA_LHA_rate.py +7 -2
- policyengine_uk/variables/gov/dwp/LHA_category.py +4 -2
- policyengine_uk/variables/gov/dwp/additional_state_pension.py +4 -2
- policyengine_uk/variables/gov/dwp/basic_state_pension.py +26 -8
- policyengine_uk/variables/gov/dwp/is_CTC_eligible.py +1 -1
- policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py +9 -13
- policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +66 -0
- policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +75 -0
- policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py +66 -0
- policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py +5 -4
- policyengine_uk/variables/gov/gov_tax.py +0 -2
- policyengine_uk/variables/gov/hmrc/household_tax.py +0 -1
- policyengine_uk/variables/gov/hmrc/income_tax/allowances/gift_aid.py +23 -0
- policyengine_uk/variables/gov/hmrc/income_tax/allowances/personal_allowance.py +9 -2
- policyengine_uk/variables/gov/hmrc/income_tax/income_tax_pre_charges.py +1 -0
- policyengine_uk/variables/gov/hmrc/income_tax/liability/property_income_tax.py +75 -0
- policyengine_uk/variables/gov/hmrc/income_tax/liability/savings_income_tax.py +4 -4
- policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_broad_base_haircut.py +43 -0
- policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.py +38 -0
- policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.py +27 -0
- policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_adjusted.py +31 -0
- policyengine_uk/variables/gov/hmrc/pensions/salary_sacrifice_returned_to_income.py +41 -0
- policyengine_uk/variables/gov/hmrc/student_loans/__init__.py +2 -0
- policyengine_uk/variables/gov/hmrc/student_loans/plan_1_interest_rate.py +23 -0
- policyengine_uk/variables/gov/hmrc/student_loans/plan_2_interest_rate.py +31 -0
- policyengine_uk/variables/gov/hmrc/student_loans/plan_4_interest_rate.py +22 -0
- policyengine_uk/variables/gov/hmrc/student_loans/plan_5_interest_rate.py +18 -0
- policyengine_uk/variables/gov/hmrc/student_loans/postgraduate_interest_rate.py +23 -0
- policyengine_uk/variables/gov/hmrc/student_loans/student_loan_plan.py +27 -0
- policyengine_uk/variables/gov/hmrc/student_loans/student_loan_repayment.py +91 -0
- policyengine_uk/variables/gov/hmrc/student_loans/student_loan_repayment_rate.py +31 -0
- policyengine_uk/variables/gov/hmrc/would_claim_child_benefit.py +5 -1
- policyengine_uk/variables/household/demographic/benunit/benunit_count_adults.py +11 -0
- policyengine_uk/variables/household/demographic/is_disabled_for_benefits.py +13 -1
- policyengine_uk/variables/household/income/hbai_household_net_income.py +29 -1
- policyengine_uk/variables/household/income/hbai_household_net_income_ahc.py +13 -0
- policyengine_uk/variables/household/income/household_net_income.py +5 -1
- policyengine_uk/variables/household/income/inflation_adjustment.py +24 -0
- policyengine_uk/variables/household/post_tax_income.py +12 -0
- policyengine_uk/variables/household/wealth/num_vehicles.py +9 -0
- policyengine_uk/variables/household/wealth/owns_vehicle.py +17 -0
- policyengine_uk/variables/input/consumption/property/council_tax.py +0 -35
- policyengine_uk/variables/input/consumption/property/employee_pension_contributions.py +8 -1
- policyengine_uk/variables/input/consumption/property/employee_pension_contributions_reported.py +16 -0
- policyengine_uk/variables/input/consumption/property/pension_contributions_via_salary_sacrifice.py +16 -0
- policyengine_uk/variables/input/employment_income.py +2 -0
- policyengine_uk/variables/input/rent.py +0 -40
- policyengine_uk/variables/input/savings_interest_income.py +3 -1
- {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/METADATA +17 -8
- {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/RECORD +252 -173
- {policyengine_uk-2.40.1.dist-info → policyengine_uk-2.65.6.dist-info}/WHEEL +1 -1
- policyengine_uk/repo.py +0 -3
- policyengine_uk/tests/policy/baseline/gov/abolitions/abolition_parameters.yaml +0 -250
- policyengine_uk/variables/contrib/policyengine/high_income_incident_tax_change.py +0 -22
- policyengine_uk-2.40.1.data/data/share/openfisca/openfisca-country-template/CHANGELOG.md +0 -2285
- policyengine_uk-2.40.1.data/data/share/openfisca/openfisca-country-template/README.md +0 -37
- policyengine_uk-2.40.1.dist-info/licenses/LICENSE +0 -661
- {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
|
+
)
|