Skip to content

Commit c491858

Browse files
authored
Merge pull request #1474 from PolicyEngine/sc_cb_reform-
Add Scottish Child Payment baby bonus reform
2 parents f804282 + 451e5b1 commit c491858

10 files changed

Lines changed: 539 additions & 11 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: minor
2+
changes:
3+
added:
4+
- Scottish Child Payment baby bonus reform.

docs/book/validation/student-loan-repayments.ipynb

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,14 @@
7070
"outputs": [],
7171
"source": [
7272
"# Plan distribution (weighted)\n",
73-
"plan_names = {0: \"None\", 1: \"Plan 1\", 2: \"Plan 2\", 3: \"Postgraduate\", 4: \"Plan 4\", 5: \"Plan 5\"}\n",
73+
"plan_names = {\n",
74+
" 0: \"None\",\n",
75+
" 1: \"Plan 1\",\n",
76+
" 2: \"Plan 2\",\n",
77+
" 3: \"Postgraduate\",\n",
78+
" 4: \"Plan 4\",\n",
79+
" 5: \"Plan 5\",\n",
80+
"}\n",
7481
"for plan_id, name in plan_names.items():\n",
7582
" count = weight[plan == plan_id].sum() / 1e6\n",
7683
" print(f\"{name}: {count:.2f}m people\")"
@@ -119,16 +126,22 @@
119126
"\n",
120127
"if has_reported.sum() > 0:\n",
121128
" # Correlation\n",
122-
" correlation = np.corrcoef(reported[has_reported], modelled[has_reported])[0, 1]\n",
129+
" correlation = np.corrcoef(reported[has_reported], modelled[has_reported])[\n",
130+
" 0, 1\n",
131+
" ]\n",
123132
" print(f\"Correlation (people with reported > 0): {correlation:.3f}\")\n",
124-
" \n",
133+
"\n",
125134
" # Match rate\n",
126135
" both_positive = (reported > 0) & (modelled > 0)\n",
127136
" match_rate = both_positive.sum() / has_reported.sum() * 100\n",
128-
" print(f\"People with both reported & modelled > 0: {match_rate:.1f}% of reporters\")\n",
129-
" \n",
137+
" print(\n",
138+
" f\"People with both reported & modelled > 0: {match_rate:.1f}% of reporters\"\n",
139+
" )\n",
140+
"\n",
130141
" # Mean values\n",
131-
" print(f\"\\nMean reported (reporters): £{reported[has_reported].mean():,.0f}\")\n",
142+
" print(\n",
143+
" f\"\\nMean reported (reporters): £{reported[has_reported].mean():,.0f}\"\n",
144+
" )\n",
132145
" print(f\"Mean modelled (reporters): £{modelled[has_reported].mean():,.0f}\")\n",
133146
" print(f\"Mean income (reporters): £{income[has_reported].mean():,.0f}\")"
134147
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
description: Additional weekly amount for Scottish Child Payment based on child age.
2+
brackets:
3+
- threshold:
4+
values:
5+
0001-01-01: 0
6+
amount:
7+
values:
8+
0001-01-01: 12.85
9+
- threshold:
10+
values:
11+
0001-01-01: 1
12+
amount:
13+
values:
14+
0001-01-01: 0
15+
metadata:
16+
type: single_amount
17+
threshold_unit: year
18+
amount_unit: currency-GBP
19+
period: week
20+
label: Scottish Child Payment baby bonus by age
21+
reference:
22+
- title: Scottish Government - Scottish Child Payment
23+
href: https://www.gov.scot/policies/social-security/scottish-child-payment/
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
description: Whether the Scottish Child Payment baby bonus reform is in effect.
2+
values:
3+
0001-01-01: false
4+
metadata:
5+
unit: bool
6+
period: year
7+
label: Scottish Child Payment baby bonus reform in effect

policyengine_uk/reforms/reforms.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
disable_simulated_benefits,
55
)
66
from .policyengine.adjust_budgets import adjust_budgets
7+
from .scotland import create_scottish_child_payment_reform
78
from policyengine_core.model_api import *
89
from policyengine_core import periods
910

@@ -15,6 +16,7 @@ def create_structural_reforms_from_parameters(parameters, period):
1516
create_household_based_hitc_reform(parameters, period),
1617
disable_simulated_benefits(parameters, period),
1718
adjust_budgets(parameters, period),
19+
create_scottish_child_payment_reform(parameters, period),
1820
]
1921
reforms = tuple(filter(lambda x: x is not None, reforms))
2022

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .scottish_child_payment_reform import create_scottish_child_payment_reform
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
from policyengine_uk.model_api import *
2+
from policyengine_core.periods import period as period_
3+
4+
5+
def create_scottish_child_payment_baby_bonus_reform() -> Reform:
6+
"""
7+
Reform that implements SCP Premium for under-ones.
8+
9+
Policy: Children under 1 receive a FIXED £40/week total payment.
10+
Children 1+ receive the standard SCP rate (inflates with inflation).
11+
12+
This is NOT a fixed bonus added to the base - it's a fixed total amount.
13+
As the base SCP rate inflates, the "bonus" for under-1s effectively
14+
decreases to maintain the £40 total.
15+
16+
Source: Scottish Budget 2026-27
17+
https://www.gov.scot/publications/scottish-budget-2026-2027-finance-secretarys-statement-13-january-2026-2/
18+
"""
19+
20+
class scottish_child_payment(Variable):
21+
label = "Scottish Child Payment"
22+
documentation = (
23+
"Scottish Child Payment provides financial support to low-income "
24+
"families in Scotland. It is paid per eligible child to families "
25+
"receiving qualifying benefits such as Universal Credit."
26+
)
27+
entity = BenUnit
28+
definition_period = YEAR
29+
value_type = float
30+
unit = GBP
31+
reference = [
32+
"https://www.gov.scot/policies/social-security/scottish-child-payment/",
33+
"https://www.socialsecurity.gov.scot/",
34+
]
35+
36+
def formula(benunit, period, parameters):
37+
# Check if household is in Scotland
38+
in_scotland = (
39+
benunit.household("country", period).decode_to_str()
40+
== "SCOTLAND"
41+
)
42+
43+
# Get SCP parameters
44+
p = parameters(
45+
period
46+
).gov.social_security_scotland.scottish_child_payment
47+
weekly_amount = p.amount
48+
49+
# SCP only available when amount > 0 (i.e., after Feb 2021)
50+
scp_available = weekly_amount > 0
51+
52+
# Count eligible children in the benefit unit
53+
is_eligible_child = benunit.members(
54+
"is_scp_eligible_child", period
55+
)
56+
eligible_children = benunit.sum(is_eligible_child)
57+
58+
# Get ages for baby bonus calculation
59+
age = benunit.members("age", period)
60+
61+
# Count children under 6 and 6+ for takeup rate calculation
62+
is_child = benunit.members("is_child", period)
63+
children_6_and_over = benunit.sum(
64+
is_child & (age >= 6) & (age < 16)
65+
)
66+
67+
# Check if receiving a qualifying benefit
68+
qb = p.qualifying_benefits
69+
70+
receives_uc = (
71+
benunit("universal_credit", period) > 0
72+
) & qb.universal_credit
73+
receives_ctc = (
74+
benunit("child_tax_credit", period) > 0
75+
) & qb.child_tax_credit
76+
receives_wtc = (
77+
benunit("working_tax_credit", period) > 0
78+
) & qb.working_tax_credit
79+
receives_income_support = (
80+
benunit("income_support", period) > 0
81+
) & qb.income_support
82+
receives_jsa_income = (
83+
benunit("jsa_income", period) > 0
84+
) & qb.jsa_income
85+
receives_esa_income = (
86+
benunit("esa_income", period) > 0
87+
) & qb.esa_income
88+
receives_pension_credit = (
89+
benunit("pension_credit", period) > 0
90+
) & qb.pension_credit
91+
92+
receives_qualifying_benefit = (
93+
receives_uc
94+
| receives_ctc
95+
| receives_wtc
96+
| receives_income_support
97+
| receives_jsa_income
98+
| receives_esa_income
99+
| receives_pension_credit
100+
)
101+
102+
# SCP Premium for under-ones: Fixed £40/week total (not base + bonus)
103+
# Policy: Children under 1 get £40/week, children 1+ get standard rate
104+
PREMIUM_RATE_UNDER_ONE = 40.0 # £40/week fixed total
105+
106+
# Calculate per-child weekly amount based on age
107+
per_child_weekly = where(
108+
age < 1,
109+
PREMIUM_RATE_UNDER_ONE, # £40/week for under-1s (TOTAL, not bonus)
110+
weekly_amount, # Standard SCP rate for 1+ (inflates with inflation)
111+
)
112+
113+
# Calculate total weekly payment for all eligible children
114+
total_weekly = benunit.sum(per_child_weekly * is_eligible_child)
115+
116+
# Convert to annual amount
117+
annual_amount = total_weekly * WEEKS_IN_YEAR
118+
119+
# Apply age-specific take-up rates in microsimulation
120+
takeup_under_6 = p.takeup_rate.under_6
121+
takeup_6_and_over = p.takeup_rate.age_6_and_over
122+
123+
has_children_6_and_over = children_6_and_over > 0
124+
takeup_rate = where(
125+
has_children_6_and_over, takeup_6_and_over, takeup_under_6
126+
)
127+
128+
takes_up = random(benunit) < takeup_rate
129+
is_in_microsimulation = benunit.simulation.dataset is not None
130+
if is_in_microsimulation:
131+
receives_payment = takes_up
132+
else:
133+
receives_payment = True
134+
135+
return (
136+
in_scotland
137+
* scp_available
138+
* receives_qualifying_benefit
139+
* receives_payment
140+
* annual_amount
141+
)
142+
143+
class reform(Reform):
144+
def apply(self):
145+
self.update_variable(scottish_child_payment)
146+
147+
return reform
148+
149+
150+
def create_scottish_child_payment_reform(
151+
parameters, period, bypass: bool = False
152+
):
153+
if bypass:
154+
return create_scottish_child_payment_baby_bonus_reform()
155+
156+
p = parameters.gov.contrib.scotland.scottish_child_payment
157+
158+
# Check if reform is active in current period or next 5 years
159+
reform_active = False
160+
current_period = period_(period)
161+
162+
for i in range(5):
163+
if p(current_period).in_effect:
164+
reform_active = True
165+
break
166+
current_period = current_period.offset(1, "year")
167+
168+
if reform_active:
169+
return create_scottish_child_payment_baby_bonus_reform()
170+
else:
171+
return None
172+
173+
174+
scottish_child_payment_reform = (
175+
create_scottish_child_payment_baby_bonus_reform()
176+
)

policyengine_uk/simulation.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
extend_single_year_dataset,
2525
)
2626
from policyengine_uk.utils.dependencies import get_variable_dependencies
27+
from policyengine_uk.reforms import create_structural_reforms_from_parameters
2728

2829
from .tax_benefit_system import CountryTaxBenefitSystem
2930

@@ -121,6 +122,14 @@ def __init__(
121122

122123
self.tax_benefit_system.reset_parameter_caches()
123124

125+
# Apply structural reforms based on parameters
126+
structural_reform = create_structural_reforms_from_parameters(
127+
self.tax_benefit_system.parameters,
128+
period_(self.default_input_period),
129+
)
130+
if structural_reform is not None:
131+
self.apply_reform(structural_reform)
132+
124133
self.move_values("capital_gains", "capital_gains_before_response")
125134
self.move_values("employment_income", "employment_income_before_lsr")
126135
self.move_values(

0 commit comments

Comments
 (0)