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
policyengine_uk/__init__.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from policyengine_core.taxbenefitsystems import TaxBenefitSystem
|
|
7
|
+
|
|
4
8
|
from policyengine_uk import entities
|
|
5
9
|
from policyengine_uk.system import (
|
|
6
10
|
CountryTaxBenefitSystem,
|
|
@@ -10,8 +14,6 @@ from policyengine_uk.system import (
|
|
|
10
14
|
parameters,
|
|
11
15
|
variables,
|
|
12
16
|
)
|
|
13
|
-
from
|
|
14
|
-
import os
|
|
15
|
-
from policyengine_core.taxbenefitsystems import TaxBenefitSystem
|
|
17
|
+
from .model_api import *
|
|
16
18
|
|
|
17
19
|
REPO = Path(__file__).parent
|
policyengine_uk/data/__init__.py
CHANGED
|
@@ -14,11 +14,17 @@ class UKSingleYearDataset:
|
|
|
14
14
|
household: pd.DataFrame
|
|
15
15
|
|
|
16
16
|
@staticmethod
|
|
17
|
-
def validate_file_path(file_path: str):
|
|
17
|
+
def validate_file_path(file_path: str, raise_exception: bool = True):
|
|
18
18
|
if not file_path.endswith(".h5"):
|
|
19
|
-
|
|
19
|
+
if raise_exception:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"File path must end with '.h5' for UKDataset."
|
|
22
|
+
)
|
|
23
|
+
return False
|
|
20
24
|
if not Path(file_path).exists():
|
|
21
|
-
|
|
25
|
+
if raise_exception:
|
|
26
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
27
|
+
return False
|
|
22
28
|
|
|
23
29
|
# Check if the file contains time_period, person, benunit, and household datasets
|
|
24
30
|
with h5py.File(file_path, "r") as f:
|
|
@@ -30,9 +36,14 @@ class UKSingleYearDataset:
|
|
|
30
36
|
]
|
|
31
37
|
for dataset in required_datasets:
|
|
32
38
|
if dataset not in f:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
if raise_exception:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Dataset '{dataset}' not found in the file: {file_path}"
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
return True
|
|
36
47
|
|
|
37
48
|
def __init__(
|
|
38
49
|
self,
|
|
@@ -42,6 +53,7 @@ class UKSingleYearDataset:
|
|
|
42
53
|
household: pd.DataFrame = None,
|
|
43
54
|
fiscal_year: int = 2025,
|
|
44
55
|
):
|
|
56
|
+
file_path = str(file_path) if file_path else None
|
|
45
57
|
if file_path is not None:
|
|
46
58
|
self.validate_file_path(file_path)
|
|
47
59
|
with pd.HDFStore(file_path) as f:
|
|
@@ -108,9 +120,12 @@ class UKSingleYearDataset:
|
|
|
108
120
|
if simulation.tax_benefit_system.variables[variable].entity.key
|
|
109
121
|
== entity
|
|
110
122
|
]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
if len(input_variables) == 0:
|
|
124
|
+
entity_dfs[entity] = pd.DataFrame()
|
|
125
|
+
else:
|
|
126
|
+
entity_dfs[entity] = simulation.calculate_dataframe(
|
|
127
|
+
input_variables, period=fiscal_year
|
|
128
|
+
)
|
|
114
129
|
|
|
115
130
|
return UKSingleYearDataset(
|
|
116
131
|
person=entity_dfs["person"],
|
|
@@ -137,7 +152,7 @@ class UKMultiYearDataset:
|
|
|
137
152
|
self.datasets[year] = dataset
|
|
138
153
|
|
|
139
154
|
if file_path is not None:
|
|
140
|
-
|
|
155
|
+
UKMultiYearDataset.validate_file_path(file_path)
|
|
141
156
|
with pd.HDFStore(file_path) as f:
|
|
142
157
|
self.datasets = {}
|
|
143
158
|
for year in f.keys():
|
|
@@ -162,6 +177,10 @@ class UKMultiYearDataset:
|
|
|
162
177
|
else:
|
|
163
178
|
raise ValueError(f"No dataset found for year {fiscal_year}.")
|
|
164
179
|
|
|
180
|
+
@property
|
|
181
|
+
def years(self):
|
|
182
|
+
return list(self.datasets.keys())
|
|
183
|
+
|
|
165
184
|
def __getitem__(self, fiscal_year: int):
|
|
166
185
|
return self.get_year(fiscal_year)
|
|
167
186
|
|
|
@@ -203,21 +222,49 @@ class UKMultiYearDataset:
|
|
|
203
222
|
return UKMultiYearDataset(datasets=list(new_datasets.values()))
|
|
204
223
|
|
|
205
224
|
@staticmethod
|
|
206
|
-
def validate_file_path(file_path: str):
|
|
225
|
+
def validate_file_path(file_path: str, raise_exception: bool = False):
|
|
207
226
|
if not file_path.endswith(".h5"):
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
227
|
+
if raise_exception:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
"File path must end with '.h5' for UKMultiYearDataset."
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
return False
|
|
211
233
|
if not Path(file_path).exists():
|
|
212
|
-
|
|
234
|
+
if raise_exception:
|
|
235
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
236
|
+
else:
|
|
237
|
+
return False
|
|
213
238
|
|
|
214
239
|
# Check if the file contains datasets for multiple years
|
|
215
240
|
with h5py.File(file_path, "r") as f:
|
|
216
241
|
for required_dataset in ["person", "benunit", "household"]:
|
|
217
242
|
if not any(f"{required_dataset}" in key for key in f.keys()):
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
243
|
+
if raise_exception:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"Dataset '{required_dataset}' not found in the file: {file_path}"
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
# Check that there is at least one dataset year in the folder (keys include e.g. person/2025)
|
|
251
|
+
|
|
252
|
+
# Check that there is at least one dataset year in the folder
|
|
253
|
+
years_found = False
|
|
254
|
+
for key in f.keys():
|
|
255
|
+
parts = key.split("/")
|
|
256
|
+
if len(parts) >= 2 and required_dataset == parts[0]:
|
|
257
|
+
years_found = True
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
if not years_found:
|
|
261
|
+
if raise_exception:
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"No yearly data found for '{required_dataset}' in file: {file_path}"
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
return False
|
|
267
|
+
return True
|
|
221
268
|
|
|
222
269
|
def load(self):
|
|
223
270
|
data = {}
|
|
@@ -228,3 +275,8 @@ class UKMultiYearDataset:
|
|
|
228
275
|
data[col] = {}
|
|
229
276
|
data[col][year] = df[col].values
|
|
230
277
|
return data
|
|
278
|
+
|
|
279
|
+
def reset_uprating(self):
|
|
280
|
+
from policyengine_uk.data.economic_assumptions import reset_uprating
|
|
281
|
+
|
|
282
|
+
reset_uprating(self)
|
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
from policyengine_uk.data import UKMultiYearDataset, UKSingleYearDataset
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import yaml
|
|
4
4
|
from policyengine_core.parameters import ParameterNode
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import numpy as np
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def extend_single_year_dataset(
|
|
11
|
+
dataset: UKSingleYearDataset,
|
|
12
|
+
tax_benefit_system_parameters: ParameterNode,
|
|
13
|
+
end_year: int = 2030,
|
|
14
|
+
) -> UKMultiYearDataset:
|
|
15
|
+
# Extend years and uprate
|
|
16
|
+
start_year = int(dataset.time_period)
|
|
17
|
+
datasets = [dataset]
|
|
18
|
+
for year in range(start_year, end_year + 1):
|
|
19
|
+
next_year = dataset.copy()
|
|
20
|
+
next_year.time_period = str(year)
|
|
21
|
+
datasets.append(next_year)
|
|
22
|
+
multi_year_dataset = UKMultiYearDataset(datasets=datasets)
|
|
23
|
+
return apply_uprating(
|
|
24
|
+
multi_year_dataset,
|
|
25
|
+
tax_benefit_system_parameters=tax_benefit_system_parameters,
|
|
26
|
+
)
|
|
7
27
|
|
|
8
28
|
|
|
9
29
|
def apply_uprating(
|
|
10
30
|
dataset: UKMultiYearDataset,
|
|
31
|
+
tax_benefit_system_parameters: ParameterNode = None,
|
|
11
32
|
):
|
|
33
|
+
from policyengine_uk.system import system
|
|
34
|
+
|
|
12
35
|
# Apply uprating to the dataset.
|
|
13
36
|
dataset = dataset.copy()
|
|
14
37
|
|
|
@@ -20,7 +43,9 @@ def apply_uprating(
|
|
|
20
43
|
continue # Don't uprate the first year
|
|
21
44
|
current_year = dataset.datasets[year]
|
|
22
45
|
prev_year = dataset.datasets[year - 1]
|
|
23
|
-
apply_single_year_uprating(
|
|
46
|
+
apply_single_year_uprating(
|
|
47
|
+
current_year, prev_year, tax_benefit_system_parameters
|
|
48
|
+
)
|
|
24
49
|
|
|
25
50
|
return dataset
|
|
26
51
|
|
|
@@ -48,7 +73,9 @@ def apply_single_year_uprating(
|
|
|
48
73
|
prev_year_value = getattr(previous_year, table_name)[
|
|
49
74
|
variable
|
|
50
75
|
]
|
|
51
|
-
current_year_value = prev_year_value *
|
|
76
|
+
current_year_value = prev_year_value * (
|
|
77
|
+
1 + index_rel_change
|
|
78
|
+
)
|
|
52
79
|
getattr(current_year, table_name)[
|
|
53
80
|
variable
|
|
54
81
|
] = current_year_value
|
|
@@ -128,9 +155,10 @@ def uprate_rent(
|
|
|
128
155
|
social_rent_growth = growth.obr.social_rent(year)
|
|
129
156
|
|
|
130
157
|
if year < 2022:
|
|
131
|
-
|
|
132
|
-
"Rent uprating is not supported for years before 2022."
|
|
158
|
+
logging.warning(
|
|
159
|
+
"Rent uprating is not supported for years before 2022. Not applying uprating."
|
|
133
160
|
)
|
|
161
|
+
pass
|
|
134
162
|
elif year < 2025:
|
|
135
163
|
# We have regional growth rates for private rent.
|
|
136
164
|
regional_growth_rate = growth.ons.private_rental_prices(year)[
|
|
@@ -158,11 +186,6 @@ def uprate_rent(
|
|
|
158
186
|
private_rent_growth = (
|
|
159
187
|
aggregate_growth - social_weight * social_rent_growth
|
|
160
188
|
) / private_weight
|
|
161
|
-
print(
|
|
162
|
-
f"Backed out private rent growth: {private_rent_growth:.1%} in {year}"
|
|
163
|
-
)
|
|
164
|
-
print(f"OBR aggregate rent growth: {aggregate_growth:.1%} in {year}")
|
|
165
|
-
print(f"Social rent growth: {social_rent_growth:.1%} in {year}")
|
|
166
189
|
|
|
167
190
|
current_year.household["rent"] = np.where(
|
|
168
191
|
is_private_rented,
|
|
@@ -182,3 +205,6 @@ def reset_uprating(
|
|
|
182
205
|
for year in dataset.datasets:
|
|
183
206
|
if year != first_year:
|
|
184
207
|
dataset.datasets[year] = dataset.datasets[first_year].copy()
|
|
208
|
+
dataset.datasets[year].time_period = str(year)
|
|
209
|
+
|
|
210
|
+
return dataset
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
if typing.TYPE_CHECKING:
|
|
4
|
+
from policyengine_uk import Microsimulation
|
|
5
|
+
from policyengine_uk.data import UKSingleYearDataset
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def filter_dataset(
|
|
9
|
+
sim: "Microsimulation", household_id: int, year: int = 2026
|
|
10
|
+
) -> "UKSingleYearDataset":
|
|
11
|
+
from policyengine_uk import Microsimulation
|
|
12
|
+
from policyengine_uk.data import UKSingleYearDataset
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
Extract a single household from a simulation dataset.
|
|
16
|
+
|
|
17
|
+
This function creates a new dataset containing only the specified household
|
|
18
|
+
and the associated benefit units and people within that household.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
sim : Microsimulation
|
|
23
|
+
The microsimulation object containing the dataset.
|
|
24
|
+
household_id : int
|
|
25
|
+
The ID of the household to extract.
|
|
26
|
+
year : int, default 2026
|
|
27
|
+
The dataset year to filter.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
UKSingleYearDataset
|
|
32
|
+
A new dataset containing only data for the specified household.
|
|
33
|
+
"""
|
|
34
|
+
dataset: UKSingleYearDataset = sim.dataset[year]
|
|
35
|
+
new_dataset = dataset.copy()
|
|
36
|
+
new_dataset.person = new_dataset.person[
|
|
37
|
+
new_dataset.person.person_household_id == household_id
|
|
38
|
+
]
|
|
39
|
+
new_dataset.household = new_dataset.household[
|
|
40
|
+
new_dataset.household.household_id == household_id
|
|
41
|
+
]
|
|
42
|
+
benunits = new_dataset.person.person_benunit_id.unique()
|
|
43
|
+
new_dataset.benunit = new_dataset.benunit[
|
|
44
|
+
new_dataset.benunit.benunit_id.isin(benunits)
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
return UKSingleYearDataset(
|
|
48
|
+
person=new_dataset.person,
|
|
49
|
+
household=new_dataset.household,
|
|
50
|
+
benunit=new_dataset.benunit,
|
|
51
|
+
fiscal_year=year,
|
|
52
|
+
)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Labour supply dynamics module for PolicyEngine UK.
|
|
2
|
+
|
|
3
|
+
This module coordinates labour supply responses by combining progression (intensive margin)
|
|
4
|
+
and participation (extensive margin) models. It implements the methodology from the OBR's
|
|
5
|
+
labour supply elasticity framework to estimate how changes in tax and benefit policies
|
|
6
|
+
affect employment and working hours.
|
|
7
|
+
|
|
8
|
+
Reference: https://obr.uk/docs/dlm_uploads/NICS-Cut-Impact-on-Labour-Supply-Note.pdf
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from policyengine_uk import Simulation
|
|
14
|
+
from microdf import MicroDataFrame
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from .progression import (
|
|
19
|
+
calculate_derivative_change,
|
|
20
|
+
calculate_relative_income_change,
|
|
21
|
+
calculate_labour_substitution_elasticities,
|
|
22
|
+
calculate_labour_net_income_elasticities,
|
|
23
|
+
calculate_employment_income_change,
|
|
24
|
+
)
|
|
25
|
+
from .participation import (
|
|
26
|
+
apply_participation_responses,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def calculate_excluded_from_labour_supply_responses(
|
|
31
|
+
sim: Simulation, count_adults: int = 2
|
|
32
|
+
):
|
|
33
|
+
"""Calculate which individuals are excluded from labour supply responses.
|
|
34
|
+
|
|
35
|
+
Excludes self-employed, full-time students, aged 60+, and individuals
|
|
36
|
+
outside the specified adult range.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
sim: PolicyEngine simulation object
|
|
40
|
+
count_adults: Number of adults to include in calculations
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Boolean array indicating which individuals are excluded
|
|
44
|
+
"""
|
|
45
|
+
# Exclude self-employed, full-time students, aged 60+, and adult_index == (0, >= count_adults + 1)
|
|
46
|
+
employment_status = sim.calculate("employment_status")
|
|
47
|
+
self_employed = np.isin(
|
|
48
|
+
employment_status, ["FT_SELF_EMPLOYED", "PT_SELF_EMPLOYED"]
|
|
49
|
+
)
|
|
50
|
+
student = employment_status == "STUDENT"
|
|
51
|
+
age = sim.calculate("age")
|
|
52
|
+
age_60_plus = age >= 60
|
|
53
|
+
adult_index = sim.calculate("adult_index")
|
|
54
|
+
excluded = (
|
|
55
|
+
self_employed
|
|
56
|
+
| student
|
|
57
|
+
| age_60_plus
|
|
58
|
+
| (adult_index == 0)
|
|
59
|
+
| (adult_index >= count_adults + 1)
|
|
60
|
+
)
|
|
61
|
+
return excluded.values
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FTEImpacts(BaseModel):
|
|
65
|
+
"""Data model for FTE impacts of labour supply responses."""
|
|
66
|
+
|
|
67
|
+
substitution_response_ftes: float
|
|
68
|
+
"""FTE impact from substitution effects."""
|
|
69
|
+
income_response_ftes: float
|
|
70
|
+
"""FTE impact from income effects."""
|
|
71
|
+
total_response_ftes: float
|
|
72
|
+
"""Total FTE impact from both substitution and income effects."""
|
|
73
|
+
|
|
74
|
+
participation_response_employment: Optional[float] = None
|
|
75
|
+
"""FTE impact from participation responses, if available."""
|
|
76
|
+
participation_response_ftes: Optional[float] = None
|
|
77
|
+
"""FTE impact from participation responses, if available."""
|
|
78
|
+
|
|
79
|
+
ftes: Optional[float] = None
|
|
80
|
+
"""Total FTE impact across all responses."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class LabourSupplyResponseData(BaseModel):
|
|
84
|
+
progression: pd.DataFrame
|
|
85
|
+
"""DataFrame containing intensive margin (progression) responses."""
|
|
86
|
+
participation: Optional[pd.DataFrame] = None
|
|
87
|
+
"""DataFrame containing extensive margin (participation) responses, if available."""
|
|
88
|
+
|
|
89
|
+
# Specific outputs for comparison with OBR outputs
|
|
90
|
+
fte_impacts: FTEImpacts
|
|
91
|
+
"""FTE impacts of labour supply responses, including both progression and participation."""
|
|
92
|
+
|
|
93
|
+
model_config = {
|
|
94
|
+
"arbitrary_types_allowed": True,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def apply_labour_supply_responses(
|
|
99
|
+
sim: Simulation,
|
|
100
|
+
target_variable: str = "hbai_household_net_income",
|
|
101
|
+
input_variable: str = "employment_income",
|
|
102
|
+
year: int = 2025,
|
|
103
|
+
count_adults: int = 2,
|
|
104
|
+
delta: float = 1_000,
|
|
105
|
+
) -> pd.DataFrame:
|
|
106
|
+
"""Apply labour supply responses to simulation and return the response vector.
|
|
107
|
+
|
|
108
|
+
This is the main function for applying dynamic labour supply responses to
|
|
109
|
+
a PolicyEngine simulation. It coordinates both intensive margin (progression)
|
|
110
|
+
and extensive margin (participation) responses to policy changes.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
sim: PolicyEngine simulation object (should have baseline attribute)
|
|
114
|
+
target_variable: Variable that drives labour supply decisions
|
|
115
|
+
input_variable: Variable representing labour supply (typically employment_income)
|
|
116
|
+
year: Year for calculation
|
|
117
|
+
count_adults: Number of adults to calculate responses for
|
|
118
|
+
delta: Size of change for marginal rate calculation (£)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
DataFrame with labour supply response information
|
|
122
|
+
"""
|
|
123
|
+
follow_obr = sim.tax_benefit_system.parameters.gov.dynamic.obr_labour_supply_assumptions(
|
|
124
|
+
year
|
|
125
|
+
)
|
|
126
|
+
if (not follow_obr) or (sim.baseline is None):
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Calculate income changes using household_net_income
|
|
130
|
+
baseline_income = sim.baseline.calculate(
|
|
131
|
+
target_variable, year, map_to="person"
|
|
132
|
+
)
|
|
133
|
+
reform_income = sim.calculate(target_variable, year, map_to="person")
|
|
134
|
+
|
|
135
|
+
baseline_income = baseline_income
|
|
136
|
+
reform_income = reform_income
|
|
137
|
+
baseline_income = pd.Series(baseline_income, index=reform_income.index)
|
|
138
|
+
|
|
139
|
+
# Calculate relative changes
|
|
140
|
+
income_rel_change = np.where(
|
|
141
|
+
baseline_income != 0,
|
|
142
|
+
(reform_income - baseline_income) / baseline_income,
|
|
143
|
+
0,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Apply intensive margin responses (progression model)
|
|
147
|
+
progression_responses = apply_progression_responses(
|
|
148
|
+
sim=sim,
|
|
149
|
+
target_variable=target_variable,
|
|
150
|
+
input_variable=input_variable,
|
|
151
|
+
year=year,
|
|
152
|
+
count_adults=count_adults,
|
|
153
|
+
delta=delta,
|
|
154
|
+
pre_calculated_income_rel_change=income_rel_change,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Apply extensive margin responses (participation model)
|
|
158
|
+
participation_responses = (
|
|
159
|
+
None # = apply_participation_responses(sim=sim, year=year)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Add FTE impacts to the response data
|
|
163
|
+
fte_impacts = FTEImpacts(
|
|
164
|
+
substitution_response_ftes=progression_responses[
|
|
165
|
+
"substitution_response_ftes"
|
|
166
|
+
].sum(),
|
|
167
|
+
income_response_ftes=progression_responses[
|
|
168
|
+
"income_response_ftes"
|
|
169
|
+
].sum(),
|
|
170
|
+
total_response_ftes=progression_responses["total_response_ftes"].sum(),
|
|
171
|
+
participation_response_employment=(
|
|
172
|
+
participation_responses["participation_change"].sum()
|
|
173
|
+
if participation_responses is not None
|
|
174
|
+
else None
|
|
175
|
+
),
|
|
176
|
+
participation_response_ftes=(
|
|
177
|
+
participation_responses["participation_change_ftes"].sum()
|
|
178
|
+
if participation_responses is not None
|
|
179
|
+
else None
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
fte_impacts.ftes = fte_impacts.total_response_ftes + (
|
|
184
|
+
fte_impacts.participation_response_ftes
|
|
185
|
+
if fte_impacts.participation_response_ftes is not None
|
|
186
|
+
else 0
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# For now, return only progression responses since participation is placeholder
|
|
190
|
+
# TODO: Combine progression and participation responses when participation model is implemented
|
|
191
|
+
return LabourSupplyResponseData(
|
|
192
|
+
progression=progression_responses,
|
|
193
|
+
participation=participation_responses,
|
|
194
|
+
fte_impacts=fte_impacts,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def apply_progression_responses(
|
|
199
|
+
sim: Simulation,
|
|
200
|
+
target_variable: str = "hbai_household_net_income",
|
|
201
|
+
input_variable: str = "employment_income",
|
|
202
|
+
year: int = 2025,
|
|
203
|
+
count_adults: int = 2,
|
|
204
|
+
delta: float = 1_000,
|
|
205
|
+
pre_calculated_income_rel_change: np.ndarray = None,
|
|
206
|
+
) -> pd.DataFrame:
|
|
207
|
+
"""Apply progression (intensive margin) labour supply responses.
|
|
208
|
+
|
|
209
|
+
This function handles the intensive margin of labour supply by calculating
|
|
210
|
+
how individuals adjust their working hours in response to policy changes.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
sim: PolicyEngine simulation object (should have baseline attribute)
|
|
214
|
+
target_variable: Variable that drives labour supply decisions
|
|
215
|
+
input_variable: Variable representing labour supply (typically employment_income)
|
|
216
|
+
year: Year for calculation
|
|
217
|
+
count_adults: Number of adults to calculate responses for
|
|
218
|
+
delta: Size of change for marginal rate calculation (£)
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
DataFrame with progression response information
|
|
222
|
+
"""
|
|
223
|
+
# Calculate changes in marginal rates (drives substitution effects)
|
|
224
|
+
derivative_changes = calculate_derivative_change(
|
|
225
|
+
sim=sim,
|
|
226
|
+
target_variable=target_variable,
|
|
227
|
+
input_variable=input_variable,
|
|
228
|
+
year=year,
|
|
229
|
+
count_adults=count_adults,
|
|
230
|
+
delta=delta,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
derivative_changes = derivative_changes.rename(
|
|
234
|
+
columns={col: f"deriv_{col}" for col in derivative_changes.columns}
|
|
235
|
+
)
|
|
236
|
+
derivative_changes["person_id"] = sim.calculate("person_id", year).values
|
|
237
|
+
|
|
238
|
+
# Add in actual implied wages
|
|
239
|
+
gross_wage = sim.calculate("employment_income", year) / sim.calculate(
|
|
240
|
+
"hours_worked", year
|
|
241
|
+
)
|
|
242
|
+
gross_wage = gross_wage.fillna(0).replace([np.inf, -np.inf], 0)
|
|
243
|
+
derivative_changes["wage_gross"] = gross_wage
|
|
244
|
+
derivative_changes["wage_baseline"] = (
|
|
245
|
+
gross_wage * derivative_changes["deriv_baseline"]
|
|
246
|
+
)
|
|
247
|
+
derivative_changes["wage_scenario"] = (
|
|
248
|
+
gross_wage * derivative_changes["deriv_scenario"]
|
|
249
|
+
)
|
|
250
|
+
derivative_changes["wage_rel_change"] = (
|
|
251
|
+
(
|
|
252
|
+
derivative_changes["wage_scenario"]
|
|
253
|
+
/ derivative_changes["wage_baseline"]
|
|
254
|
+
- 1
|
|
255
|
+
)
|
|
256
|
+
.replace([np.inf, -np.inf, np.nan], 0)
|
|
257
|
+
.fillna(0)
|
|
258
|
+
)
|
|
259
|
+
derivative_changes["wage_abs_change"] = (
|
|
260
|
+
derivative_changes["wage_scenario"]
|
|
261
|
+
- derivative_changes["wage_baseline"]
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Calculate changes in income levels (drives income effects)
|
|
265
|
+
if pre_calculated_income_rel_change is not None:
|
|
266
|
+
n_people = len(sim.calculate("person_id", year))
|
|
267
|
+
income_changes = pd.DataFrame(
|
|
268
|
+
{
|
|
269
|
+
"baseline": np.zeros(n_people),
|
|
270
|
+
"scenario": np.zeros(n_people),
|
|
271
|
+
"rel_change": pre_calculated_income_rel_change,
|
|
272
|
+
"abs_change": np.zeros(n_people),
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
income_changes = calculate_relative_income_change(
|
|
277
|
+
sim, target_variable, year
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
income_changes = income_changes.rename(
|
|
281
|
+
columns={col: f"income_{col}" for col in income_changes.columns}
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
df = pd.concat([derivative_changes, income_changes], axis=1).fillna(0)
|
|
285
|
+
|
|
286
|
+
# Get elasticity parameters by demographic group
|
|
287
|
+
substitution_elasticities = calculate_labour_substitution_elasticities(sim)
|
|
288
|
+
income_elasticities = calculate_labour_net_income_elasticities(sim)
|
|
289
|
+
|
|
290
|
+
df["income_elasticity"] = income_elasticities
|
|
291
|
+
df["substitution_elasticity"] = substitution_elasticities
|
|
292
|
+
|
|
293
|
+
# Get baseline employment income levels
|
|
294
|
+
employment_income = sim.calculate(input_variable, year)
|
|
295
|
+
|
|
296
|
+
df["employment_income"] = employment_income
|
|
297
|
+
df["hours_per_week"] = sim.calculate("hours_worked", year) / 52
|
|
298
|
+
|
|
299
|
+
# Calculate total labour supply response
|
|
300
|
+
response_df = calculate_employment_income_change(
|
|
301
|
+
employment_income=employment_income,
|
|
302
|
+
derivative_changes=derivative_changes,
|
|
303
|
+
income_changes=income_changes,
|
|
304
|
+
substitution_elasticities=substitution_elasticities,
|
|
305
|
+
income_elasticities=income_elasticities,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
df = pd.concat([df, response_df], axis=1)
|
|
309
|
+
|
|
310
|
+
# Apply relative {substitution, income, total} changes to hours as well
|
|
311
|
+
# Apply relative changes to hours using the same factor for all response types
|
|
312
|
+
for response_type in [
|
|
313
|
+
"substitution_response",
|
|
314
|
+
"income_response",
|
|
315
|
+
"total_response",
|
|
316
|
+
]:
|
|
317
|
+
df[f"{response_type}_ftes"] = (
|
|
318
|
+
df[response_type]
|
|
319
|
+
/ df["employment_income"]
|
|
320
|
+
* df["hours_per_week"]
|
|
321
|
+
/ 37.5
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
excluded = calculate_excluded_from_labour_supply_responses(
|
|
325
|
+
sim, count_adults=count_adults
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
for col in df.columns:
|
|
329
|
+
df.loc[excluded, col] = 0
|
|
330
|
+
|
|
331
|
+
df["excluded"] = excluded
|
|
332
|
+
|
|
333
|
+
response = response_df["total_response"].values
|
|
334
|
+
|
|
335
|
+
# Apply the labour supply response to the simulation
|
|
336
|
+
sim.reset_calculations()
|
|
337
|
+
sim.set_input(input_variable, year, employment_income + response)
|
|
338
|
+
|
|
339
|
+
weight = sim.calculate("household_weight", year, map_to="person").values
|
|
340
|
+
|
|
341
|
+
result = MicroDataFrame(df, weights=weight)
|
|
342
|
+
|
|
343
|
+
return result[~result.excluded].drop(columns=["excluded"])
|