diff --git a/changelog.d/2183.fixed.md b/changelog.d/2183.fixed.md new file mode 100644 index 0000000000..3097b99f77 --- /dev/null +++ b/changelog.d/2183.fixed.md @@ -0,0 +1 @@ +- Updated England childcare entitlement valuations to the published 2025–26 and 2026–27 national-average funding rates. diff --git a/policyengine_uk/parameters/gov/dfe/childcare_funding_rate.yaml b/policyengine_uk/parameters/gov/dfe/childcare_funding_rate.yaml index 0962566d14..7d8ccf8ff8 100644 --- a/policyengine_uk/parameters/gov/dfe/childcare_funding_rate.yaml +++ b/policyengine_uk/parameters/gov/dfe/childcare_funding_rate.yaml @@ -1,4 +1,7 @@ -description: The Department for Education converts childcare hours into expenses for childcare entitlement at these rates. +description: >- + National-average English local-authority hourly funding used to value + childcare entitlements. These combined age-group averages are a model + valuation proxy, not individual provider fees or programme-specific rates. metadata: type: single_amount threshold_unit: year @@ -8,17 +11,30 @@ metadata: reference: - title: House of Commons Library | Early years funding in England href: https://commonslibrary.parliament.uk/research-briefings/cbp-8052/ + - title: DfE 2025 to 2026 early years national funding formulae technical note, Table 6 (historical, withdrawn) + href: https://www.gov.uk/government/publications/early-years-funding-2025-to-2026/2025-to-2026-early-years-national-funding-formulae-technical-note#table-6-2025-to-2026-national-average-of-local-authority-hourly-funding-rates + - title: DfE 2026 to 2027 early years national funding formulae technical note, Table 6 + href: https://www.gov.uk/government/publications/early-years-funding-2026-to-2027/2026-to-2027-early-years-national-funding-formulae-technical-note#table-6-2026-to-2027-national-average-of-local-authority-hourly-funding-rates +# Funding years run April to March. The existing annual model samples +# 30 April; it retains its separate annual approximation of the hours rollout. +# Later years continue to use the existing CPI projection, not official rates. brackets: - threshold: 2015-01-01: 0 amount: 2024-04-01: 11.22 + 2025-04-01: 11.54 + 2026-04-01: 12.04 - threshold: 2015-01-01: 2 amount: 2024-04-01: 8.28 + 2025-04-01: 8.53 + 2026-04-01: 8.90 - threshold: 2015-01-01: 3 amount: - 2024-04-01: 5.88 \ No newline at end of file + 2024-04-01: 5.88 + 2025-04-01: 6.12 + 2026-04-01: 6.61 diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml index b642b1c4b5..9fb1a9963c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement.yaml @@ -3,8 +3,8 @@ # counts those hours towards the 30. Every child here is in England (the default) # and claims the universal entitlement (the default), so a 3- or 4-year-old's # universal 570 hours a year come off the 1,140 the parameter gives. Rates per -# hour: 2025 £8.486995 (age 2) and £6.026996 (3-4); 2026 £8.775556 (age 2) and -# £6.231916 (3-4). +# hour: 2025 £8.53 (age 2) and £6.12 (3-4); 2026 £8.90 (age 2) and +# £6.61 (3-4). - name: 3-year-old gets the 15 working parent hours above the universal 15 period: 2025 absolute_error_margin: 0.05 @@ -17,9 +17,9 @@ members: [child1] extended_childcare_entitlement_eligible: true output: - # (1,140 - 570) x 6.026996 - extended_childcare_entitlement: 3435.39 - universal_childcare_entitlement: 3435.39 + # (1,140 - 570) x 6.12 + extended_childcare_entitlement: 3488.40 + universal_childcare_entitlement: 3488.40 - name: Eligible for 15 hours - 2 year old (evaluated at Jan 1, 2025 before Sept expansion) period: 2025 @@ -33,8 +33,8 @@ members: [child1] extended_childcare_entitlement_eligible: true output: - # 15 x 38 x 8.486995; no targeted hours, so nothing comes off. - extended_childcare_entitlement: 4837.59 + # 15 x 38 x 8.53; no targeted hours, so nothing comes off. + extended_childcare_entitlement: 4862.10 - name: Not eligible (one condition not met) period: 2025 @@ -64,8 +64,8 @@ members: [child1, child2] extended_childcare_entitlement_eligible: true output: - # 570 x 8.486995 + (1,140 - 570) x 6.026996 - extended_childcare_entitlement: 8272.97 + # 570 x 8.53 + (1,140 - 570) x 6.12 + extended_childcare_entitlement: 8350.50 - name: Not eligible - Family with multiple children but conditions not met period: 2025 @@ -100,8 +100,8 @@ family_type: COUPLE_WITH_CHILDREN extended_childcare_entitlement_eligible: true output: - # 570 x 8.486995 + 2 x (1,140 - 570) x 6.026996 - extended_childcare_entitlement: 11708.36 + # 570 x 8.53 + 2 x (1,140 - 570) x 6.12 + extended_childcare_entitlement: 11838.90 - name: No expenses for zero hours period: 2025 @@ -130,10 +130,10 @@ members: [child1] extended_childcare_entitlement_eligible: true output: - # (20 x 38 - 570) x 6.026996: 190 working parent hours on top of the + # (20 x 38 - 570) x 6.12: 190 working parent hours on top of the # universal 570. - extended_childcare_entitlement: 1145.13 - universal_childcare_entitlement: 3435.39 + extended_childcare_entitlement: 1162.80 + universal_childcare_entitlement: 3488.40 - name: Child using fewer hours than maximum entitlement - multiple children period: 2025 @@ -151,8 +151,8 @@ members: [child1, child2] extended_childcare_entitlement_eligible: true output: - # 10 x 38 x 8.486995 + max(0, 15 x 38 - 570) x 6.026996 - extended_childcare_entitlement: 3225.06 + # 10 x 38 x 8.53 + max(0, 15 x 38 - 570) x 6.12 + extended_childcare_entitlement: 3241.40 - name: Benefit unit maximum hours cap applied period: 2025 @@ -171,8 +171,8 @@ extended_childcare_entitlement_eligible: true maximum_extended_childcare_hours_usage: 40 output: - # The cap applies to each child: 2 x (1,140 - 570) x 6.026996 - extended_childcare_entitlement: 6870.78 + # The cap applies to each child: 2 x (1,140 - 570) x 6.12 + extended_childcare_entitlement: 6976.80 - name: A family cap below the universal hours leaves the universal hours whole period: 2025 @@ -191,7 +191,7 @@ # 10 x 38 = 380 hours is under the universal 570, so no working parent # hours, and the universal 570 stay. extended_childcare_entitlement: 0 - universal_childcare_entitlement: 3435.39 + universal_childcare_entitlement: 3488.40 - name: A family cap above 15 adds the hours between 15 and the cap period: 2025 @@ -207,8 +207,8 @@ extended_childcare_entitlement_eligible: true maximum_extended_childcare_hours_usage: 18 output: - # (18 x 38 - 570) x 6.026996 - extended_childcare_entitlement: 687.08 + # (18 x 38 - 570) x 6.12 + extended_childcare_entitlement: 697.68 - name: Benefit unit without maximum hours cap applied with 3 years old child period: 2025 @@ -226,7 +226,7 @@ output: # The child uses 15 hours, all of them universal. extended_childcare_entitlement: 0 - universal_childcare_entitlement: 3435.39 + universal_childcare_entitlement: 3488.40 - name: Eligible for 30 hours - 2 year old after September 2025 expansion period: 2026 @@ -240,8 +240,8 @@ members: [child1] extended_childcare_entitlement_eligible: true output: - # 30 x 38 x 8.775556; no targeted hours. - extended_childcare_entitlement: 10004.13 + # 30 x 38 x 8.90; no targeted hours. + extended_childcare_entitlement: 10146.00 - name: Eligible for 30 hours - Family with multiple 2-year-olds after expansion period: 2026 @@ -257,8 +257,8 @@ members: [child1, child2] extended_childcare_entitlement_eligible: true output: - # 1,140 x 8.775556 + (1,140 - 570) x 6.231916 - extended_childcare_entitlement: 13556.33 + # 1,140 x 8.90 + (1,140 - 570) x 6.61 + extended_childcare_entitlement: 13913.70 # SI 2022/1134 reg 13(2)(b): a child looked after by a local authority is not a # qualifying child, so only the other three-year-old's working parent hours @@ -279,5 +279,5 @@ members: [child1, child2] extended_childcare_entitlement_eligible: true output: - extended_childcare_entitlement: 3435.39 - universal_childcare_entitlement: [3435.39, 3435.39] + extended_childcare_entitlement: 3488.40 + universal_childcare_entitlement: [3488.40, 3488.40] diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_per_child.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_per_child.yaml index a445d1dd3d..9397df199b 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_per_child.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_per_child.yaml @@ -1,8 +1,8 @@ # Each child's working parent hours are the part of its funded hours above the # universal (3-4) or targeted (2) hours: Childcare Act 2016 s.1(6)(a), and DfE's # statutory guidance (April 2026) para A1.11 for a 2-year-old eligible for both. -# Rates per hour: 2025 £8.486995 (age 2); 2026 £11.891514 (under 2), -# £8.775556 (age 2) and £6.231916 (3-4). +# Rates per hour: 2025 £8.53 (age 2); 2026 £12.04 (under 2), +# £8.90 (age 2) and £6.61 (3-4). - name: A 2-year-old eligible for both gets 15 targeted plus 15 working parent hours period: 2026 absolute_error_margin: 0.05 @@ -16,10 +16,10 @@ extended_childcare_entitlement_eligible: true targeted_childcare_entitlement_eligible: true output: - # 570 x 8.775556 each - targeted_childcare_entitlement: 5002.07 - extended_childcare_entitlement_per_child: 5002.07 - extended_childcare_entitlement: 5002.07 + # 570 x 8.90 each + targeted_childcare_entitlement: 5073.00 + extended_childcare_entitlement_per_child: 5073.00 + extended_childcare_entitlement: 5073.00 - name: Before September 2025 a 2-year-old eligible for both gets the targeted 15 hours period: 2025 @@ -35,8 +35,8 @@ targeted_childcare_entitlement_eligible: true output: # The working parent total (15 hours at 1 January 2025) is no more than - # the targeted 570 hours: 570 x 8.486995 targeted, nothing on top. - targeted_childcare_entitlement: 4837.59 + # the targeted 570 hours: 570 x 8.53 targeted, nothing on top. + targeted_childcare_entitlement: 4862.10 extended_childcare_entitlement_per_child: 0 - name: A 1-year-old has no universal or targeted hours to come off @@ -51,8 +51,8 @@ members: [child] extended_childcare_entitlement_eligible: true output: - # 30 x 38 x 11.891514 - extended_childcare_entitlement_per_child: 13556.33 + # 30 x 38 x 12.04 + extended_childcare_entitlement_per_child: 13725.60 - name: Working parent participation funds universal first even with false standalone take-up period: 2026 @@ -67,10 +67,10 @@ extended_childcare_entitlement_eligible: true would_claim_universal_childcare: false output: - # 15 x 38 x 6.231916 in each programme, preserving all 30 funded hours. + # 15 x 38 x 6.61 in each programme, preserving all 30 funded hours. universal_childcare_entitlement_eligible: true - universal_childcare_entitlement: 3552.19 - extended_childcare_entitlement_per_child: 3552.19 + universal_childcare_entitlement: 3767.70 + extended_childcare_entitlement_per_child: 3767.70 is_child_receiving_universal_childcare: true is_child_receiving_extended_childcare: true @@ -88,8 +88,8 @@ extended_childcare_entitlement_eligible: true would_claim_universal_childcare: false output: - # 10 x 38 x 6.231916; the unpaid standalone floor is not added. - universal_childcare_entitlement: 2368.13 + # 10 x 38 x 6.61; the unpaid standalone floor is not added. + universal_childcare_entitlement: 2511.80 extended_childcare_entitlement_per_child: 0 is_child_receiving_universal_childcare: true is_child_receiving_extended_childcare: false @@ -108,7 +108,7 @@ maximum_extended_childcare_hours_usage: 10 would_claim_universal_childcare: false output: - universal_childcare_entitlement: 2368.13 + universal_childcare_entitlement: 2511.80 extended_childcare_entitlement_per_child: 0 - name: Legal universal eligibility alone does not create take-up @@ -142,9 +142,9 @@ would_claim_targeted_childcare: false output: targeted_childcare_entitlement_eligible: true - # 15 x 38 x 8.775556 in each programme (DfE A1.11). - targeted_childcare_entitlement: 5002.07 - extended_childcare_entitlement_per_child: 5002.07 + # 15 x 38 x 8.90 in each programme (DfE A1.11). + targeted_childcare_entitlement: 5073.00 + extended_childcare_entitlement_per_child: 5073.00 is_child_receiving_targeted_childcare: true is_child_receiving_extended_childcare: true @@ -163,8 +163,8 @@ extended_childcare_entitlement_eligible: true would_claim_targeted_childcare: false output: - # 10 x 38 x 8.775556, with no unfunded floor subtracted. - targeted_childcare_entitlement: 3334.71 + # 10 x 38 x 8.90, with no unfunded floor subtracted. + targeted_childcare_entitlement: 3382.00 extended_childcare_entitlement_per_child: 0 is_child_receiving_targeted_childcare: true is_child_receiving_extended_childcare: false @@ -183,7 +183,7 @@ extended_childcare_entitlement_eligible: true would_claim_targeted_childcare: false output: - targeted_childcare_entitlement: 4837.59 + targeted_childcare_entitlement: 4862.10 extended_childcare_entitlement_per_child: 0 - name: Legal targeted eligibility alone does not create take-up @@ -231,9 +231,9 @@ members: [parent, child] extended_childcare_entitlement_eligible: true output: - # (1,140 - 570) x 6.231916 for the child only - extended_childcare_entitlement_per_child: [0, 3552.19] - extended_childcare_entitlement: 3552.19 + # (1,140 - 570) x 6.61 for the child only + extended_childcare_entitlement_per_child: [0, 3767.70] + extended_childcare_entitlement: 3767.70 # The same overlap through the targeted eligibility formula itself: Income # Support is a qualifying benefit, and eligibility for the working parent @@ -256,9 +256,9 @@ country: ENGLAND output: targeted_childcare_entitlement_eligible: true - # 570 x 8.775556 each - targeted_childcare_entitlement: 5002.07 - extended_childcare_entitlement_per_child: 5002.07 + # 570 x 8.90 each + targeted_childcare_entitlement: 5073.00 + extended_childcare_entitlement_per_child: 5073.00 is_child_receiving_targeted_childcare: true is_child_receiving_extended_childcare: true @@ -280,8 +280,8 @@ country: ENGLAND output: targeted_childcare_entitlement_eligible: true - # 570 x 8.486995 targeted; the 15-hour working parent total adds nothing. - targeted_childcare_entitlement: 4837.59 + # 570 x 8.53 targeted; the 15-hour working parent total adds nothing. + targeted_childcare_entitlement: 4862.10 extended_childcare_entitlement_per_child: 0 is_child_receiving_targeted_childcare: true is_child_receiving_extended_childcare: false diff --git a/policyengine_uk/tests/policy/integration/entitledto_scenarios.yaml b/policyengine_uk/tests/policy/integration/entitledto_scenarios.yaml index 95c2636d94..41c2963f3d 100644 --- a/policyengine_uk/tests/policy/integration/entitledto_scenarios.yaml +++ b/policyengine_uk/tests/policy/integration/entitledto_scenarios.yaml @@ -74,7 +74,10 @@ universal_credit: 12_490.86 housing_benefit: 0 child_benefit: 2_251.60 - household_net_income: 36_114.90 + # The 3yo's 570 universal hours at the 2025 funding rate of £6.12 + # are valued at £3,488.40, up £53.01 from £3,435.39. + universal_childcare_entitlement: [0, 0, 0, 3_488.40] + household_net_income: 36_167.91 - name: "EntitledTo #3 - pensioner couple, low state pension, qualifies for Pension Credit" period: 2025 diff --git a/policyengine_uk/tests/test_childcare_funding_rates.py b/policyengine_uk/tests/test_childcare_funding_rates.py new file mode 100644 index 0000000000..35daf38b16 --- /dev/null +++ b/policyengine_uk/tests/test_childcare_funding_rates.py @@ -0,0 +1,165 @@ +"""DfE combined national-average funding is the model's hourly valuation. + +Table 6 of each funding-year technical note supplies the rate oracles. These +are local-authority funding averages, not individual providers' fees. The +existing annual model samples 30 April, without blending the hours rollout. +""" + +from pathlib import Path + +import numpy as np +import pytest +from policyengine_core.parameters import load_parameter_file + +from policyengine_uk import Simulation +from policyengine_uk.tax_benefit_system import system + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +RATES = { + 2024: [11.22, 8.28, 5.88], + 2025: [11.54, 8.53, 6.12], + 2026: [12.04, 8.90, 6.61], +} +AGES = np.array([1, 2, 3]) + + +def working_family(year, country="ENGLAND", child_hours=30, family_hours=30): + people = {"parent": {"age": {year: 35}, "employment_income": {year: 30_000}}} + people.update( + { + f"child{age}": { + "age": {year: age}, + "max_free_entitlement_hours_used": {year: child_hours}, + } + for age in (1, 2, 3) + } + ) + members = list(people) + return { + "people": people, + "benunits": { + "family": { + "members": members, + "maximum_extended_childcare_hours_usage": {year: family_hours}, + } + }, + "households": {"home": {"members": members, "country": {year: country}}}, + } + + +@pytest.mark.parametrize("year", [2025, 2026]) +def test_published_april_rates_and_prior_day(year): + raw = load_parameter_file( + str( + Path(__file__).parents[1] / "parameters/gov/dfe/childcare_funding_rate.yaml" + ), + "childcare_funding_rate", + ) + np.testing.assert_allclose( + raw(f"{year}-04-01").calc(AGES), RATES[year], rtol=0, atol=1e-10 + ) + np.testing.assert_allclose( + raw(f"{year}-03-31").calc(AGES), RATES[year - 1], rtol=0, atol=1e-10 + ) + + +@pytest.mark.parametrize("year", [2024, 2025, 2026]) +def test_april_anchors_reach_processed_annual_rates(year): + rates = system.parameters(str(year)).gov.dfe.childcare_funding_rate + np.testing.assert_allclose(rates.calc(AGES), RATES[year], rtol=0, atol=1e-10) + + +@pytest.mark.parametrize( + "year,universal,extended_per_child,extended_total", + [ + (2025, [0, 0, 0, 3488.40], [0, 6577.80, 4862.10, 3488.40], 14928.30), + (2026, [0, 0, 0, 3767.70], [0, 13725.60, 10146.00, 3767.70], 27639.30), + ], +) +def test_published_rates_reach_genuine_working_family_awards( + year, universal, extended_per_child, extended_total +): + sim = Simulation(situation=working_family(year)) + # 2025 retains the existing 15-hour under-three annual schedule; 2026 + # gives 30. A three-year-old gets 570 universal hours plus 570 extended. + np.testing.assert_allclose( + sim.calculate("universal_childcare_entitlement", year), + universal, + rtol=0, + atol=0.02, + ) + np.testing.assert_allclose( + sim.calculate("extended_childcare_entitlement_per_child", year), + extended_per_child, + rtol=0, + atol=0.02, + ) + np.testing.assert_allclose( + sim.calculate("extended_childcare_entitlement", year), + extended_total, + rtol=0, + atol=0.02, + ) + + +@pytest.mark.parametrize("year,expected", [(2025, 4862.10), (2026, 5073.00)]) +def test_published_rates_reach_targeted_award_for_reported_qualifying_benefit( + year, expected +): + # An ordinary reported Income Support award is an existing qualifying + # benefit input. Eligibility and the childcare amount are calculated. + sim = Simulation( + situation={ + "people": {"parent": {"age": {year: 35}}, "child": {"age": {year: 2}}}, + "benunits": { + "family": { + "members": ["parent", "child"], + "income_support": {year: 1000}, + } + }, + "households": { + "home": {"members": ["parent", "child"], "country": {year: "ENGLAND"}} + }, + } + ) + np.testing.assert_allclose( + sim.calculate("targeted_childcare_entitlement", year), + [0, expected], + rtol=0, + atol=0.01, + ) + + +@pytest.mark.parametrize("child_hours,family_hours", [(10, 30), (30, 10)]) +def test_child_and_family_usage_limits_keep_the_published_rate( + child_hours, family_hours +): + sim = Simulation( + situation=working_family( + 2026, child_hours=child_hours, family_hours=family_hours + ) + ) + # A one-year-old gets min(30,10) * 38 * £12.04, with no base hours. + assert sim.calculate("extended_childcare_entitlement_per_child", 2026)[ + 1 + ] == pytest.approx(4575.20, abs=0.01) + + +def test_outside_england_retains_zero_entitlements(): + sim = Simulation(situation=working_family(2026, country="WALES")) + for variable in ( + "universal_childcare_entitlement", + "targeted_childcare_entitlement", + "extended_childcare_entitlement", + ): + assert not np.asarray(sim.calculate(variable, 2026)).any() + + +def test_existing_cpi_projection_starts_from_the_2026_funding_anchor(): + # This verifies the retained projection rule, not an official 2027 rate. + p = system.parameters + factor = p("2027").gov.benefit_uprating_cpi / p("2026").gov.benefit_uprating_cpi + expected = np.array([12.04, 8.90, 6.61]) * factor + np.testing.assert_allclose( + p("2027").gov.dfe.childcare_funding_rate.calc(AGES), expected, rtol=0, atol=1e-8 + )