Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog_entry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- bump: patch
changes:
fixed:
- Temporarily disabled OBR participation responses.
16 changes: 9 additions & 7 deletions policyengine_uk/dynamics/labour_supply.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def calculate_excluded_from_labour_supply_responses(
| (adult_index == 0)
| (adult_index >= count_adults + 1)
)
return excluded
return excluded.values


class FTEImpacts(BaseModel):
Expand Down Expand Up @@ -132,8 +132,9 @@ def apply_labour_supply_responses(
)
reform_income = sim.calculate(target_variable, year, map_to="person")

baseline_income = baseline_income.values
reform_income = reform_income.values
baseline_income = baseline_income
reform_income = reform_income
baseline_income = pd.Series(baseline_income, index=reform_income.index)

# Calculate relative changes
income_rel_change = np.where(
Expand All @@ -154,7 +155,9 @@ def apply_labour_supply_responses(
)

# Apply extensive margin responses (participation model)
participation_responses = apply_participation_responses(sim=sim, year=year)
participation_responses = (
None # = apply_participation_responses(sim=sim, year=year)
)

# Add FTE impacts to the response data
fte_impacts = FTEImpacts(
Expand Down Expand Up @@ -336,11 +339,10 @@ def apply_progression_responses(
response = response_df["total_response"].values

# Apply the labour supply response to the simulation
# NOTE: Don't reset calculations as this breaks UC and other benefit calculations
# Instead, just update the employment income with the behavioral response
sim.reset_calculations()
sim.set_input(input_variable, year, employment_income + response)

weight = sim.calculate("household_weight", year, map_to="person")
weight = sim.calculate("household_weight", year, map_to="person").values

result = MicroDataFrame(df, weights=weight)

Expand Down
10 changes: 9 additions & 1 deletion policyengine_uk/dynamics/progression.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def calculate_derivative(
Array of marginal rates clipped between 0 and 1
"""
# Get baseline values for input variable and identify adults
input_variable_values = sim.calculate(input_variable, year)
input_variable_values = sim.calculate(input_variable, year).copy()
adult_index = sim.calculate("adult_index")
entity_key = sim.tax_benefit_system.variables[input_variable].entity.key

Expand Down Expand Up @@ -67,6 +67,10 @@ def calculate_derivative(
~pd.Series(adult_index).isin(range(1, count_adults + 1))
] = np.nan

# Reset simulation to original state
sim.reset_calculations()
sim.set_input(input_variable, year, input_variable_values)

# Clip to ensure rates are between 0 and 1 (0% to 100% retention)
return rel_marginal_wages.clip(0, 1)

Expand Down Expand Up @@ -96,6 +100,9 @@ def calculate_relative_income_change(
reformed_target_values = sim.calculate(
target_variable, year, map_to="person"
)
original_target_values = pd.Series(
original_target_values, index=reformed_target_values.index
)

# Calculate relative change, handling division by zero
rel_change = (
Expand Down Expand Up @@ -159,6 +166,7 @@ def calculate_derivative_change(
count_adults=count_adults,
delta=delta,
)
original_deriv = pd.Series(original_deriv, index=reformed_deriv.index)

# Calculate relative and absolute changes in marginal rates
rel_change = reformed_deriv / original_deriv - 1
Expand Down
67 changes: 8 additions & 59 deletions policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,16 @@
class is_benefit_cap_exempt(Variable):
value_type = bool
entity = BenUnit
label = "Whether exempt from the benefits cap"
label = (
"Whether exempt from the benefits cap because of health or disability"
)
definition_period = YEAR
reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected"

def formula(benunit, period, parameters):
# Check if anyone in benefit unit is over state pension age
person = benunit.members
over_pension_age = person("is_SP_age", period)
has_pensioner = benunit.any(over_pension_age)

# UC-specific exemptions
# Limited capability for work and work-related activity
has_lcwra = benunit.any(
person("uc_limited_capability_for_WRA", period)
)

# Carer element in UC indicates caring for someone with disability
gets_uc_carer_element = benunit("uc_carer_element", period) > 0

# Earnings exemption for UC (£846/month = £10,152/year)
# Note: Only check earned income, not UC amount itself to avoid circular dependency
uc_earned = benunit("uc_earned_income", period)
earnings_threshold = 10_152
meets_earnings_test = uc_earned >= earnings_threshold

# Disability and carer benefits that exempt from cap
QUAL_PERSONAL_BENEFITS = [
"attendance_allowance",
"carers_allowance",
"dla", # Disability Living Allowance (includes components)
"pip_dl", # PIP daily living component
"pip_m", # PIP mobility component
"iidb", # Industrial injuries disability benefit
]

# ESA and Working Tax Credit
QUAL_BENUNIT_BENEFITS = [
"esa_income", # Income-based ESA
"working_tax_credit", # If getting WTC, likely working enough
]

qualifying_personal_benefits = add(
benunit, period, QUAL_PERSONAL_BENEFITS
)
qualifying_benunit_benefits = add(
benunit, period, QUAL_BENUNIT_BENEFITS
)

# Check for Armed Forces Compensation Scheme payments
afcs = benunit("afcs", period) > 0

# ESA contribution-based with support component
esa_support_component = benunit("esa_contrib", period) > 0

return (
has_pensioner
| has_lcwra
| gets_uc_carer_element
| meets_earnings_test
| (qualifying_personal_benefits > 0)
| (qualifying_benunit_benefits > 0)
| afcs
| esa_support_component
exempt_health = benunit(
"is_benefit_cap_exempt_health_disability", period
)
exempt_other = benunit("is_benefit_cap_exempt_other", period)
exempt_earnings = benunit("is_benefit_cap_exempt_earnings", period)
return exempt_health | exempt_earnings | exempt_other
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from policyengine_uk.model_api import *


class is_benefit_cap_exempt_earnings(Variable):
value_type = bool
entity = BenUnit
label = "Whether exempt from the benefits cap for non-health/disability reasons"
definition_period = YEAR
reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected"

def formula(benunit, period, parameters):
# Check if anyone in benefit unit is over state pension age
person = benunit.members
over_pension_age = person("is_SP_age", period)
has_pensioner = benunit.any(over_pension_age)

# UC-specific exemptions
# Limited capability for work and work-related activity
has_lcwra = benunit.any(
person("uc_limited_capability_for_WRA", period)
)

# Carer element in UC indicates caring for someone with disability
gets_uc_carer_element = benunit("uc_carer_element", period) > 0

# Earnings exemption for UC (£846/month = £10,152/year)
# Note: Only check earned income, not UC amount itself to avoid circular dependency
uc_earned = benunit.sum(
benunit.members("employment_income", period)
+ benunit.members("self_employment_income", period)
- benunit.members("income_tax", period)
- benunit.members("national_insurance", period)
)
earnings_threshold = 10_152
meets_earnings_test = uc_earned >= earnings_threshold

# Disability and carer benefits that exempt from cap
QUAL_PERSONAL_BENEFITS = [
"attendance_allowance",
"carers_allowance",
"dla", # Disability Living Allowance (includes components)
"pip_dl", # PIP daily living component
"pip_m", # PIP mobility component
"iidb", # Industrial injuries disability benefit
]

# ESA and Working Tax Credit
QUAL_BENUNIT_BENEFITS = [
"esa_income", # Income-based ESA
"working_tax_credit", # If getting WTC, likely working enough
]

qualifying_personal_benefits = add(
benunit, period, QUAL_PERSONAL_BENEFITS
)
qualifying_benunit_benefits = add(
benunit, period, QUAL_BENUNIT_BENEFITS
)

# Check for Armed Forces Compensation Scheme payments
afcs = benunit("afcs", period) > 0

# ESA contribution-based with support component
esa_support_component = benunit("esa_contrib", period) > 0

return meets_earnings_test
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from policyengine_uk.model_api import *


class is_benefit_cap_exempt_health_disability(Variable):
value_type = bool
entity = BenUnit
label = (
"Whether exempt from the benefits cap because of health or disability"
)
definition_period = YEAR
reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected"

def formula(benunit, period, parameters):
# Check if anyone in benefit unit is over state pension age
person = benunit.members
over_pension_age = person("is_SP_age", period)
has_pensioner = benunit.any(over_pension_age)

# UC-specific exemptions
# Limited capability for work and work-related activity
has_lcwra = benunit.any(
person("uc_limited_capability_for_WRA", period)
)

# Carer element in UC indicates caring for someone with disability
gets_uc_carer_element = benunit("uc_carer_element", period) > 0

# Earnings exemption for UC (£846/month = £10,152/year)
# Note: Only check earned income, not UC amount itself to avoid circular dependency
uc_earned = benunit.sum(
benunit.members("employment_income", period)
+ benunit.members("self_employment_income", period)
- benunit.members("income_tax", period)
- benunit.members("national_insurance", period)
)
earnings_threshold = 10_152
meets_earnings_test = uc_earned >= earnings_threshold

# Disability and carer benefits that exempt from cap
QUAL_PERSONAL_BENEFITS = [
"attendance_allowance",
"carers_allowance",
"dla", # Disability Living Allowance (includes components)
"pip_dl", # PIP daily living component
"pip_m", # PIP mobility component
"iidb", # Industrial injuries disability benefit
]

# ESA and Working Tax Credit
QUAL_BENUNIT_BENEFITS = [
"esa_income", # Income-based ESA
"working_tax_credit", # If getting WTC, likely working enough
]

qualifying_personal_benefits = add(
benunit, period, QUAL_PERSONAL_BENEFITS
)
qualifying_benunit_benefits = add(
benunit, period, QUAL_BENUNIT_BENEFITS
)

# Check for Armed Forces Compensation Scheme payments
afcs = benunit("afcs", period) > 0

# ESA contribution-based with support component
esa_support_component = benunit("esa_contrib", period) > 0

return (
has_lcwra
| gets_uc_carer_element
| (qualifying_personal_benefits > 0)
| (qualifying_benunit_benefits > 0)
| afcs
| esa_support_component
)
66 changes: 66 additions & 0 deletions policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from policyengine_uk.model_api import *


class is_benefit_cap_exempt_other(Variable):
value_type = bool
entity = BenUnit
label = "Whether exempt from the benefits cap for non-health/disability reasons"
definition_period = YEAR
reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected"

def formula(benunit, period, parameters):
# Check if anyone in benefit unit is over state pension age
person = benunit.members
over_pension_age = person("is_SP_age", period)
has_pensioner = benunit.any(over_pension_age)

# UC-specific exemptions
# Limited capability for work and work-related activity
has_lcwra = benunit.any(
person("uc_limited_capability_for_WRA", period)
)

# Carer element in UC indicates caring for someone with disability
gets_uc_carer_element = benunit("uc_carer_element", period) > 0

# Earnings exemption for UC (£846/month = £10,152/year)
# Note: Only check earned income, not UC amount itself to avoid circular dependency
uc_earned = benunit.sum(
benunit.members("employment_income", period)
+ benunit.members("self_employment_income", period)
- benunit.members("income_tax", period)
- benunit.members("national_insurance", period)
)
earnings_threshold = 10_152
meets_earnings_test = uc_earned >= earnings_threshold

# Disability and carer benefits that exempt from cap
QUAL_PERSONAL_BENEFITS = [
"attendance_allowance",
"carers_allowance",
"dla", # Disability Living Allowance (includes components)
"pip_dl", # PIP daily living component
"pip_m", # PIP mobility component
"iidb", # Industrial injuries disability benefit
]

# ESA and Working Tax Credit
QUAL_BENUNIT_BENEFITS = [
"esa_income", # Income-based ESA
"working_tax_credit", # If getting WTC, likely working enough
]

qualifying_personal_benefits = add(
benunit, period, QUAL_PERSONAL_BENEFITS
)
qualifying_benunit_benefits = add(
benunit, period, QUAL_BENUNIT_BENEFITS
)

# Check for Armed Forces Compensation Scheme payments
afcs = benunit("afcs", period) > 0

# ESA contribution-based with support component
esa_support_component = benunit("esa_contrib", period) > 0

return has_pensioner | afcs | esa_support_component