diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb..d2dbb750c 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: minor + changes: + added: + - Scottish Child Payment baby bonus reform. diff --git a/docs/book/validation/student-loan-repayments.ipynb b/docs/book/validation/student-loan-repayments.ipynb index 26b16268e..af53e50c7 100644 --- a/docs/book/validation/student-loan-repayments.ipynb +++ b/docs/book/validation/student-loan-repayments.ipynb @@ -70,7 +70,14 @@ "outputs": [], "source": [ "# Plan distribution (weighted)\n", - "plan_names = {0: \"None\", 1: \"Plan 1\", 2: \"Plan 2\", 3: \"Postgraduate\", 4: \"Plan 4\", 5: \"Plan 5\"}\n", + "plan_names = {\n", + " 0: \"None\",\n", + " 1: \"Plan 1\",\n", + " 2: \"Plan 2\",\n", + " 3: \"Postgraduate\",\n", + " 4: \"Plan 4\",\n", + " 5: \"Plan 5\",\n", + "}\n", "for plan_id, name in plan_names.items():\n", " count = weight[plan == plan_id].sum() / 1e6\n", " print(f\"{name}: {count:.2f}m people\")" @@ -119,16 +126,22 @@ "\n", "if has_reported.sum() > 0:\n", " # Correlation\n", - " correlation = np.corrcoef(reported[has_reported], modelled[has_reported])[0, 1]\n", + " correlation = np.corrcoef(reported[has_reported], modelled[has_reported])[\n", + " 0, 1\n", + " ]\n", " print(f\"Correlation (people with reported > 0): {correlation:.3f}\")\n", - " \n", + "\n", " # Match rate\n", " both_positive = (reported > 0) & (modelled > 0)\n", " match_rate = both_positive.sum() / has_reported.sum() * 100\n", - " print(f\"People with both reported & modelled > 0: {match_rate:.1f}% of reporters\")\n", - " \n", + " print(\n", + " f\"People with both reported & modelled > 0: {match_rate:.1f}% of reporters\"\n", + " )\n", + "\n", " # Mean values\n", - " print(f\"\\nMean reported (reporters): £{reported[has_reported].mean():,.0f}\")\n", + " print(\n", + " f\"\\nMean reported (reporters): £{reported[has_reported].mean():,.0f}\"\n", + " )\n", " print(f\"Mean modelled (reporters): £{modelled[has_reported].mean():,.0f}\")\n", " print(f\"Mean income (reporters): £{income[has_reported].mean():,.0f}\")" ] diff --git a/policyengine_uk/parameters/gov/contrib/scotland/scottish_child_payment/baby_bonus.yaml b/policyengine_uk/parameters/gov/contrib/scotland/scottish_child_payment/baby_bonus.yaml new file mode 100644 index 000000000..444a404aa --- /dev/null +++ b/policyengine_uk/parameters/gov/contrib/scotland/scottish_child_payment/baby_bonus.yaml @@ -0,0 +1,23 @@ +description: Additional weekly amount for Scottish Child Payment based on child age. +brackets: + - threshold: + values: + 0001-01-01: 0 + amount: + values: + 0001-01-01: 12.85 + - threshold: + values: + 0001-01-01: 1 + amount: + values: + 0001-01-01: 0 +metadata: + type: single_amount + threshold_unit: year + amount_unit: currency-GBP + period: week + label: Scottish Child Payment baby bonus by age + reference: + - title: Scottish Government - Scottish Child Payment + href: https://www.gov.scot/policies/social-security/scottish-child-payment/ diff --git a/policyengine_uk/parameters/gov/contrib/scotland/scottish_child_payment/in_effect.yaml b/policyengine_uk/parameters/gov/contrib/scotland/scottish_child_payment/in_effect.yaml new file mode 100644 index 000000000..110e9768d --- /dev/null +++ b/policyengine_uk/parameters/gov/contrib/scotland/scottish_child_payment/in_effect.yaml @@ -0,0 +1,7 @@ +description: Whether the Scottish Child Payment baby bonus reform is in effect. +values: + 0001-01-01: false +metadata: + unit: bool + period: year + label: Scottish Child Payment baby bonus reform in effect diff --git a/policyengine_uk/reforms/reforms.py b/policyengine_uk/reforms/reforms.py index b27f2aab9..be00af9a3 100644 --- a/policyengine_uk/reforms/reforms.py +++ b/policyengine_uk/reforms/reforms.py @@ -4,6 +4,7 @@ disable_simulated_benefits, ) from .policyengine.adjust_budgets import adjust_budgets +from .scotland import create_scottish_child_payment_reform from policyengine_core.model_api import * from policyengine_core import periods @@ -15,6 +16,7 @@ def create_structural_reforms_from_parameters(parameters, period): create_household_based_hitc_reform(parameters, period), disable_simulated_benefits(parameters, period), adjust_budgets(parameters, period), + create_scottish_child_payment_reform(parameters, period), ] reforms = tuple(filter(lambda x: x is not None, reforms)) diff --git a/policyengine_uk/reforms/scotland/__init__.py b/policyengine_uk/reforms/scotland/__init__.py new file mode 100644 index 000000000..d33418501 --- /dev/null +++ b/policyengine_uk/reforms/scotland/__init__.py @@ -0,0 +1 @@ +from .scottish_child_payment_reform import create_scottish_child_payment_reform diff --git a/policyengine_uk/reforms/scotland/scottish_child_payment_reform.py b/policyengine_uk/reforms/scotland/scottish_child_payment_reform.py new file mode 100644 index 000000000..68050badd --- /dev/null +++ b/policyengine_uk/reforms/scotland/scottish_child_payment_reform.py @@ -0,0 +1,176 @@ +from policyengine_uk.model_api import * +from policyengine_core.periods import period as period_ + + +def create_scottish_child_payment_baby_bonus_reform() -> Reform: + """ + Reform that implements SCP Premium for under-ones. + + Policy: Children under 1 receive a FIXED £40/week total payment. + Children 1+ receive the standard SCP rate (inflates with inflation). + + This is NOT a fixed bonus added to the base - it's a fixed total amount. + As the base SCP rate inflates, the "bonus" for under-1s effectively + decreases to maintain the £40 total. + + Source: Scottish Budget 2026-27 + https://www.gov.scot/publications/scottish-budget-2026-2027-finance-secretarys-statement-13-january-2026-2/ + """ + + class scottish_child_payment(Variable): + label = "Scottish Child Payment" + documentation = ( + "Scottish Child Payment provides financial support to low-income " + "families in Scotland. It is paid per eligible child to families " + "receiving qualifying benefits such as Universal Credit." + ) + entity = BenUnit + definition_period = YEAR + value_type = float + unit = GBP + reference = [ + "https://www.gov.scot/policies/social-security/scottish-child-payment/", + "https://www.socialsecurity.gov.scot/", + ] + + def formula(benunit, period, parameters): + # Check if household is in Scotland + in_scotland = ( + benunit.household("country", period).decode_to_str() + == "SCOTLAND" + ) + + # Get SCP parameters + p = parameters( + period + ).gov.social_security_scotland.scottish_child_payment + weekly_amount = p.amount + + # SCP only available when amount > 0 (i.e., after Feb 2021) + scp_available = weekly_amount > 0 + + # Count eligible children in the benefit unit + is_eligible_child = benunit.members( + "is_scp_eligible_child", period + ) + eligible_children = benunit.sum(is_eligible_child) + + # Get ages for baby bonus calculation + age = benunit.members("age", period) + + # Count children under 6 and 6+ for takeup rate calculation + is_child = benunit.members("is_child", period) + children_6_and_over = benunit.sum( + is_child & (age >= 6) & (age < 16) + ) + + # Check if receiving a qualifying benefit + qb = p.qualifying_benefits + + receives_uc = ( + benunit("universal_credit", period) > 0 + ) & qb.universal_credit + receives_ctc = ( + benunit("child_tax_credit", period) > 0 + ) & qb.child_tax_credit + receives_wtc = ( + benunit("working_tax_credit", period) > 0 + ) & qb.working_tax_credit + receives_income_support = ( + benunit("income_support", period) > 0 + ) & qb.income_support + receives_jsa_income = ( + benunit("jsa_income", period) > 0 + ) & qb.jsa_income + receives_esa_income = ( + benunit("esa_income", period) > 0 + ) & qb.esa_income + receives_pension_credit = ( + benunit("pension_credit", period) > 0 + ) & qb.pension_credit + + receives_qualifying_benefit = ( + receives_uc + | receives_ctc + | receives_wtc + | receives_income_support + | receives_jsa_income + | receives_esa_income + | receives_pension_credit + ) + + # SCP Premium for under-ones: Fixed £40/week total (not base + bonus) + # Policy: Children under 1 get £40/week, children 1+ get standard rate + PREMIUM_RATE_UNDER_ONE = 40.0 # £40/week fixed total + + # Calculate per-child weekly amount based on age + per_child_weekly = where( + age < 1, + PREMIUM_RATE_UNDER_ONE, # £40/week for under-1s (TOTAL, not bonus) + weekly_amount, # Standard SCP rate for 1+ (inflates with inflation) + ) + + # Calculate total weekly payment for all eligible children + total_weekly = benunit.sum(per_child_weekly * is_eligible_child) + + # Convert to annual amount + annual_amount = total_weekly * WEEKS_IN_YEAR + + # Apply age-specific take-up rates in microsimulation + takeup_under_6 = p.takeup_rate.under_6 + takeup_6_and_over = p.takeup_rate.age_6_and_over + + has_children_6_and_over = children_6_and_over > 0 + takeup_rate = where( + has_children_6_and_over, takeup_6_and_over, takeup_under_6 + ) + + takes_up = random(benunit) < takeup_rate + is_in_microsimulation = benunit.simulation.dataset is not None + if is_in_microsimulation: + receives_payment = takes_up + else: + receives_payment = True + + return ( + in_scotland + * scp_available + * receives_qualifying_benefit + * receives_payment + * annual_amount + ) + + class reform(Reform): + def apply(self): + self.update_variable(scottish_child_payment) + + return reform + + +def create_scottish_child_payment_reform( + parameters, period, bypass: bool = False +): + if bypass: + return create_scottish_child_payment_baby_bonus_reform() + + p = parameters.gov.contrib.scotland.scottish_child_payment + + # Check if reform is active in current period or next 5 years + reform_active = False + current_period = period_(period) + + for i in range(5): + if p(current_period).in_effect: + reform_active = True + break + current_period = current_period.offset(1, "year") + + if reform_active: + return create_scottish_child_payment_baby_bonus_reform() + else: + return None + + +scottish_child_payment_reform = ( + create_scottish_child_payment_baby_bonus_reform() +) diff --git a/policyengine_uk/simulation.py b/policyengine_uk/simulation.py index 301219595..1b320405f 100644 --- a/policyengine_uk/simulation.py +++ b/policyengine_uk/simulation.py @@ -24,6 +24,7 @@ extend_single_year_dataset, ) from policyengine_uk.utils.dependencies import get_variable_dependencies +from policyengine_uk.reforms import create_structural_reforms_from_parameters from .tax_benefit_system import CountryTaxBenefitSystem @@ -121,6 +122,14 @@ def __init__( self.tax_benefit_system.reset_parameter_caches() + # Apply structural reforms based on parameters + structural_reform = create_structural_reforms_from_parameters( + self.tax_benefit_system.parameters, + period_(self.default_input_period), + ) + if structural_reform is not None: + self.apply_reform(structural_reform) + self.move_values("capital_gains", "capital_gains_before_response") self.move_values("employment_income", "employment_income_before_lsr") self.move_values( diff --git a/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/scottish_child_payment_baby_bonus.yaml b/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/scottish_child_payment_baby_bonus.yaml new file mode 100644 index 000000000..70e1bf72c --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/scottish_child_payment_baby_bonus.yaml @@ -0,0 +1,274 @@ +# Tests for Scottish Child Payment Premium for under-ones reform +# +# POLICY: Children under 1 receive FIXED £40/week TOTAL payment +# Children 1+ receive standard SCP rate (£27.15 in 2026, inflates thereafter) +# +# This is NOT a fixed bonus - it's a fixed total amount. +# As standard rate inflates, the effective "premium" decreases to maintain £40 total. +# +# 2026 calculations: +# - Standard SCP: £27.15/week * 52 = £1,411.80/year +# - Premium for under-1s: £40/week * 52 = £2,080/year (TOTAL, not base + bonus) +# - Effective premium: £40 - £27.15 = £12.85/week (but this changes as base inflates) + +# Test 1: Scottish family with 1 baby (age 0) - WITHOUT reform +- name: Scottish family with 1 baby - baseline (no baby bonus) + period: 2026 + absolute_error_margin: 1 + input: + people: + parent: + age: 30 + baby: + age: 0 + benunits: + family: + members: + - parent + - baby + universal_credit: 5000 + households: + household: + members: + - parent + - baby + region: SCOTLAND + output: + # 1 child * £27.15/week * 52 weeks = £1,411.80 + scottish_child_payment: 1412 + +# Test 2: Scottish family with 1 baby (age 0) - WITH reform +- name: Scottish family with 1 baby - with baby bonus + period: 2026 + absolute_error_margin: 1 + reforms: policyengine_uk.reforms.scotland.scottish_child_payment_reform.scottish_child_payment_reform + input: + gov.contrib.scotland.scottish_child_payment.in_effect: true + people: + parent: + age: 30 + baby: + age: 0 + benunits: + family: + members: + - parent + - baby + universal_credit: 5000 + households: + household: + members: + - parent + - baby + region: SCOTLAND + output: + # 1 baby * £40/week * 52 = £2,080/year (TOTAL payment for under-1) + scottish_child_payment: 2080 + +# Test 3: Scottish family with 2 babies (both age 0) - WITH reform +- name: Scottish family with 2 babies - with baby bonus + period: 2026 + absolute_error_margin: 1 + reforms: policyengine_uk.reforms.scotland.scottish_child_payment_reform.scottish_child_payment_reform + input: + gov.contrib.scotland.scottish_child_payment.in_effect: true + people: + parent: + age: 30 + baby1: + age: 0 + baby2: + age: 0 + benunits: + family: + members: + - parent + - baby1 + - baby2 + universal_credit: 5000 + households: + household: + members: + - parent + - baby1 + - baby2 + region: SCOTLAND + output: + # 2 babies * £40/week * 52 = £4,160/year (TOTAL payment for 2 under-1s) + scottish_child_payment: 4160 + +# Test 4: Scottish family with 1 baby and 1 older child - WITH reform +- name: Scottish family with 1 baby and 1 older child - with baby bonus + period: 2026 + absolute_error_margin: 1 + reforms: policyengine_uk.reforms.scotland.scottish_child_payment_reform.scottish_child_payment_reform + input: + gov.contrib.scotland.scottish_child_payment.in_effect: true + people: + parent: + age: 30 + baby: + age: 0 + older_child: + age: 5 + benunits: + family: + members: + - parent + - baby + - older_child + universal_credit: 5000 + households: + household: + members: + - parent + - baby + - older_child + region: SCOTLAND + output: + # 1 baby @ £40/week + 1 older child @ £27.15/week = £67.15/week + # £67.15 * 52 = £3,491.80/year + scottish_child_payment: 3492 + +# Test 5: Scottish family with only older children (no babies) - WITH reform +- name: Scottish family with older children only - no baby bonus + period: 2026 + absolute_error_margin: 1 + reforms: policyengine_uk.reforms.scotland.scottish_child_payment_reform.scottish_child_payment_reform + input: + gov.contrib.scotland.scottish_child_payment.in_effect: true + people: + parent: + age: 30 + child1: + age: 3 + child2: + age: 7 + benunits: + family: + members: + - parent + - child1 + - child2 + universal_credit: 5000 + households: + household: + members: + - parent + - child1 + - child2 + region: SCOTLAND + output: + # 2 older children @ £27.15/week * 52 = £2,823.60 (standard rate, no premium) + scottish_child_payment: 2824 + +# Test 6: English family with baby - no SCP regardless of reform +- name: English family with baby - no SCP + period: 2026 + absolute_error_margin: 1 + reforms: policyengine_uk.reforms.scotland.scottish_child_payment_reform.scottish_child_payment_reform + input: + gov.contrib.scotland.scottish_child_payment.in_effect: true + people: + parent: + age: 30 + baby: + age: 0 + benunits: + family: + members: + - parent + - baby + universal_credit: 5000 + households: + household: + members: + - parent + - baby + region: NORTH_EAST + output: + scottish_child_payment: 0 + +# Test 7: Child at age boundary (exactly 1 year old) - no baby bonus +- name: Scottish family with 1 year old child - no baby bonus + period: 2026 + absolute_error_margin: 1 + reforms: policyengine_uk.reforms.scotland.scottish_child_payment_reform.scottish_child_payment_reform + input: + gov.contrib.scotland.scottish_child_payment.in_effect: true + people: + parent: + age: 30 + child: + age: 1 + benunits: + family: + members: + - parent + - child + universal_credit: 5000 + households: + household: + members: + - parent + - child + region: SCOTLAND + output: + # 1 child @ £27.15/week * 52 = £1,411.80 (standard rate - age >= 1, no premium) + scottish_child_payment: 1412 + +# Test 8: Higher income (£45k) - no qualifying benefit, no SCP +- name: Scottish family with £45k income - no SCP (no qualifying benefit) + period: 2026 + absolute_error_margin: 1 + reforms: policyengine_uk.reforms.scotland.scottish_child_payment_reform.scottish_child_payment_reform + input: + gov.contrib.scotland.scottish_child_payment.in_effect: true + people: + parent: + age: 30 + employment_income: 45000 + baby: + age: 0 + benunits: + family: + members: + - parent + - baby + households: + household: + members: + - parent + - baby + region: SCOTLAND + output: + # No UC at £45k income, so no SCP eligibility + scottish_child_payment: 0 + +# Test 9: Higher income (£50k) - no qualifying benefit, no SCP +- name: Scottish family with £50k income - no SCP (no qualifying benefit) + period: 2026 + absolute_error_margin: 1 + reforms: policyengine_uk.reforms.scotland.scottish_child_payment_reform.scottish_child_payment_reform + input: + gov.contrib.scotland.scottish_child_payment.in_effect: true + people: + parent: + age: 30 + employment_income: 50000 + baby: + age: 0 + benunits: + family: + members: + - parent + - baby + households: + household: + members: + - parent + - baby + region: SCOTLAND + output: + # No UC at £50k income, so no SCP eligibility + scottish_child_payment: 0 diff --git a/policyengine_uk/variables/gov/social_security_scotland/scottish_child_payment.py b/policyengine_uk/variables/gov/social_security_scotland/scottish_child_payment.py index 702e12d9c..dfff3862f 100644 --- a/policyengine_uk/variables/gov/social_security_scotland/scottish_child_payment.py +++ b/policyengine_uk/variables/gov/social_security_scotland/scottish_child_payment.py @@ -34,12 +34,10 @@ def formula(benunit, period, parameters): # Count eligible children in the benefit unit is_eligible_child = benunit.members("is_scp_eligible_child", period) - eligible_children = benunit.sum(is_eligible_child) - # Count children under 6 and 6+ for takeup rate calculation + # Get ages for baby bonus calculation age = benunit.members("age", period) is_child = benunit.members("is_child", period) - children_under_6 = benunit.sum(is_child & (age < 6)) children_6_and_over = benunit.sum(is_child & (age >= 6) & (age < 16)) # Check if receiving a qualifying benefit @@ -79,8 +77,29 @@ def formula(benunit, period, parameters): | receives_pension_credit ) - # Calculate annual payment - annual_amount = eligible_children * weekly_amount * WEEKS_IN_YEAR + # Check if SCP Premium for under-ones is enabled (parametric reform) + # This allows enabling via parameter_changes without structural reform + baby_bonus_in_effect = parameters( + period + ).gov.contrib.scotland.scottish_child_payment.in_effect + + # SCP Premium for under-ones: Fixed £40/week total for babies under 1 + # Policy: Children under 1 get £40/week, children 1+ get standard rate + # Source: Scottish Budget 2026-27 + PREMIUM_RATE_UNDER_ONE = 40.0 # £40/week fixed total + + # Calculate per-child weekly amount based on age (if reform is active) + per_child_weekly = where( + baby_bonus_in_effect & (age < 1), + PREMIUM_RATE_UNDER_ONE, # £40/week for under-1s (TOTAL, not bonus) + weekly_amount, # Standard SCP rate for 1+ or when reform inactive + ) + + # Calculate total weekly payment for all eligible children + total_weekly = benunit.sum(per_child_weekly * is_eligible_child) + + # Convert to annual amount + annual_amount = total_weekly * WEEKS_IN_YEAR # Apply age-specific take-up rates in microsimulation # 97% for families with only children under 6