Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1417-review-followups.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Follow-ups from the reviews of microcosm#1417, #1109 and #1416. `tools/check_uk_itl_projections.py` refuses `--current-income-for-annual-measures` unless `--diagnostics` or `--references` names the in-sample basis: this tree's target references say what a build of this tree binds, not what a file built before the annual income descriptors bound, so on that basis every band and area cell of such a file read `in` at the default `--year`. It also refuses an empty reason, which turned the current-income route off, and the receipt records the reason. `uk_runtime.itl_differential` names the engine variables its cells read (`TAX`, `TOTAL_INCOME`, `IS_SP_AGE`, `AGE`). New engine-free tests hold the contract's three bound `policyengine` bindings to what the check reads, every key but `notes` and `metric_name`, and run the build's own `materialize_target_bindings` on a `UKFrameTargetAdapter` against the check's masks for all 35 bound cells, band edges from the compiled spec and area geography included. An edit to a binding's filter, band variable or band reading (whole pence, an upper bound, a period factor) therefore fails until the check follows it. The note on `NATIONAL_TOLERANCE` cites the vendored band counts (at the calendar-2025 window five of the eleven bands hold under 400,000 taxpayers, and the top three hold fewer than the smallest area cell) in place of a claim that national cells sum over millions, and a test reads the note against the rows; the tolerances stay 5% and 10%. The check imports `UK_HMRC_ITL_UK_GEOGRAPHY_ID` instead of repeating it. The US fiscal refresh's reviewed `estate_income` exclusion also asks for re-adjudication when the flag is absent and the raw base becomes the reference, and the `materialize_target_bindings` docstring limits the plain-row period refusal to a row that declares a period other than the calibration period.
Original file line number Diff line number Diff line change
Expand Up @@ -716,8 +716,8 @@ def materialize_target_bindings(
A provider binding reads its columns as the frame holds them, so a
period it declares must be one its value reads hold. A declared period
that any one read misses refuses the run: for a provider row on any
frame with a period model, for a plain row on a frame with period
metadata.
frame with a period model, and for a plain row that declares any period
but the calibration period, on a frame with period metadata.
"""

provider_registry = {**default_provider_registry(), **(providers or {})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,37 @@
import numpy as np

from microcosm.build.ledger_targets import CALENDAR_YEAR_WINDOW_WEIGHTS
from microcosm.build.uk_runtime.hmrc_uprating import ITL_BANDS
from microcosm.build.uk_runtime.hmrc_uprating import (
ITL_BANDS,
UK_HMRC_ITL_UK_GEOGRAPHY_ID,
)
from microcosm.calibrate.geography_constants import UK_REGION_TIER

#: The engine variables a cell reads, as the contract's ``policyengine``
#: bindings of the ``BOUND_BY`` targets name them: every cell counts people
#: whose ``TAX`` exceeds zero, a band reads ``TOTAL_INCOME``, the counts of
#: and below State Pension age read ``IS_SP_AGE`` and the count aged 65 and
#: over reads ``AGE``. ``resolved_people`` reads them. The tests hold them to
#: the bindings, and each bound cell's mask to what the build materializes
#: for its target.
TAX = "income_tax_before_winter_fuel_payment_charge_annual"
TOTAL_INCOME = "total_income_after_loss_relief_annual"

#: Largest relative miss a cell may show: national cells are sums over
#: millions of weighted taxpayers, a region's over a twelfth of them.
IS_SP_AGE = "is_SP_age"
AGE = "age"

#: Largest relative miss a cell may show: ``REGIONAL_TOLERANCE`` for each
#: Table 2.2 area, ``NATIONAL_TOLERANCE`` for every other cell (Table 2.5's
#: bands and all-band count, Table 2.1's United Kingdom counts, the count
#: below State Pension age and the residents' sums). Geography, not size,
#: decides which: at the calendar-2025 window of the vendored rows, five of
#: the eleven bands hold under 400,000 taxpayers (342,750 at £150,000 to
#: £200,000, 372,500 at £200,000 to £500,000, 65,500 at £500,000 to £1m,
#: 20,750 at £1m to £2m and 10,750 at £2m and over), and the top three hold
#: fewer than the smallest area cell (Northern Ireland's State Pension age
#: count, 198,750).
NATIONAL_TOLERANCE = 0.05
REGIONAL_TOLERANCE = 0.10

UNITED_KINGDOM = "K02000001"
#: The ``key`` of the sum over the twelve region-tier areas.
RESIDENTS = "residents"
AGE_MEASURES = ("state_pension_age", "age_65_and_over")
Expand Down Expand Up @@ -102,7 +121,7 @@ def key(self) -> str:
if self.band is not None:
lower, upper = self.band
return f"{lower}+" if upper is None else f"{lower}-{upper}"
return self.area or UNITED_KINGDOM
return self.area or UK_HMRC_ITL_UK_GEOGRAPHY_ID

@property
def sort_key(self) -> tuple[Any, ...]:
Expand Down Expand Up @@ -178,9 +197,9 @@ def _read_by_cells(row: Mapping[str, Any]) -> bool:
measure = str(row.get("measure_id"))
geography = str((row.get("geography") or {}).get("id"))
if table == "2.5":
return measure == _TABLE_2_5_COUNT and geography == UNITED_KINGDOM
return measure == _TABLE_2_5_COUNT and geography == UK_HMRC_ITL_UK_GEOGRAPHY_ID
if table == "2.1":
return measure in AGE_MEASURES and geography == UNITED_KINGDOM
return measure in AGE_MEASURES and geography == UK_HMRC_ITL_UK_GEOGRAPHY_ID
return (
table == "2.2" and measure in AGE_MEASURES and geography in _REGION_TIER_CODES
)
Expand Down Expand Up @@ -333,16 +352,16 @@ def need(key):
cell(
"2.5",
BAND_COUNT,
need(("2.5", _TABLE_2_5_COUNT, UNITED_KINGDOM, band)),
need(("2.5", _TABLE_2_5_COUNT, UK_HMRC_ITL_UK_GEOGRAPHY_ID, band)),
band=band,
)
for band in ITL_BANDS
]
all_taxpayers = need(("2.5", _TABLE_2_5_COUNT, UNITED_KINGDOM, None))
all_taxpayers = need(("2.5", _TABLE_2_5_COUNT, UK_HMRC_ITL_UK_GEOGRAPHY_ID, None))
all_cell = _summed(cell("2.5", ALL_TAXPAYERS, all_taxpayers), cells)
cells.append(all_cell)
for measure in AGE_MEASURES:
national = window.get(("2.1", measure, UNITED_KINGDOM, None))
national = window.get(("2.1", measure, UK_HMRC_ITL_UK_GEOGRAPHY_ID, None))
areas = [
window.get(("2.2", measure, code, None)) for _level, code in UK_REGION_TIER
]
Expand Down Expand Up @@ -577,7 +596,7 @@ def resolved_people(resolver: Any) -> ITLPeople:
weight=_person(resolver, "household_weight").astype(float),
taxpayer=_person(resolver, TAX).astype(float) > 0,
total_income=_person(resolver, TOTAL_INCOME).astype(float),
state_pension_age=_person(resolver, "is_SP_age").astype(bool),
age=_person(resolver, "age").astype(float),
state_pension_age=_person(resolver, IS_SP_AGE).astype(bool),
age=_person(resolver, AGE).astype(float),
area=np.array([by_name.get(name, "") for name in region]),
)
Loading
Loading