Skip to content

Calibrate Microcosm UK to CenTax's parent income tabulations - #1399

Draft
MaxGhenis wants to merge 5 commits into
uk-pair-release-2151from
uk-centax-childcare-parent-targets
Draft

MaxGhenis wants to merge 5 commits into
uk-pair-release-2151from
uk-centax-childcare-parent-targets

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

WIP (draft). Stacked on #1396 (uk-pair-release-2151). Don't merge before #1396.

Adds CenTax's HMRC tabulations of parents' incomes as Microcosm UK national calibration targets (Advani, Flew, Pepin-Hall and Summers 2026, Removing the childcare cliff-edge). The targets set the income distribution of parents of 3- and 4-year-olds in England around the £100k childcare cliff, which the £100k childcare dashboard reads.

What it adds

  • Derived facts. uk/centax_childcare_parent_facts.json holds 9 England cells built from Figures 3.1, 3.3, 3.6 and 3.7. Each cell is CenTax's published share times the baseline build's own parent count. tools/build_uk_centax_childcare_parent_facts.py builds the file, and the workbook's sha256 is pinned (8c238557…).
  • Nine contract targets. They are microcosm.centax_childcare_parents.*, bound on the national line only, with registry-parity accounting:
    • four ANI bands partitioning (0, £200k] in 2021-22 pounds: £0–50k, £50–80k, £80–120k, £120–200k;
    • five splits of (£80k, £150k]: women, main income source other than employment, and London / South excl. London / North and Midlands.
  • Two person measures. These are centax_parent_of_eligible_3_4_year_old and centax_employment_main_income_source. The second weighs the year's pay against every component of the engine's total_income (ITA 2007 s. 23), taking social_security_income benefit by benefit; a tie goes to pay. The resolver also computes adjusted_net_income_annual on the annual-income simulation.
  • One loader for the derived facts. uk_runtime/derived_facts.py merges the high-value home stock and the CenTax cells at every national compile site. Those sites are the full and national target inputs, the release-cut certifier, the compile-parity receipts, the UC matrix diagnostic and the incumbent evaluation. The receipts name both resources.
  • Diagnostic. tools/diagnose_uk_centax_parents.py reports each cell's fit and support on any national H5: records, Kish effective households and the top-ten household share.

Never targets (d2014): CenTax's model outputs. That means the bunching estimates (Tables 3.1, 3.2 and 4.1), frictions, elasticities, marginal bunchers and every cost. Figure 2.1 (childcare-account shares), Figure 3.12 (non-working partners) and Table A.1's levels stay diagnostics inside the resource.

Design calls

  • Shares, not CenTax's levels. Microcosm targets are sums, so each share becomes a count: the share times the baseline's own parents in the band's range at the calibration year. The cells set the shape of the income distribution and leave its level where the DfE caseloads put it. CenTax's level is an identified sample (1,648,632 parents of 3- and 4-year-olds in Table A.1), not a population count.
  • Wide bands around £100k. In 2021-22, with the cliff at £100k, the (£80k, £120k] band held both the parents who bunched below it and those they came from above it. The FRS also has too few effective households above £80k to bind narrower bins (see support below). The five splits cover (£80k, £150k], the widest band that every split figure covers, and the three region groups partition it.
    • Not bunching-neutral after ageing (review r1). Aged to 2025, the band and the split band start at £99,817 while the cliff stays at £100k. So a parent who bunches that year to below £99,817 leaves them for the band below. Their counts assume no bunching under the frozen cliff, and they lean high by about the parents who bunch below that edge. CenTax estimates that 2,610 parents of 3- and 4-year-olds bunched in 2022 (p. 22). A lower middle edge, about £75k in 2021-22 pounds, would hold both years' cliffs. That's a follow-up: it needs another national build.
  • Ageing. The edges are CenTax's 2021-22 pounds aged to the calibration year (2025) by the OBR average earnings index that policyengine-uk carries (×1.2477), rounded to whole pounds. I chose earnings because the spine's incomes are aged that way, so each cell keeps its place in the distribution. CPI would move the edges against the incomes. The shares themselves aren't aged: the shape of 2021-22 is assumed to hold in 2025.
  • Parent identification. CenTax finds parents in three ways: childcare accounts (the 2018–2025 panel), PAYE parental leave (from 2014-15), and address (UPRN) matching of partners. See pp. 12–13 and Appendix A (Figure A.1). So it misses couples with none of these. Those are mostly self-employed or company-owning parents over £100k, where neither partner takes recorded leave or ever opened an account. The report says this overstates parents below £100k relative to those above (p. 13). The upper cells may therefore lean low, and the non-employee split especially.
  • Child definition. A parent is an adult (not an HBAI dependent child) in a benefit unit with a dependent child in England whose DfE age is 3, 4 or 5 and whose early_years_term_share_before_school is above a sixth. DfE age is the model year less birth_year, keyed as uk_runtime/dfe_spring_term.py keys it. The term-share rule means the child is age-eligible for the universal hours in at least one of the year's three terms. CenTax counts it the same way: "eligible for free childcare hours in 2022 for a positive number of months" (Table A.1 notes). The DfE caseload rows read the same age and term share but count the spring term only (a share above one half).
  • Income. HMRC's data count what each parent received in the tax year. So the cells read adjusted_net_income_annual on the annual-income simulation, not the point-in-time ANI.
  • England only. CenTax tabulates England's free hours, so the targets use England's geography pin (TARGET_PREFIX_GEOGRAPHY_PINS).
  • National line only. calibration_lines: ["national"]; the local line drops the rows.

Invariants (tested)

  • The four band shares partition (0, £200k], and so do the three region shares within the split band.
  • Each count is its share times the baseline's parent count (rounded to 0.001), and the four band counts add back to that total.
  • The aged edges stay contiguous and rise with the uprating index.
  • The parent measure marks whole benefit units of adults and is invariant to row order.
  • The main-source measure is monotone in pay and scale-invariant. Every other source can outrank pay. The compared sources are exactly the installed engine's total_income leaves (an engine test).
  • Every source in that comparison is the year's amount. A claimant paid contributory ESA for part of the year, whose current ESA is above the year's pay, counts as an employee (a resolver test that fails on the previous resolver).
  • The diagnostic's support over FRS source households never exceeds its support over Microcosm households. It has fewer units, a lower Kish count and a larger top-ten share, and equals it when each household is its own source (Hypothesis, plus a worked example).
  • The contract binds exactly the resource's cells, on the national line.
  • Every national compile reads the merged rows.
  • The build's materialized target column equals the diagnostic count on all nine cells (a differential check on a6299504; see below).

Hypothesis properties cover the edges, both measures, the share partition on any positive workbook, and the order-independent rows digest.

Status

  • Derived facts, measures, diagnostic
  • 9 contract targets, merged loader, tests
  • Regenerate target_references.json, the membership report and the signed compile-parity receipt. These are additions only: nine references, nine membership entries, nine ledger_only receipt rows. Each feed read peaked at 27 GB and took 11–13 min. They're committed locally and not pushed yet.
  • Independent review r1 (GPT-6.1 Sol): request changes. The major finding was that other investment and miscellaneous income were left out of the main-source comparison; it's fixed in 85181c8. The minor finding, the bunching claim after ageing, is corrected above and in the selector notes.
  • Independent review r2 (GPT-6.1 Sol): approve. It raised one unconfirmed concern: the main-source measure read the contributory benefits at their current amounts, not the year's. That is fixed in ea2e3d6ed (local). On both nationals, the nine cells and every parent aggregate are identical under the old and new measure.
  • Local national build with the new rows (1,277 targets). It is never staged or certified, and its gate report is signed with a throwaway key.
    • All nine rows fit within 0.5% of their targets.
    • Of the 1,199 published shared targets, the same ones are within 10% as on a6299504, and none moved by more than a point. Effective sample size fell 1.2%.
    • The before/after also carries a locally rebuilt spine and a newer Python. The noise baseline that would separate those runs after 10/29.
  • The diagnostic now counts support over FRS source households (c35cd4eb5, local). The high-income cells rest on few source households. The non-employee cell rests on fewer than ten, so the build withholds its figures; it stays bound under the 2026-10-21 rule.
  • Dashboard static cost before and after, local only. The numbers are for Max's review and are not published here; the live dashboard is unchanged (d2013).
  • Independent review r3 (GPT-6.1 Sol) of ea2e3d6ed and c35cd4eb5: approve with nits. It found no production-code defect. Its one P3, that the top-ten source share was not pinned apart from the household share, is fixed in ad0b2ee24 (local), and the new test fails on that mutant.
  • Push 3b9941603, ea2e3d6ed, c35cd4eb5 and ad0b2ee24. They are held until a ruling on where the derived workbook values live.
  • María's sign-off; land after Build the national + local-area pair on policyengine-uk 2.151.0, and drop UK model-figure withholding #1396

axiom: n/a: microsim calibration targets only, no policy rule changes.

🤖 Generated with Claude Code

MaxGhenis and others added 5 commits October 25, 2026 13:45
CenTax's HMRC tabulations of English parents of 3- and 4-year-olds
(Advani, Flew, Pepin-Hall and Summers 2026, Removing the childcare
cliff-edge, Figures 3.1, 3.3, 3.6 and 3.7, tax year 2021-22) become nine
derived facts in uk/centax_childcare_parent_facts.json. Four are ANI band
shares of the parents in (0, £200k]: (0, 50k], (50k, 80k], (80k, 120k]
and (120k, 200k]. Five split the parents in (£80k, £150k]: women, a main
income source other than employment, London, the South excluding London,
and the North and Midlands. Each share is multiplied by the baseline
build's own parent count, so the targets set the shape and leave the
level to the DfE caseloads.

uk_runtime/centax_childcare_parents.py holds the cells, the edges (the
2021-22 edges aged by policyengine-uk's OBR average earnings index, 2025
over 2021, which is 1.24772), the loader's checks, and two measures:
- centax_parent_of_eligible_3_4_year_old: an adult of a benefit unit with
  a dependent child in England, DfE age 3 to 5, before school in at
  least one term;
- centax_employment_main_income_source: employment is the year's largest
  income source.
The measure resolver computes both and refuses a persisted column of
either. adjusted_net_income_annual joins the annual income measures,
because HMRC's data count the tax year's income.

tools/build_uk_centax_childcare_parent_facts.py reads the pinned
workbook (sha256 8c238557…). tools/diagnose_uk_centax_parents.py
reports each cell's fit and household support on any national H5. The
parent count, 2,165,344, comes from its run on the a6299504 staged
national.

Never targets: CenTax's model outputs, which are the bunching tables,
frictions, elasticities, marginal bunchers and costs. The Table A.1
levels and the Figure 2.1 and 3.12 shares are kept as diagnostics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nine targets (microcosm.centax_childcare_parents.*) bind the derived
CenTax facts on the national line only, at England grain: four adjusted
net income bands and five splits of (£80k, £150k]. One loader,
uk_runtime/derived_facts.py, merges them with the high-value home stock
at every national compile site, and the target-input receipts name both
resources. The reference generator reads the new resource, pins the
family to England and streams the feed line by line. The compile-parity
receipt tool marks the nine rows ledger_only with a stated rationale.

Tests cover the resource, the two person measures, the contract and the
merged compile sites, with Hypothesis properties for the edges, the
measures and the share partition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w CenTax finds parents

The plain-column review test lists every variable a contract binding reads as
a filter or band. The two CenTax measures are new there. Neither is a stored
read: the resolver refuses them as frame inputs, computes the parent flag from
the engine's term shares at the calibration period, and computes the main
income source from the annual measures. So they join the reviewed set.

Each CenTax target's selector note now says how CenTax identifies parents
(childcare accounts, PAYE parental leave, address matching of partners) and
that this leaves out some parents above £100k, so the upper cells may lean low.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… correct the bunching claim

Review r1 (GPT-6.1 Sol) found the main-source measure left out other
investment income and taxable miscellaneous income. A parent with £30,000 of
pay and £100,000 of other investment income counted as an employee, so the
non-employee cell undercounted. The measure now weighs pay against every
component of the engine's total_income (ITA 2007 s. 23), with
social_security_income taken benefit by benefit. An engine test pins the
compared set to the installed engine's own total_income leaves. A parametrized
test checks that each source can outrank pay, and a resolver regression covers
other investment income.

The selector notes said the (£80k, £120k] band's count does not depend on
bunching. That held in 2021-22, when the cliff sat inside the band. Aged to
2025, the band and the split band start at £99,817 while the cliff stays at
£100k, so a parent who bunches below that edge leaves them. The notes now say
so, and size the lean with CenTax's 2,610 bunching parents of 3- and
4-year-olds in 2022 (p. 22).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22

juaristi22 commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

@vahid-ahmadi could you email Arun Advani and ask whether we may publish the values this PR derives from CenTax's workbook (AdvaniFlewPepinhallSummers2026_ChildcareThreshold_data.xlsx, which he sent you on 21 October)? The nine targets are built from its shares for Figures 3.1, 3.3, 3.6 and 3.7, and uk/centax_childcare_parent_facts.json also records the bin sums and the Figure 3.12 shares. CenTax's report page links only the PDF, so the workbook itself isn't public.

Workbook recommendation (María): if Arun agrees, the workbook goes into Chronicle as a CenTax source, the way the high-value home inputs (HM Land Registry's Price Paid Data and the NSPL) are registered there. Microcosm then keeps only its own derivation: each share times the build's own parent count, with the band edges aged. That follows our rule that publisher numbers come from Chronicle, not from files copied into microcosm.

@MaxGhenis this recommendation is for you too. Could you also push your four local commits (3b9941603, ea2e3d6ed, c35cd4eb5 and ad0b2ee24) to this branch, so the rebase on current main can start from them?

Design sign-off (María, 3 November): the design calls in the description are signed off:

  • CenTax's shares, not its levels;
  • the four ANI bands up to £200k and the five splits of (£80k, £150k];
  • the edges aged from 2021-22 by the OBR average earnings index, with the 2021-22 shares held;
  • the child definition (DfE age and the term share);
  • annual ANI;
  • England only, on the national line only.

Not signed yet: the bunching edge after ageing, the couples CenTax's linkage misses, the non-employee cell's support and the fit evidence. They wait for a build on current main.

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Review of 85181c8. I looked at the derivation (share times the baseline's parent count), the bands and how they are aged, how the five splits are defined, the parent and main-source measures, the loader, the build tool and the tests. I checked the figure definitions only against the public PDF (Advani, Flew, Pepin-Hall and Summers 2026). I did not open the workbook. The head is stale and four commits aren't pushed yet, so I kept to points that should survive the rebase. A full re-review can follow once those commits are pushed and the branch is rebased. I don't reopen María's signed-off design calls, and I leave her four open items (the bunching edge after ageing, couples the linkage misses, non-employee support, fit evidence) to her list.

Must

  1. The parent count includes adults that HMRC's frame cannot contain. See uk_runtime/centax_childcare_parents.py:214-242 and tools/diagnose_uk_centax_parents.py:226. CenTax's parents are taxpayers found through childcare accounts, PAYE parental leave or UPRN matching. The UPRN data are built from PAYE RTI and SA addresses (pp. 12-13 and pp. 54-55). A partner with no PAYE or SA record is therefore outside CenTax's sample. That covers a partner whose only ANI is savings interest, a taxable DWP benefit such as Carer's Allowance, or contributory ESA. parent_count counts every adult of the benefit unit with 0 < adjusted_net_income_annual <= 249,543, and those people almost all sit in the bottom band. Each share is then applied to that larger count, so every upper cell is scaled up with it. The model count is 2,165,344, against CenTax's 1,648,632 identified parents (Table A.1). The gap isn't all out-of-frame adults, but every 100,000 of them raises all nine targets by 4.6% (100,000 / 2,165,344). That includes the (£80k, £120k] band, the one the £100k dashboard reads. Suggested fix: have the diagnostic report the weighted parents in (0, £62,386] with no employment, self-employment or private pension income in the year. If that number is material, either (a) require a PAYE or SA footprint in centax_parent_of_eligible_3_4_year_old, or (b) take the shares within (£50k, £200k] in 2021-22 pounds, scale them by the model's parents above £62,386, and keep ani_0_to_50k as a diagnostic rather than a target. Option (b) also helps with item 2.

Should

  1. The four bands bind a level, and it comes from a6299504. See tools/build_uk_centax_childcare_parent_facts.py:394,415-424 and the loader at centax_childcare_parents.py:456-553. All four bands are bound and they partition (0, £249,543], so together they fix England's total parents in that range at a6299504's 2,165,344. That is a level target, not just a shape. After the rebase (new spine, Build the national + local-area pair on policyengine-uk 2.151.0, and drop UK model-figure withholding #1396, the DfE rows) the total still comes from the old build. Example: if the post-rebase baseline has 2.10m such parents, the fit is pulled up 3.1% to reach the old total, and most of that pull lands on the bottom band. If the tool is rerun on each release instead, the total just follows whatever the previous build produced. The loader checks the edges, the uprating and that the share arithmetic adds up, but nothing ties inputs.baseline.sha256 to the build being calibrated. Suggested fix: rebuild the resource on the post-rebase baseline. Then add a compile or certifier check that the current baseline's count is within a set tolerance of parameters.parent_count, or refuse when inputs.baseline isn't the incoming baseline. Leaving one band unbound, as in 1(b), keeps the bottom of the distribution free. The nine rows carry no tolerance or weight of their own, so they enter on the default loss like the other national rows. That makes the post-rebase before/after on the 1,199 shared targets (your 1.2% ESS figure) the evidence to rerun.

  2. The region cells rest on a grouping the PDF doesn't give, and on pooled nominal bands. See centax_childcare_parents.py:120-144 and build_uk_centax_childcare_parent_facts.py:200-211,224. The Figure 3.7 notes (p. 25) give the sample (England and Scotland, pooled 2019-2022) but don't say which regions make up "South excl. London" or "North and Midlands". The East of England's place is inferred from the ONS child-population check in the module comment. If the East sits in North and Midlands, the South cell is too high and the North and Midlands cell too low by the East's whole share of the band. The pooling also applies the nominal (£80k, £150k] band to four tax years of earnings growth, while the split band's total comes from 2021-22 alone (Figure 3.1). Suggested fix: say in the selector notes that the grouping is inferred, and have the diagnostic report the three region cells under the other placement of the East. Until the grouping is confirmed, consider binding London only, which the figure does name.

Nit

  1. CENTAX_SPLIT_BAND_2022 (centax_childcare_parents.py:119): the £150k top edge was the additional-rate threshold in 2021-22. Figures 3.3 and 3.7 both show a spike in the last bin. Aged to £187,158 it marks no 2025-26 threshold, while the current £125,140 additional-rate threshold falls inside the band. The effect is small (one bin of extra mass), but it belongs next to the £100k note in the selector notes.
  2. employment_main_income_source (centax_childcare_parents.py:245-263): the PDF (Figure 3.3 notes, p. 21) says only "main source of income is from employment". It doesn't say whether that means the largest single source or more than half of income. The measure uses the largest single source. A parent with £60k pay, £50k dividends and £30k profit is an employee under that rule but not under a half rule. Worth one line in the notes saying the PDF leaves this open.
  3. SHEETS (build_uk_centax_childcare_parent_facts.py:70-98): the PDF notes say the bins are £1,000 wide for Figures 3.1 and 3.6, but the tool reads £500 steps for both. The PDF also doesn't say whether bins are labelled by their upper or lower edge. test_band_sums_take_bins_by_their_upper_edge tests the code's convention, not the workbook's. The edges at £80k, £120k and £200k avoid the spikes, so the other convention would move each cell by one bin. A comment on both points would save the next reader from checking the code against the PDF.
  4. fact_row (build_uk_centax_childcare_parent_facts.py:313-321): period_coverage says 2025-01-01 to 2025-12-31 with basis "flow". The contract rows say fiscal_year_flow, and the measure is the 2025-26 tax year's ANI. The high-value-homes rows use calendar dates, but they record a stock. Either use the tax year's dates or note that 2025 is the engine year.
  5. As María noted, the resource also records workbook_sums, the Figure 3.12 means and the Table A.1 levels (build_uk_centax_childcare_parent_facts.py:214-278,467-468). Only diagnose_uk_centax_parents.py:256 reads them, and nothing on the build path does. They could go in the diagnostic's own output, which would leave the resource holding only what the loader validates.

Checked and fine

  • The aged edges: 50k, 80k, 120k, 150k and 200k times 1.24772 give 62,386, 99,817, 149,726, 187,158 and 249,543. The split edges match the band edges, and the loader refuses edges that don't.
  • band_sum takes (lower, upper] on both the £500 and £1,000 steps. max(fig31) == 200k is guarded, and so are the series reader's header and gap checks.
  • Year conventions: "2022" is 2021-22 (p. 2). Figures 3.1, 3.3 and 3.6 are single-year and 3.7 is pooled, as the module says. Only the English columns of Figure 3.7 enter the region shares.
  • The child definition matches CenTax's ("eligible ... for a positive number of months", Table A.1 notes). A DfE age 2 child can't qualify within the tax year, and the term-share floor at a sixth tests float32 thirds safely.
  • The female share within the split band agrees with Table A.1's 26.6% for £80k-£140k. Each split is the split-band share times the within-band share, and the three region cells add up to the split band.
  • Each fact key's digest covers the workbook sha256, the baseline sha256 and the method, so rebuilding on a new baseline changes the keys.
  • Tests: the PR's engine-free tests pass locally at the head. That's test_uk_centax_childcare_parents.py, test_uk_measure_simulation.py, test_uk_population_targets.py, test_uk_full_targets.py, test_uk_high_value_homes_region_control.py, test_uk_measurement_period.py and shared/test_country_spec.py, 313 passed. I did not run the engine test test_uk_centax_main_income_source.py (no policyengine-uk in my environment). I also skipped a merge-base comparison, because the branch conflicts with its base.

CI at 85181c8: no workflow runs; combined status pending with no checks reported.

Verdict: the shares-times-count derivation is clean and well guarded. Before the rebased build is judged on fit, two things should be settled: what population parent_count counts (1) and which baseline it comes from (2).

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
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.

3 participants