Correct the entitlement calibration targets to DfE published figures - #474
Correct the entitlement calibration targets to DfE published figures#474vahid-ahmadi wants to merge 2 commits into
Conversation
juaristi22
left a comment
There was a problem hiding this comment.
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-256cefdb06593016446d215433cf31f80649b8ce18f89e454a5ad7931b0ab32d72d - 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)
- [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 - [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, andtargeted_childcare_entitlement_eligibleremoves 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 - [C3] The changelog entry is not consumed by Towncrier. The repository config expects fragments under
breaking/,added/,changed/,fixed/, orremoved/;towncrier build --draftrendered “No significant changes.” Move/rename the fragment to a configured type. —changelog.d/entitlement-calibration-targets.md:1
Should Address
- [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 - [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 - [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 - [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=1smoke 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.
10e5173 to
971746d
Compare
9cd12dd to
b412ded
Compare
|
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. 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 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 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 ( 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 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
left a comment
There was a problem hiding this comment.
Program Review
Source Documents
- DfE release: Funded early education and childcare, reporting year 2026
- Methodology: DfE funded early education and childcare methodology
- Reviewed head SHA:
b412ded9205c3ceb5d5a8376be73affc921467a2 - Mode: full after force-push, plus incremental review from
971746d8bec086363acf02154ecb111829e733b8; base is #472 head0853bab4b74cd92a2349990c782d43cea6a1f51b
Critical (Must Fix)
- [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
- [A2 STILL OPEN] Link the exact downloadable DfE table/methodology, not only the release landing page.
- [A3 STILL OPEN] Pin/test the exact model contract used for scheme exclusivity and period dates.
- [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.mdfragment 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.
b412ded to
7eb5a62
Compare
|
@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. 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) ** 2Every entry enters with equal weight. The universal target is Tax-Free Childcare and extended spending stay. Neither is caseload times a constant: TFC spending varies with childcare expenditure, extended with 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 ( A2/A3/A4 are not addressed in this push. |
|
@juaristi22 re-review requested. C1 is actioned by dropping the two spending targets rather than documenting them — I confirmed the equal-weight loop in |
juaristi22
left a comment
There was a problem hiding this comment.
Program Review
Source Documents
- DfE release: Funded early education and childcare, reporting year 2026, January 2024 national figures in
headline_figures_feeac_2011_2026.csv. - Methodology: DfE funded early education and childcare methodology.
- Reviewed head SHA:
7eb5a62e929ddff870b45f7c4be9c80ea9ae2f5c - Mode: full patch review against stacked base
af5867e399d14ca11c367727c4be9c80ea9ae2f5c, plus semantic comparison from reviewed headb412ded9205c3ceb5d5a8376be73affc921467a2.
Critical (Must Fix)
- None.
Should Address
- [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 - [A3 STILL OPEN] The target mapping still depends on an unpinned cross-repo formula. The 416,537 comparator relies on
universal_childcare_entitlement_eligibleexcluding 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 - [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=116oruniversal=417to the old figures would still pass. Add exact assertions tied to the cited DfE counts. —policyengine_uk_data/tests/test_childcare_targets.py:47 - [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.
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.
7eb5a62 to
990a762
Compare
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.csvin 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_eligiblein 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: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):
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:
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
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.