Skip to content

Commit 5ecd52f

Browse files
Temporarily disable OBR participation responses (#1347)
* Fix benefit cap exemption logic * Format * Fix circular dependency * Update tests * Fix bug in LCWRA calculation * Add changes * Fix LSR issues * Versioniong * Format
1 parent ab13323 commit 5ecd52f

7 files changed

Lines changed: 237 additions & 67 deletions

File tree

changelog_entry.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- bump: patch
2+
changes:
3+
fixed:
4+
- Temporarily disabled OBR participation responses.

policyengine_uk/dynamics/labour_supply.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def calculate_excluded_from_labour_supply_responses(
5858
| (adult_index == 0)
5959
| (adult_index >= count_adults + 1)
6060
)
61-
return excluded
61+
return excluded.values
6262

6363

6464
class FTEImpacts(BaseModel):
@@ -132,8 +132,9 @@ def apply_labour_supply_responses(
132132
)
133133
reform_income = sim.calculate(target_variable, year, map_to="person")
134134

135-
baseline_income = baseline_income.values
136-
reform_income = reform_income.values
135+
baseline_income = baseline_income
136+
reform_income = reform_income
137+
baseline_income = pd.Series(baseline_income, index=reform_income.index)
137138

138139
# Calculate relative changes
139140
income_rel_change = np.where(
@@ -154,7 +155,9 @@ def apply_labour_supply_responses(
154155
)
155156

156157
# Apply extensive margin responses (participation model)
157-
participation_responses = apply_participation_responses(sim=sim, year=year)
158+
participation_responses = (
159+
None # = apply_participation_responses(sim=sim, year=year)
160+
)
158161

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

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

343-
weight = sim.calculate("household_weight", year, map_to="person")
345+
weight = sim.calculate("household_weight", year, map_to="person").values
344346

345347
result = MicroDataFrame(df, weights=weight)
346348

policyengine_uk/dynamics/progression.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def calculate_derivative(
3838
Array of marginal rates clipped between 0 and 1
3939
"""
4040
# Get baseline values for input variable and identify adults
41-
input_variable_values = sim.calculate(input_variable, year)
41+
input_variable_values = sim.calculate(input_variable, year).copy()
4242
adult_index = sim.calculate("adult_index")
4343
entity_key = sim.tax_benefit_system.variables[input_variable].entity.key
4444

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

70+
# Reset simulation to original state
71+
sim.reset_calculations()
72+
sim.set_input(input_variable, year, input_variable_values)
73+
7074
# Clip to ensure rates are between 0 and 1 (0% to 100% retention)
7175
return rel_marginal_wages.clip(0, 1)
7276

@@ -96,6 +100,9 @@ def calculate_relative_income_change(
96100
reformed_target_values = sim.calculate(
97101
target_variable, year, map_to="person"
98102
)
103+
original_target_values = pd.Series(
104+
original_target_values, index=reformed_target_values.index
105+
)
99106

100107
# Calculate relative change, handling division by zero
101108
rel_change = (
@@ -159,6 +166,7 @@ def calculate_derivative_change(
159166
count_adults=count_adults,
160167
delta=delta,
161168
)
169+
original_deriv = pd.Series(original_deriv, index=reformed_deriv.index)
162170

163171
# Calculate relative and absolute changes in marginal rates
164172
rel_change = reformed_deriv / original_deriv - 1

policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py

Lines changed: 8 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,67 +4,16 @@
44
class is_benefit_cap_exempt(Variable):
55
value_type = bool
66
entity = BenUnit
7-
label = "Whether exempt from the benefits cap"
7+
label = (
8+
"Whether exempt from the benefits cap because of health or disability"
9+
)
810
definition_period = YEAR
911
reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected"
1012

1113
def formula(benunit, period, parameters):
12-
# Check if anyone in benefit unit is over state pension age
13-
person = benunit.members
14-
over_pension_age = person("is_SP_age", period)
15-
has_pensioner = benunit.any(over_pension_age)
16-
17-
# UC-specific exemptions
18-
# Limited capability for work and work-related activity
19-
has_lcwra = benunit.any(
20-
person("uc_limited_capability_for_WRA", period)
21-
)
22-
23-
# Carer element in UC indicates caring for someone with disability
24-
gets_uc_carer_element = benunit("uc_carer_element", period) > 0
25-
26-
# Earnings exemption for UC (£846/month = £10,152/year)
27-
# Note: Only check earned income, not UC amount itself to avoid circular dependency
28-
uc_earned = benunit("uc_earned_income", period)
29-
earnings_threshold = 10_152
30-
meets_earnings_test = uc_earned >= earnings_threshold
31-
32-
# Disability and carer benefits that exempt from cap
33-
QUAL_PERSONAL_BENEFITS = [
34-
"attendance_allowance",
35-
"carers_allowance",
36-
"dla", # Disability Living Allowance (includes components)
37-
"pip_dl", # PIP daily living component
38-
"pip_m", # PIP mobility component
39-
"iidb", # Industrial injuries disability benefit
40-
]
41-
42-
# ESA and Working Tax Credit
43-
QUAL_BENUNIT_BENEFITS = [
44-
"esa_income", # Income-based ESA
45-
"working_tax_credit", # If getting WTC, likely working enough
46-
]
47-
48-
qualifying_personal_benefits = add(
49-
benunit, period, QUAL_PERSONAL_BENEFITS
50-
)
51-
qualifying_benunit_benefits = add(
52-
benunit, period, QUAL_BENUNIT_BENEFITS
53-
)
54-
55-
# Check for Armed Forces Compensation Scheme payments
56-
afcs = benunit("afcs", period) > 0
57-
58-
# ESA contribution-based with support component
59-
esa_support_component = benunit("esa_contrib", period) > 0
60-
61-
return (
62-
has_pensioner
63-
| has_lcwra
64-
| gets_uc_carer_element
65-
| meets_earnings_test
66-
| (qualifying_personal_benefits > 0)
67-
| (qualifying_benunit_benefits > 0)
68-
| afcs
69-
| esa_support_component
14+
exempt_health = benunit(
15+
"is_benefit_cap_exempt_health_disability", period
7016
)
17+
exempt_other = benunit("is_benefit_cap_exempt_other", period)
18+
exempt_earnings = benunit("is_benefit_cap_exempt_earnings", period)
19+
return exempt_health | exempt_earnings | exempt_other
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from policyengine_uk.model_api import *
2+
3+
4+
class is_benefit_cap_exempt_earnings(Variable):
5+
value_type = bool
6+
entity = BenUnit
7+
label = "Whether exempt from the benefits cap for non-health/disability reasons"
8+
definition_period = YEAR
9+
reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected"
10+
11+
def formula(benunit, period, parameters):
12+
# Check if anyone in benefit unit is over state pension age
13+
person = benunit.members
14+
over_pension_age = person("is_SP_age", period)
15+
has_pensioner = benunit.any(over_pension_age)
16+
17+
# UC-specific exemptions
18+
# Limited capability for work and work-related activity
19+
has_lcwra = benunit.any(
20+
person("uc_limited_capability_for_WRA", period)
21+
)
22+
23+
# Carer element in UC indicates caring for someone with disability
24+
gets_uc_carer_element = benunit("uc_carer_element", period) > 0
25+
26+
# Earnings exemption for UC (£846/month = £10,152/year)
27+
# Note: Only check earned income, not UC amount itself to avoid circular dependency
28+
uc_earned = benunit.sum(
29+
benunit.members("employment_income", period)
30+
+ benunit.members("self_employment_income", period)
31+
- benunit.members("income_tax", period)
32+
- benunit.members("national_insurance", period)
33+
)
34+
earnings_threshold = 10_152
35+
meets_earnings_test = uc_earned >= earnings_threshold
36+
37+
# Disability and carer benefits that exempt from cap
38+
QUAL_PERSONAL_BENEFITS = [
39+
"attendance_allowance",
40+
"carers_allowance",
41+
"dla", # Disability Living Allowance (includes components)
42+
"pip_dl", # PIP daily living component
43+
"pip_m", # PIP mobility component
44+
"iidb", # Industrial injuries disability benefit
45+
]
46+
47+
# ESA and Working Tax Credit
48+
QUAL_BENUNIT_BENEFITS = [
49+
"esa_income", # Income-based ESA
50+
"working_tax_credit", # If getting WTC, likely working enough
51+
]
52+
53+
qualifying_personal_benefits = add(
54+
benunit, period, QUAL_PERSONAL_BENEFITS
55+
)
56+
qualifying_benunit_benefits = add(
57+
benunit, period, QUAL_BENUNIT_BENEFITS
58+
)
59+
60+
# Check for Armed Forces Compensation Scheme payments
61+
afcs = benunit("afcs", period) > 0
62+
63+
# ESA contribution-based with support component
64+
esa_support_component = benunit("esa_contrib", period) > 0
65+
66+
return meets_earnings_test
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from policyengine_uk.model_api import *
2+
3+
4+
class is_benefit_cap_exempt_health_disability(Variable):
5+
value_type = bool
6+
entity = BenUnit
7+
label = (
8+
"Whether exempt from the benefits cap because of health or disability"
9+
)
10+
definition_period = YEAR
11+
reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected"
12+
13+
def formula(benunit, period, parameters):
14+
# Check if anyone in benefit unit is over state pension age
15+
person = benunit.members
16+
over_pension_age = person("is_SP_age", period)
17+
has_pensioner = benunit.any(over_pension_age)
18+
19+
# UC-specific exemptions
20+
# Limited capability for work and work-related activity
21+
has_lcwra = benunit.any(
22+
person("uc_limited_capability_for_WRA", period)
23+
)
24+
25+
# Carer element in UC indicates caring for someone with disability
26+
gets_uc_carer_element = benunit("uc_carer_element", period) > 0
27+
28+
# Earnings exemption for UC (£846/month = £10,152/year)
29+
# Note: Only check earned income, not UC amount itself to avoid circular dependency
30+
uc_earned = benunit.sum(
31+
benunit.members("employment_income", period)
32+
+ benunit.members("self_employment_income", period)
33+
- benunit.members("income_tax", period)
34+
- benunit.members("national_insurance", period)
35+
)
36+
earnings_threshold = 10_152
37+
meets_earnings_test = uc_earned >= earnings_threshold
38+
39+
# Disability and carer benefits that exempt from cap
40+
QUAL_PERSONAL_BENEFITS = [
41+
"attendance_allowance",
42+
"carers_allowance",
43+
"dla", # Disability Living Allowance (includes components)
44+
"pip_dl", # PIP daily living component
45+
"pip_m", # PIP mobility component
46+
"iidb", # Industrial injuries disability benefit
47+
]
48+
49+
# ESA and Working Tax Credit
50+
QUAL_BENUNIT_BENEFITS = [
51+
"esa_income", # Income-based ESA
52+
"working_tax_credit", # If getting WTC, likely working enough
53+
]
54+
55+
qualifying_personal_benefits = add(
56+
benunit, period, QUAL_PERSONAL_BENEFITS
57+
)
58+
qualifying_benunit_benefits = add(
59+
benunit, period, QUAL_BENUNIT_BENEFITS
60+
)
61+
62+
# Check for Armed Forces Compensation Scheme payments
63+
afcs = benunit("afcs", period) > 0
64+
65+
# ESA contribution-based with support component
66+
esa_support_component = benunit("esa_contrib", period) > 0
67+
68+
return (
69+
has_lcwra
70+
| gets_uc_carer_element
71+
| (qualifying_personal_benefits > 0)
72+
| (qualifying_benunit_benefits > 0)
73+
| afcs
74+
| esa_support_component
75+
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from policyengine_uk.model_api import *
2+
3+
4+
class is_benefit_cap_exempt_other(Variable):
5+
value_type = bool
6+
entity = BenUnit
7+
label = "Whether exempt from the benefits cap for non-health/disability reasons"
8+
definition_period = YEAR
9+
reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected"
10+
11+
def formula(benunit, period, parameters):
12+
# Check if anyone in benefit unit is over state pension age
13+
person = benunit.members
14+
over_pension_age = person("is_SP_age", period)
15+
has_pensioner = benunit.any(over_pension_age)
16+
17+
# UC-specific exemptions
18+
# Limited capability for work and work-related activity
19+
has_lcwra = benunit.any(
20+
person("uc_limited_capability_for_WRA", period)
21+
)
22+
23+
# Carer element in UC indicates caring for someone with disability
24+
gets_uc_carer_element = benunit("uc_carer_element", period) > 0
25+
26+
# Earnings exemption for UC (£846/month = £10,152/year)
27+
# Note: Only check earned income, not UC amount itself to avoid circular dependency
28+
uc_earned = benunit.sum(
29+
benunit.members("employment_income", period)
30+
+ benunit.members("self_employment_income", period)
31+
- benunit.members("income_tax", period)
32+
- benunit.members("national_insurance", period)
33+
)
34+
earnings_threshold = 10_152
35+
meets_earnings_test = uc_earned >= earnings_threshold
36+
37+
# Disability and carer benefits that exempt from cap
38+
QUAL_PERSONAL_BENEFITS = [
39+
"attendance_allowance",
40+
"carers_allowance",
41+
"dla", # Disability Living Allowance (includes components)
42+
"pip_dl", # PIP daily living component
43+
"pip_m", # PIP mobility component
44+
"iidb", # Industrial injuries disability benefit
45+
]
46+
47+
# ESA and Working Tax Credit
48+
QUAL_BENUNIT_BENEFITS = [
49+
"esa_income", # Income-based ESA
50+
"working_tax_credit", # If getting WTC, likely working enough
51+
]
52+
53+
qualifying_personal_benefits = add(
54+
benunit, period, QUAL_PERSONAL_BENEFITS
55+
)
56+
qualifying_benunit_benefits = add(
57+
benunit, period, QUAL_BENUNIT_BENEFITS
58+
)
59+
60+
# Check for Armed Forces Compensation Scheme payments
61+
afcs = benunit("afcs", period) > 0
62+
63+
# ESA contribution-based with support component
64+
esa_support_component = benunit("esa_contrib", period) > 0
65+
66+
return has_pensioner | afcs | esa_support_component

0 commit comments

Comments
 (0)