Skip to content

Correct the entitlement calibration targets to DfE published figures - #474

Open
vahid-ahmadi wants to merge 2 commits into
sync-childcare-calibration-targetsfrom
fix-entitlement-calibration-targets
Open

Correct the entitlement calibration targets to DfE published figures#474
vahid-ahmadi wants to merge 2 commits into
sync-childcare-calibration-targetsfrom
fix-entitlement-calibration-targets

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Stacked on #472, which moves the targets into datasets/childcare/targets.py. Base retargets to main when that merges.

Summary

The universal and early-learning-for-2-year-olds targets were unsourced "prior DfE values", and neither matches DfE's published data. Corrected here, along with one take-up rate that DfE publishes directly.

Source

DfE, Funded early education and childcare, reporting year 2026 (published 2 July 2026), national figures for January 2024, from data/headline_figures_feeac_2011_2026.csv in the release's data bundle.

January 2024 rather than the latest, because the calibration checks evaluate the model at 2024. These series move fast — using the 2026 figures instead would have made the targeted target look 1.52× too high rather than 1.12×, and the universal one 1.35× rather than 1.18×.

The universal comparator has to net off the working parent entitlement

universal_childcare_entitlement_eligible in policyengine-uk ends with & ~has_extended_childcare — the schemes are modelled as mutually exclusive. So the variable covers children on the universal entitlement only, not the 1.13 million headline:

January 2024
registered for the universal entitlement, excluding reception 778,327
registered for the working parent entitlement, 3 and 4-year-olds 361,790
universal entitlement only 416,537

Against a prior target of 490,000 — 1.18× too high.

Early learning for 2-year-olds: 115,852 registered against a prior target of 130,000 — 1.12× too high.

Spending targets

The DfE caseloads at the statutory 570 hours a year and the DfE funding rate for the age band in 2024-25 (£5.88 for 3 and 4-year-olds, £8.28 for 2-year-olds):

corrected prior
universal 416,537 × 570 × 5.88 = £1.396bn £1.70bn
targeted 115,852 × 570 × 8.28 = £0.547bn £0.60bn

Take-up: one change, sourced; one deliberately not made

Early learning for 2-year-olds — changed. DfE publishes this take-up rate directly, as the share of eligible children registered:

year registered eligible take-up
2022 135,410 188,318 71.9%
2023 124,211 167,976 73.9%
2024 115,852 154,957 74.8%
2025 95,031 145,824 65.2%
2026 85,629 119,598 71.6%

The repo carried an unsourced FRS-derived 0.597. Setting the published 0.748 moves the built dataset from 0.83× to 1.02× of DfE's registered count, on both caseload and spending.

Universal — not changed. DfE's 85.9% is measured against all 3 and 4-year-olds outside reception, a different denominator from the model's eligible population, so it cannot be transferred. The built dataset sits at 0.90× the corrected target. Reaching 1.00× would need take-up of about 0.63, which is target-fitting rather than a published figure.

The residual is a question about the modelled eligible base, not take-up: the model implies about 662,000 eligible children where DfE's comparable non-reception, non-working-parent population is about 518,000. The model over-counts eligibility and under-counts take-up, and the two partly cancel. Worth a separate look; changing the rate alone would fit one error against another.

Effect

before after
universal spending 0.74× 0.90× target £1.70bn → £1.396bn
universal caseload 0.76× 0.89× 490k → 417k
targeted spending 0.76× 1.02× target £0.60bn → £0.547bn, take-up 0.597 → 0.748
targeted caseload 0.74× 1.02× 130k → 116k

Note on method

Three earlier attempts at childcare calibration in this repo, mine included, went wrong on exactly one thing: comparing a model figure against a published one with a different denominator or a different year. The TFC caseload target cited 985 thousand where its own source said 1,085,020 (#470); I then compared an annual model aggregate against a point-in-time HMRC count, and later a 2024 model figure against 2026 DfE data. Both the netting-off and the year-matching above exist because of those.

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Program Review

Source Documents

  • Official release: DfE, Funded early education and childcare, reporting year 2026
  • Methodology: DfE funded early education and childcare methodology
  • Underlying data: headline_figures_feeac_2011_2026.csv, downloaded from the release's official all-data bundle; SHA-256 cefdb06593016446d215433cf31f80649b8ce18f89e454a5ad7931b0ab32d72d
  • Year: January 2024 source data; model/calibration period 2024
  • Scope: PR changes only, including model/data-stack interaction
  • Reviewed head SHA: 10e51732a47539fdff5ca759d1a129ba3cdfc949
  • Mode: full
  • Branch: stacked on data #472 at a48ce74b35215a01d2ec303f8d235610b2ac6add; ahead 1, behind 0

Critical (Must Fix)

  1. [C1] The proposed spending targets are not DfE spending observations. DfE defines these figures as January headcounts of children registered for at least some provision; it does not say each child consumed all 570 hours or publish £1.396bn/£0.547bn outturns. Multiplying every head by the statutory maximum and an April 2024 national-average rate creates a modeled full-entitlement amount. It also makes the spend target algebraically redundant with caseload, so matching both does not independently validate expenditure. Use an observed allocation/outturn or label and justify this as a modeled maximum rather than “spending.” — policyengine_uk_data/datasets/childcare/targets.py:43
  2. [C2] The 74.8% source population does not match the model's operative eligible population. DfE says working-parent entitlement for two-year-olds began in April 2024, after the January 2024 census. But policyengine-uk 2.89.2 gives two-year-olds extended eligibility from 2024-01-01, and targeted_childcare_entitlement_eligible removes extended-eligible benefit units. Thus the source numerator/denominator contains no corresponding January exclusion while the model does. The reported 1.02× fit can therefore be cancellation against a different eligible base, not validation of 0.748. Align the model period/population before transferring the rate. — policyengine_uk_data/parameters/take_up/targeted_childcare.yaml:19
  3. [C3] The changelog entry is not consumed by Towncrier. The repository config expects fragments under breaking/, added/, changed/, fixed/, or removed/; towncrier build --draft rendered “No significant changes.” Move/rename the fragment to a configured type. — changelog.d/entitlement-calibration-targets.md:1

Should Address

  1. [A1] Document the EL2 denominator limitation. DfE says its eligible count omits multiple two-year-olds and several non-economic eligibility routes, so the published percentage overstates true take-up. PolicyEngine models a different subset and also omits several of those routes. Quantify this approximation rather than describing 0.748 as directly transferable. — policyengine_uk_data/parameters/take_up/targeted_childcare.yaml:8
  2. [A2] Reference the exact dataset and methodology. The landing page corroborates the headline rate, but the YAML should link the specific data-information/download surface and the methodology that defines the numerator and denominator. — policyengine_uk_data/parameters/take_up/targeted_childcare.yaml:5
  3. [A3] Pin and test the model contract. The target netting relies on policyengine-uk formulas and rates, while the package allows any policyengine-uk>=2.89.2. A later model can change those semantics without changing these target constants. Record the exact model/data heads used for the ratios and run a combined integration build. — pyproject.toml:24
  4. [A4] Add exact tests and release-grade evidence. No test asserts the 2024 value/boundary or DfE arithmetic. GitHub reports no checks on this head, and #472's ±40% TESTING=1 smoke build does not validate the published enhanced FRS. Add focused source-contract tests and report a reproducible full-build/release comparison. — policyengine_uk_data/tests/test_childcare.py:47

Cross-PR integration

  • data #472: this PR is correctly stacked on its shared TARGETS, but inherits #472's smoke-build-versus-release gap and ±40% default tolerance.
  • data #473: current #473 is a sibling from main, not an ancestor of #474. It changes TFC stochastic input assignment and does not resolve C1/C2; merge/rebase both and run one build before drawing conclusions from the eight childcare ratios.
  • UK #1830 / #1834: these affect Tax-Free Childcare, not universal/EL2 definitions. They do not cure this PR's source-population issues, but they change the TFC outputs checked by the same #472 test, so the four-PR combined model/data versions must be captured in any final calibration evidence.

Source Audit Summary

Category Count
Confirmed correct CSV values/rate 5
Arithmetic errors 0
PDF mismatches 0 (no relevant PDF)
Comparator/definition mismatches 2

The exact official CSV confirms 778,327 universal nursery children, 361,790 working-parent 3–4-year-olds, 115,852 EL2 registered, 154,957 EL2 eligible, and 74.763967% take-up. The PR's subtraction and multiplications are numerically correct; the blockers are what those numbers measure.

Validation Summary

Check Result
Regulatory/source definitions 2 critical mismatches
Reference quality Values corroborated; spending interpretation not corroborated
Code patterns Ruff/check/format and git diff --check passed; Towncrier failed to consume fragment
Focused parameter check Passed: 2023=0.597, 2024/2026=0.748; targets import correctly
Full private-data build Not run; gated/heavy, and no GitHub checks reported
CI status No checks reported on exact head

Review Severity: REQUEST_CHANGES

Next Steps

Use an observed funding allocation/outturn or explicitly redefine the derived amounts; align the January 2024 comparator with the model's 2024 eligibility population; fix the Towncrier path; then run and publish one reproducible combined full-build comparison across #472/#473/#474 and UK #1830/#1834.

@vahid-ahmadi
vahid-ahmadi force-pushed the fix-entitlement-calibration-targets branch from 10e5173 to 971746d Compare August 28, 2026 11:45
@vahid-ahmadi
vahid-ahmadi force-pushed the fix-entitlement-calibration-targets branch 2 times, most recently from 9cd12dd to b412ded Compare August 28, 2026 12:15
@vahid-ahmadi

Copy link
Copy Markdown
Collaborator Author

All three criticals accepted. Two of them changed what this PR does.

C2 — the 74.8% is not transferable. Take-up change removed.

You are right and I verified it. gov.dfe.extended_childcare_entitlement.hours gives 2-year-olds 15 hours from 2024-01-01, and targeted_childcare_entitlement_eligible excludes benefit units qualifying for the extended entitlement. The real working-parent expansion for 2-year-olds began in April 2024, after DfE's January census, so DfE's eligible count of 154,957 contains no corresponding exclusion while the model's base does.

So the 1.02× fit was, as you say, cancellation against a different eligible base rather than validation of 0.748. The rate change is reverted, and the parameter file now records why and what has to be aligned first — starting with that 2024-01-01 date, which looks like it should be April.

That leaves the model at 0.83× on early learning for 2-year-olds after the target correction, which is an honest number rather than a fitted one.

C1 — the spending targets are a modelled maximum, not observations.

Accepted, and relabelled rather than defended. The module now says explicitly that DfE publishes January headcounts of children registered for at least some provision, that it publishes no per-programme spending, and that these figures are the caseloads at the statutory 570 hours and the DfE funding rate — therefore caseload times a constant, and not independent of the caseload targets. Matching both does not separately validate expenditure.

I have not replaced them with an allocation outturn because I could not find a per-programme one. The claim is now the narrower one: an upper bound at the published headcounts, and an improvement on figures that could not be traced at all.

C3 — changelog. Renamed to entitlement-calibration-targets.fixed.md.

A1 — EL2 denominator. Recorded in the parameter file: DfE notes its eligible count omits multiple two-year-olds and several non-economic routes, so the published percentage overstates true take-up, and PolicyEngine models a different subset of those routes again. That is now part of the reason the rate is not transferred.

A2 — references. The module cites the release, names the exact file (headline_figures_feeac_2011_2026.csv), and shows the derivation line by line.

A3, A4 — pinning and integration evidence. Not done, and I want to be straight about why: a combined four-PR build needs a full 512-epoch run against the private FRS, which I cannot do here, and the CI build cannot substitute for the reasons in #472. The ratios I have reported are from the published enhanced_frs_2024_25 at revision 2966541f with the model changes applied locally. That is stated, and it is weaker than release-grade evidence.

Thank you for checking the CSV independently — confirming the subtraction and the 74.763967% is what let me be confident the arithmetic was sound and the problem was in what the numbers measure. That distinction has been the recurring failure mode across this whole set.

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Program Review

Source Documents

Critical (Must Fix)

  1. [C1 STILL OPEN] The £1.396bn and £0.547bn figures remain unsuitable calibration targets. The new commit accurately labels them modeled full-entitlement upper bounds and says they are mechanically redundant with caseload. That disclosure resolves the misleading-source claim, but the values are still fed to calibration as if independent expenditure observations. Constraining weights to both a headcount and that same headcount times a constant double-weights one source and calibrates to full theoretical usage rather than actual hours. Use observed allocation/outturn evidence, apply an explicit usage correction, or remove these spending constraints. — policyengine_uk_data/datasets/childcare/targets.py:65

Should Address

  1. [A2 STILL OPEN] Link the exact downloadable DfE table/methodology, not only the release landing page.
  2. [A3 STILL OPEN] Pin/test the exact model contract used for scheme exclusivity and period dates.
  3. [A4 STILL OPEN] Add exact tests for the changed target values; inherited #472 tests do not assert them.

Resolved Prior Findings

  • [C2 RESOLVED] The PR no longer transfers DfE's January 74.8% rate. It restores 0.597 and documents the April-expansion/model-population mismatch and denominator limitation.
  • [C3 RESOLVED] The .fixed.md fragment is consumed by Towncrier.
  • [A1 RESOLVED] DfE's omitted eligibility routes and upward take-up bias are now documented.

Validation Summary

  • Prior source audit reconfirmed DfE headcounts and arithmetic; the new text now describes the modeled-spend interpretation accurately.
  • Inherited target tests: 6 passed; Ruff passed.
  • Towncrier: renders the Fixed fragment.
  • CI: no checks on the exact head.

Review Severity: REQUEST_CHANGES

Next Steps

Replace the upper-bound spending constraints with observed expenditure or a sourced usage-adjusted model, then add exact target tests and an exact model/data integration run.

@vahid-ahmadi
vahid-ahmadi force-pushed the fix-entitlement-calibration-targets branch from b412ded to 7eb5a62 Compare August 28, 2026 14:05
@vahid-ahmadi

Copy link
Copy Markdown
Collaborator Author

@juaristi22 C1 accepted — the two spending targets are dropped rather than re-documented.

You were right that labelling them as modelled upper bounds did not stop the optimiser treating them as independent evidence. I verified the mechanism before changing anything. takeup_rate.objective is:

for key in targets["spending"]:
    loss += (spending[key] / targets["spending"][key] - 1) ** 2
for key in targets["caseload"]:
    loss += (caseload[key] / targets["caseload"][key] - 1) ** 2

Every entry enters with equal weight. The universal target is 416,537 x 570 x 5.88, i.e. the caseload target times 3,352, and the model pays every recipient of these two schemes the same per-child amount — so the spending ratio equals the caseload ratio and the loop adds the same term twice. That doubled the pull of universal and targeted against Tax-Free Childcare and extended hours on no extra evidence, and because both assume full 570-hour usage they are upper bounds, so the duplicate term pulled weights up.

Tax-Free Childcare and extended spending stay. Neither is caseload times a constant: TFC spending varies with childcare expenditure, extended with maximum_extended_childcare_hours_usage. The loss goes from eight terms to six.

The caseloads are untouched — 417k universal and 116k targeted are still the sourced DfE figures this PR is mainly about. Restoring the spending constraints needs an allocation or outturn source, which is now stated in the module rather than left as an inference.

Also fixed the towncrier type on #472's fragment (childcare-calibration-targets.md.fixed.md), which was rendering "No significant changes" — that was your C3 there. Rebased this branch on the updated #472.

A2/A3/A4 are not addressed in this push.

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator Author

@juaristi22 re-review requested. C1 is actioned by dropping the two spending targets rather than documenting them — I confirmed the equal-weight loop in takeup_rate.objective first. Caseloads are untouched. A2/A3/A4 are not addressed. Are you happy with this, or do you want the A-items in the same PR?

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Program Review

Source Documents

Critical (Must Fix)

  • None.

Should Address

  1. [A2 STILL OPEN] Link the exact downloadable DfE data file or its table URL. The docstring now identifies the CSV precisely, which is much better, but only links the release landing page. A direct source makes the subtraction reproducible after the release UI changes. — policyengine_uk_data/datasets/childcare/targets.py:33
  2. [A3 STILL OPEN] The target mapping still depends on an unpinned cross-repo formula. The 416,537 comparator relies on universal_childcare_entitlement_eligible excluding extended-eligible children. Record/test the compatible model version so a later eligibility refactor cannot silently invalidate the subtraction. — policyengine_uk_data/datasets/childcare/targets.py:46
  3. [A4 STILL OPEN] Tests do not assert the two corrected caseload values. They test target shape and exact TFC values, but a regression from targeted=116 or universal=417 to the old figures would still pass. Add exact assertions tied to the cited DfE counts. — policyengine_uk_data/tests/test_childcare_targets.py:47
  4. [A5 NEW] The Towncrier fragment renders its first paragraph with a duplicated bullet. Remove the initial - so the generated Fixed entry does not begin - - Correct.... — changelog.d/entitlement-calibration-targets.fixed.md:1

Resolved Prior Findings

  • [C1 RESOLVED] Universal and targeted spending constraints are removed. They no longer double-weight caseload evidence or pull calibration toward the full-570-hour upper bound.
  • [C2 RESOLVED] The PR does not transfer DfE's non-comparable 74.8% take-up rate.
  • [C3 RESOLVED] The fixed fragment is consumed by Towncrier.
  • [A1 RESOLVED] DfE denominator limitations and the January/April population mismatch are documented.

Validation Summary

  • Prior source audit verified the DfE headcounts and subtraction; the latest commit removes, rather than reinterprets, the unsound spending constraints.
  • Focused target tests: 7 passed.
  • Ruff: passed on changed Python files.
  • Towncrier: fragment is consumed, with the duplicated-bullet formatting issue above.
  • Exact-head GitHub CI: no checks reported at review time; PR is mergeable.

Review Severity: APPROVE_WITH_COMMENTS

Next Steps

Add exact assertions for the corrected caseloads and clean up source/contract/changelog details; the calibration-target blocker itself is fixed.

vahid-ahmadi and others added 2 commits August 28, 2026 16:36
The universal and early-learning-for-2-year-olds targets were unsourced
"prior DfE values" and neither matched DfE's published data. Corrected
against DfE, "Funded early education and childcare", reporting year 2026,
national figures for January 2024 to match the year the checks evaluate
(data/headline_figures_feeac_2011_2026.csv).

The universal comparator has to net off the working parent entitlement.
universal_childcare_entitlement_eligible in policyengine-uk ends with
`& ~has_extended_childcare`, so the variable covers children on the
universal entitlement *only*, not the 1.13 million headline:

  registered excluding reception                   778,327
  working parent entitlement, 3 and 4-year-olds    361,790
  => universal entitlement only                    416,537

against a prior target of 490,000, which was 1.18x too high. Early learning
for 2-year-olds was 115,852 registered against a prior target of 130,000,
1.12x too high.

The spending figures are a modelled full-entitlement value, not an observed
outturn: DfE publishes headcounts of children registered for at least some
provision, not per-programme spending, so these are the caseloads at the
statutory 570 hours and the DfE funding rate for the age band. They are
therefore caseload times a constant and not independent of the caseload
targets. That is stated in the module. It is an improvement on figures that
could not be traced at all, not a substitute for an allocation outturn.

No take-up change. DfE publishes an early-learning-for-2-year-olds take-up
rate directly, 74.8% in January 2024, but it is not transferable: the model
gives 2-year-olds extended entitlement from 2024-01-01 and excludes
extended-eligible benefit units from the targeted entitlement, while the real
working-parent expansion for 2-year-olds began in April 2024, after DfE's
January census. DfE's eligible count contains no corresponding exclusion, so
applying its rate to the model's smaller base would fit one difference
against another. The reasoning, and the 2024-01-01 date that needs aligning
first, are recorded in the parameter file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011NKBpgpwwY5ZXHUwh1GJQT
Each is its caseload times a constant, so it restated a target the registry
already held. takeup_rate.objective weights every entry equally, so the two
schemes counted twice against TFC and extended hours on no extra evidence.
Both also assumed full 570-hour usage, making them upper bounds.

TFC and extended spending stay: they vary with expenditure and hours used, so
neither is redundant with its caseload.
@vahid-ahmadi
vahid-ahmadi force-pushed the fix-entitlement-calibration-targets branch from 7eb5a62 to 990a762 Compare August 28, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants