diff --git a/.gitignore b/.gitignore index 9a741bc49..70d8396d3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,10 @@ **/_build !policyengine_uk_data/storage/*.csv **/version.json +# Build output: household weights by local area, derived from FRS microdata +policyengine_uk_data/storage/local_geography_weights.csv.gz + +.brma-cache/ + +# Hypothesis example database (property-based tests) +.hypothesis/ diff --git a/changelog.d/brma-census-weights.fixed.md b/changelog.d/brma-census-weights.fixed.md new file mode 100644 index 000000000..ca02ec8f4 --- /dev/null +++ b/changelog.d/brma-census-weights.fixed.md @@ -0,0 +1 @@ +- Draw FRS households' Broad Rental Market Areas in proportion to census private-rented households by BRMA and bedrooms, instead of the row counts of the 2019-20 LHA list of rents, whose Scottish, Welsh and Northern Ireland lists were copies of English BRMAs' lists (uk-data#515). Remove `lha_list_of_rents.csv.gz`. diff --git a/changelog.d/frs-claimant-or-partner.added.md b/changelog.d/frs-claimant-or-partner.added.md new file mode 100644 index 000000000..b20eb8332 --- /dev/null +++ b/changelog.d/frs-claimant-or-partner.added.md @@ -0,0 +1 @@ +Supply `is_claimant_or_partner`, each benefit unit's single adult or couple, from FRS adult-table membership, so policyengine-uk reads the survey's roles instead of inferring them from ages. SPI records and the public transfer dataset (`enhanced_cps_2025.h5`, column added in place) carry it too, and stacking refuses to combine a person table that has it with one that does not. diff --git a/changelog.d/frs-cvpay-not-property-income.fixed.md b/changelog.d/frs-cvpay-not-property-income.fixed.md new file mode 100644 index 000000000..555464200 --- /dev/null +++ b/changelog.d/frs-cvpay-not-property-income.fixed.md @@ -0,0 +1 @@ +Stop counting the rent a boarder or lodger pays (FRS CVPAY) as that boarder's or lodger's own property income. diff --git a/changelog.d/frs-empstati-other-inactive.fixed.md b/changelog.d/frs-empstati-other-inactive.fixed.md new file mode 100644 index 000000000..f46130465 --- /dev/null +++ b/changelog.d/frs-empstati-other-inactive.fixed.md @@ -0,0 +1 @@ +Map FRS EMPSTATI code 11 ("Other inactive") to `employment_status` OTHER_INACTIVE; it had fallen through to LONG_TERM_DISABLED, which also put other-inactive adults into the ESA health-condition and support-group proxies. An adult EMPSTATI code the mapping does not know now fails the build instead of defaulting. diff --git a/changelog.d/frs-pc-reported-capital.changed.md b/changelog.d/frs-pc-reported-capital.changed.md new file mode 100644 index 000000000..d6433759f --- /dev/null +++ b/changelog.d/frs-pc-reported-capital.changed.md @@ -0,0 +1 @@ +Carry each benefit unit's FRS total capital (TOTCAPB4, DWP's current benefit-unit savings and investments measure; TOTCAPB3 for earlier survey years) into policyengine-uk's `pension_credit_reported_capital`, so Pension Credit's capital test uses the survey's own benefit-unit capital instead of imputed household wealth. diff --git a/changelog.d/frs-property-losses-subrent.fixed.md b/changelog.d/frs-property-losses-subrent.fixed.md new file mode 100644 index 000000000..097b15f2c --- /dev/null +++ b/changelog.d/frs-property-losses-subrent.fixed.md @@ -0,0 +1 @@ +Stop counting FRS property losses (ROYYR1 with RENTPROF = 2) as property income, and count rent from sub-letting part of the home (SUBRENT) for every tenure, not only owner-occupiers. diff --git a/changelog.d/frs-uc-gainful-self-employment.fixed.md b/changelog.d/frs-uc-gainful-self-employment.fixed.md new file mode 100644 index 000000000..34363c66f --- /dev/null +++ b/changelog.d/frs-uc-gainful-self-employment.fixed.md @@ -0,0 +1 @@ +Set policyengine-uk's Universal Credit input uc_is_in_gainful_self_employment from the FRS, as a survey proxy for UC Regs 2013 reg 64 that overrides the model's income-based default: true for every adult whose main job (EMPSTATI) is self-employment, including traders who break even or make a loss, and for anyone whose self-employment profit is above their employment income (ADM H4034); false otherwise. The SPI copy re-derives it from its own imputed incomes. diff --git a/changelog.d/hb-dwp-age-targets.changed.md b/changelog.d/hb-dwp-age-targets.changed.md new file mode 100644 index 000000000..c36ead1fc --- /dev/null +++ b/changelog.d/hb-dwp-age-targets.changed.md @@ -0,0 +1 @@ +Calibrate Housing Benefit to DWP's Great Britain spending and claims by age group, replacing the OBR Housing Benefit target that the model compared with UK-wide Housing Benefit. Pension-age Housing Benefit is calibrated to DWP's figures over Pension Credit qualifying age. Working-age Housing Benefit is calibrated to DWP's figures under that age less all supported and temporary accommodation Housing Benefit, for which policyengine-uk has no rules (£585m and 107k claims in 2025-26, against £5.78bn and 460k in total); treating the model's working-age Housing Benefit as general needs is an approximation. Modelled GB Housing Benefit for 2025-26 falls from about £11.7bn to £8.5bn (DWP: £12.9bn), because pension-age Housing Benefit was about 1.6 times DWP's figure while about £5.2bn of supported and temporary accommodation Housing Benefit remains unmodelled as such. Stop setting `would_claim_uc` for benefit units whose adults have all reached State Pension age, and build with policyengine-uk 2.102.5 so pension-age families can make new Housing Benefit claims. diff --git a/changelog.d/hmrc-salary-sacrifice-relief-targets.fixed.md b/changelog.d/hmrc-salary-sacrifice-relief-targets.fixed.md new file mode 100644 index 000000000..f7fb4a92f --- /dev/null +++ b/changelog.d/hmrc-salary-sacrifice-relief-targets.fixed.md @@ -0,0 +1 @@ +Restore the HMRC salary sacrifice income tax and NICs relief calibration targets. They had been lost since the July 2026 private pension statistics release, when the old HMRC CSV was withdrawn (410 Gone). The targets now come from the 2024-25 Tables 6.1 and 6.2, mapped to year 2024. A committed copy is used when the download fails, and a table for another tax year or without the expected rows fails the build. Income tax relief is the rise in tax on pay under the person's rUK or Scottish rates, relieved at each rate the sacrifice straddles; it had been assigned whole to one band by comparing adjusted net income with taxable-income thresholds. The NICs targets follow the Class 1 rates, including the employer rate rise to 15% from April 2025. The OBR-labelled NICs relief targets, which repeated HMRC's 2023-24 figures, and the separately rounded income tax relief total are removed. diff --git a/changelog.d/oa-region-constrained.fixed.md b/changelog.d/oa-region-constrained.fixed.md new file mode 100644 index 000000000..740395723 --- /dev/null +++ b/changelog.d/oa-region-constrained.fixed.md @@ -0,0 +1 @@ +Draw each cloned household's Output Area from its own FRS region rather than from anywhere in its country, so `region_code_oa`, `la_code_oa` and `constituency_code_oa` no longer contradict `region` (88.5% of English households in release 1.57.4 carried an OA from another region). diff --git a/changelog.d/obr-fallback-non-workbook.fixed.md b/changelog.d/obr-fallback-non-workbook.fixed.md new file mode 100644 index 000000000..4ec774889 --- /dev/null +++ b/changelog.d/obr-fallback-non-workbook.fixed.md @@ -0,0 +1 @@ +Fall back to the committed OBR EFO workbooks when obr.uk answers 200 with a body that is not a workbook (such as its HTML "No Access" page), not only when the request fails. The parse error used to escape the fallback, and `get_targets()` dropped every target parsed from that workbook (the nine OBR receipts and NICs targets in the case observed), logging the error while the build carried on. A body openpyxl cannot read is now treated as a permanent failure: it is not retried, and a warning names the URL, the status and the parse error. diff --git a/changelog.d/obr-uc-welfare-cap-targets.fixed.md b/changelog.d/obr-uc-welfare-cap-targets.fixed.md new file mode 100644 index 000000000..41872c214 --- /dev/null +++ b/changelog.d/obr-uc-welfare-cap-targets.fixed.md @@ -0,0 +1 @@ +Calibrate universal credit to one OBR total for Great Britain, the sum of EFO table 4.9's rows inside and outside the welfare cap. The outside-the-cap row is DWP's UC equivalent of JSA (the Intensive Work Search group), not UC for households the benefit cap leaves alone: computed that way, it asked for £12.9bn of nearly all UC (estimate £72bn, +460%), and the inside-the-cap row was compared with all UK UC. Also count the open top band of the DWP UC payment distribution ("£2500.01 or over" a month), which parsed to missing bounds so its four targets (1.4k to 83k households) could never be met, and make the bands meet without gaps, so awards of exactly a band's top, or a penny a month after deductions, land in their Stat-Xplore band. diff --git a/changelog.d/pc-takeup-over-eligible.fixed.md b/changelog.d/pc-takeup-over-eligible.fixed.md new file mode 100644 index 000000000..ab1eb650d --- /dev/null +++ b/changelog.d/pc-takeup-over-eligible.fixed.md @@ -0,0 +1 @@ +Solve Pension Credit take-up over entitled benefit units in Great Britain, at DWP's FYE 2024 caseload take-up of 62%, after the imputations so entitlement reflects imputed capital. Entitled non-reporters, including in Northern Ireland, are drawn at the solved probability. Units with no entitlement in the calibration year are drawn at DWP's Savings Credit-only caseload take-up (37% in FYE 2024), the rate at which units a reform or a later year newly entitles claim. Reports on SPI-synthetic households, which are imputed from SPI incomes rather than observed, no longer anchor take-up. Calibrate Pension Credit to DWP's Great Britain spending and caseload, replacing the OBR target that the model compared with UK-wide Pension Credit. diff --git a/changelog.d/spi-income-earnings-groups.fixed.md b/changelog.d/spi-income-earnings-groups.fixed.md new file mode 100644 index 000000000..af1530c13 --- /dev/null +++ b/changelog.d/spi-income-earnings-groups.fixed.md @@ -0,0 +1 @@ +Draw SPI incomes for the enhanced FRS's SPI-synthetic rows within earnings groups (employee, self-employed, both, neither) set by each row's FRS employment status, so employees draw pay, the self-employed draw a trade, people out of work draw no earnings, and children keep their own (zero) incomes (#504). Calibrate to the ONS LFS counts of employees and the self-employed, and report rather than train on HMRC's local counts of taxpayers with employment income, which are annual and include part-year earners the FRS records as out of work. diff --git a/changelog.d/spi-synthetic-reported-benefits.fixed.md b/changelog.d/spi-synthetic-reported-benefits.fixed.md new file mode 100644 index 000000000..a10ad0154 --- /dev/null +++ b/changelog.d/spi-synthetic-reported-benefits.fixed.md @@ -0,0 +1 @@ +SPI-synthetic rows no longer report income-related (except council tax reduction, which keeps its imputed value for now), out-of-work or Child Benefit receipt, take their industrial injuries, armed forces compensation and bereavement support from the FRS donor, and get UC, Pension Credit and `receives_benefits_in_own_right` flags from their own reports instead of the donor's. diff --git a/changelog.d/uk-data-batch-relock.changed.md b/changelog.d/uk-data-batch-relock.changed.md new file mode 100644 index 000000000..e9bbde552 --- /dev/null +++ b/changelog.d/uk-data-batch-relock.changed.md @@ -0,0 +1 @@ +Lock policyengine-uk 2.122.2 (from 2.93.0) and policyengine-core 3.32.13 (from 3.31.1); #525 needs policyengine-uk 2.122.0 or later. diff --git a/docs/imputations.md b/docs/imputations.md index 9e6964ed3..75fd1a56d 100644 --- a/docs/imputations.md +++ b/docs/imputations.md @@ -211,6 +211,19 @@ Assigns student loan plan type based on age and reported repayments. --- +## Broad Rental Market Area Assignment + +**Source:** Census private-rented households by BRMA and bedrooms (not QRF; applied when the base FRS dataset is built) + +The FRS identifies only the region, but Local Housing Allowance rates vary by Broad Rental Market Area (BRMA). `datasets/brma.py` draws each benefit unit's BRMA within its region, in proportion to the private-rented households in each BRMA: +- shared-accommodation and one-bedroom LHA categories use one-bedroom homes; +- the two-, three- and four-or-more-bedroom categories use homes with that many bedrooms; +- Northern Ireland's census has no bedrooms, so its weights are the same for every category. + +A household takes one of its benefit units' BRMAs, chosen at random. Sources, method and validation are in `storage/BRMA_DATA_SOURCES.md`. + +--- + ## Calibration Targets After imputation, household weights are calibrated to match aggregate statistics from: diff --git a/docs/oa_calibration_pipeline.md b/docs/oa_calibration_pipeline.md index 8339931f0..a8722826c 100644 --- a/docs/oa_calibration_pipeline.md +++ b/docs/oa_calibration_pipeline.md @@ -23,7 +23,7 @@ Build the OA crosswalk and population-weighted assignment function. **Deliverables:** - `policyengine_uk_data/calibration/oa_crosswalk.py` — downloads/builds the OA → LSOA → MSOA → LA → constituency → region → country crosswalk - `policyengine_uk_data/storage/oa_crosswalk.csv.gz` — compressed crosswalk file -- `policyengine_uk_data/calibration/oa_assignment.py` — assigns cloned records to OAs (population-weighted, country-constrained) +- `policyengine_uk_data/calibration/oa_assignment.py` — assigns cloned records to OAs (population-weighted within the household's FRS region) - Tests validating crosswalk completeness and assignment correctness **Data sources:** @@ -42,13 +42,14 @@ Clone each FRS household N times and assign each clone a different OA. **Deliverables:** - `policyengine_uk_data/calibration/clone_and_assign.py` — clones all three entity tables (household, person, benunit), remaps IDs, divides weights by N, attaches OA geography columns -- `datasets/create_datasets.py` — clone step inserted after imputations, before uprating/calibration (N=10 production, N=2 testing) +- `datasets/create_datasets.py` — clone step inserted after imputations, before uprating/calibration (N defaults to 10, or 2 with `TESTING=1`; `PE_UK_DATA_OA_CLONES` overrides it) - `tests/test_clone_and_assign.py` — 14 tests covering dimensions, weight preservation, ID uniqueness, FK integrity, country constraints, data preservation **Key design:** -- N=10 clones in production, N=2 in testing mode +- N defaults to 10 clones (2 with `TESTING=1`); the `PE_UK_DATA_OA_CLONES` environment variable overrides it, and the release (`push.yaml`) and pull request workflows set it to 1 - Constituency collision avoidance: each clone gets a different constituency where possible -- Country constraint preserved: English households → English OAs only +- Region constraint: each clone's OA is drawn from the household's own FRS region (Wales and Scotland are one region each), so `region_code_oa`, `la_code_oa` and `constituency_code_oa` never contradict `region`. A household with no region below the country falls back to its country. No LA or constituency straddles a region, so every OA stays reachable +- Collision avoidance draws from the same region; the smallest (North East) has 27 constituencies, more than the default 10 clones - Weights divided by N so population totals are preserved - Pure pandas/numpy operations — no simulation required, fast execution @@ -133,7 +134,7 @@ Generate per-area H5 files from sparse L0-calibrated weights. **Deliverables:** - `policyengine_uk_data/calibration/publish_local_h5s.py` — extracts per-area H5 subsets from the sparse weight vector; each H5 contains only active households (non-zero weight) with their calibrated weights, plus the linked person and benunit rows - `policyengine_uk_data/calibration/long_geography.py` — exports matrix-free local geography weights as an OA-first long table, with constituency and LA rows derived from assigned OA geography -- `datasets/create_datasets.py` — publish step wired in after calibration, before downrating +- `datasets/create_datasets.py` — exports `local_geography_weights.csv.gz` after calibration; `publish_local_h5s()` is not called by the build - `tests/test_publish_local_h5s.py` — 13 tests covering area-household mapping, H5 structure, pruned-household exclusion, weight correctness, person/benunit FK integrity, full publish cycle, summary statistics, and validation **Key design:** diff --git a/policyengine_uk_data/calibration/clone_and_assign.py b/policyengine_uk_data/calibration/clone_and_assign.py index 397567c1f..f1907b9dc 100644 --- a/policyengine_uk_data/calibration/clone_and_assign.py +++ b/policyengine_uk_data/calibration/clone_and_assign.py @@ -1,9 +1,9 @@ """Clone-and-assign: duplicate FRS households and assign OA geography. Each FRS household is cloned N times. Each clone gets a different -Output Area (population-weighted, country-constrained, with -constituency collision avoidance). Weights are divided by N so -population totals are preserved. +Output Area (population-weighted, drawn from the household's FRS +region, with constituency collision avoidance). Weights are divided +by N so population totals are preserved. This is the UK equivalent of policyengine-us-data's clone-and-assign approach (PRs #457, #531). @@ -19,37 +19,35 @@ from policyengine_uk.data import UKSingleYearDataset from policyengine_uk_data.calibration.oa_assignment import ( + FRS_COUNTRY_MAP, + _REGION_CODE_PREFIX_TO_COUNTRY, + _normalise_region, assign_random_geography, ) logger = logging.getLogger(__name__) -# FRS region values that map to each country -_REGION_TO_COUNTRY_CODE = { - "NORTH_EAST": 1, - "NORTH_WEST": 1, - "YORKSHIRE": 1, - "EAST_MIDLANDS": 1, - "WEST_MIDLANDS": 1, - "EAST_OF_ENGLAND": 1, - "LONDON": 1, - "SOUTH_EAST": 1, - "SOUTH_WEST": 1, - "WALES": 2, - "SCOTLAND": 3, - "NORTHERN_IRELAND": 4, - "UNKNOWN": 1, # Default to England -} +_COUNTRY_TO_FRS_CODE = {name: code for code, name in FRS_COUNTRY_MAP.items()} def _household_country_codes(dataset: UKSingleYearDataset) -> np.ndarray: - """Extract FRS country codes (1-4) from household region.""" - regions = dataset.household["region"].values - codes = np.array( - [_REGION_TO_COUNTRY_CODE.get(str(r), 1) for r in regions], - dtype=np.int32, - ) - return codes + """Extract FRS country codes (1-4) from household region. + + Uses the same region normalisation as the OA sampler, so both read + a region the same way. A region with nothing below the country + (missing or ``UNKNOWN``) counts as England; an unrecognised value + raises. + """ + codes = [] + for region in dataset.household["region"].values: + region_code = _normalise_region(region) + country = ( + "England" + if region_code is None + else _REGION_CODE_PREFIX_TO_COUNTRY[region_code[0]] + ) + codes.append(_COUNTRY_TO_FRS_CODE[country]) + return np.array(codes, dtype=np.int32) def _remap_ids( @@ -76,9 +74,11 @@ def clone_and_assign( ) -> UKSingleYearDataset: """Clone each FRS household N times and assign OA geography. - Each clone gets a population-weighted random Output Area, - constrained to its country, with constituency collision - avoidance across clones. + Each clone gets a population-weighted random Output Area in + its household's FRS region (Wales and Scotland are one region + each), with constituency collision avoidance across clones. + The OA's region, LA and constituency therefore never + contradict the household's ``region``. Household weights are divided by n_clones so aggregate population totals are preserved. @@ -112,7 +112,6 @@ def clone_and_assign( # NI is excluded until NISRA updates their download URLs. from policyengine_uk_data.calibration.oa_assignment import ( _load_country_distributions, - FRS_COUNTRY_MAP, ) available_distributions = _load_country_distributions( @@ -145,6 +144,7 @@ def clone_and_assign( n_clones=n_clones, seed=seed, crosswalk_path=crosswalk_path, + household_regions=hh["region"].values[has_oa], ) else: geography = None diff --git a/policyengine_uk_data/calibration/oa_assignment.py b/policyengine_uk_data/calibration/oa_assignment.py index d57c10a49..c8d23e7a4 100644 --- a/policyengine_uk_data/calibration/oa_assignment.py +++ b/policyengine_uk_data/calibration/oa_assignment.py @@ -3,8 +3,10 @@ Assigns population-weighted random Output Areas to household clones, with constituency collision avoidance (each clone of the same household gets a different constituency where -possible) and country constraints (English households get -English OAs only, etc.). +possible) and region constraints (a London household gets a +London OA, a Welsh household a Welsh OA, and so on). The FRS +region is the finest geography the survey records, so it bounds +where a household's OA can be. Analogous to policyengine-us-data's clone_and_assign.py. """ @@ -39,6 +41,32 @@ "NORTHERN_IRELAND": "Northern Ireland", } +# FRS regions (the policyengine-uk ``region`` enum) to the region codes +# the OA crosswalk carries: ONS RGN codes in England, and the +# crosswalk's country-level pseudo-codes elsewhere. +FRS_REGION_TO_CODE = { + "NORTH_EAST": "E12000001", + "NORTH_WEST": "E12000002", + "YORKSHIRE": "E12000003", + "EAST_MIDLANDS": "E12000004", + "WEST_MIDLANDS": "E12000005", + "EAST_OF_ENGLAND": "E12000006", + "LONDON": "E12000007", + "SOUTH_EAST": "E12000008", + "SOUTH_WEST": "E12000009", + "WALES": "W99999999", + "SCOTLAND": "S99999999", + "NORTHERN_IRELAND": "N99999999", +} +_REGION_CODE_PREFIX_TO_COUNTRY = { + "E": "England", + "W": "Wales", + "S": "Scotland", + "N": "Northern Ireland", +} +# Region values that carry no location below the country. +_UNKNOWN_REGIONS = {"", "UNKNOWN"} + @dataclass class GeographyAssignment: @@ -60,6 +88,54 @@ class GeographyAssignment: n_clones: int +def _distribution(subset: pd.DataFrame) -> Dict[str, np.ndarray]: + """Population-weighted sampling frame for a set of OAs.""" + pop = subset["population"].values.astype(np.float64) + total = pop.sum() + if total == 0: + # Uniform if no population data + probs = np.ones(len(subset)) / len(subset) + else: + probs = pop / total + + return { + "oa_codes": subset["oa_code"].values, + "constituencies": subset["constituency_code"].values, + "lsoa_codes": subset["lsoa_code"].values, + "msoa_codes": subset["msoa_code"].values, + "la_codes": subset["la_code"].values, + "region_codes": subset["region_code"].values, + "probs": probs, + } + + +def _load_numeric_crosswalk(crosswalk_path: Optional[str]) -> pd.DataFrame: + path = Path(crosswalk_path) if crosswalk_path else None + xw = load_oa_crosswalk(path) + # Ensure population is numeric + xw["population"] = pd.to_numeric(xw["population"], errors="coerce").fillna(0) + return xw + + +@lru_cache(maxsize=1) +def _load_region_distributions( + crosswalk_path: Optional[str] = None, +) -> Dict[str, Dict]: + """Load OA distributions grouped by crosswalk region code. + + Returns: + Dict mapping region code (e.g. ``E12000007``, ``W99999999``) + to the same sampling frame as ``_load_country_distributions``, + with probabilities summing to 1 within the region. + """ + xw = _load_numeric_crosswalk(crosswalk_path) + region_codes = xw["region_code"].fillna("").astype(str).str.strip() + return { + code: _distribution(xw[region_codes == code]) + for code in sorted(set(region_codes) - {""}) + } + + @lru_cache(maxsize=1) def _load_country_distributions( crosswalk_path: Optional[str] = None, @@ -75,11 +151,7 @@ def _load_country_distributions( - crosswalk_idx: np.ndarray of indices into the full crosswalk DataFrame """ - path = Path(crosswalk_path) if crosswalk_path else None - xw = load_oa_crosswalk(path) - - # Ensure population is numeric - xw["population"] = pd.to_numeric(xw["population"], errors="coerce").fillna(0) + xw = _load_numeric_crosswalk(crosswalk_path) distributions = {} for country_name in [ @@ -95,23 +167,7 @@ def _load_country_distributions( logger.warning(f"No OAs found for {country_name}") continue - pop = subset["population"].values.astype(np.float64) - total = pop.sum() - if total == 0: - # Uniform if no population data - probs = np.ones(len(subset)) / len(subset) - else: - probs = pop / total - - distributions[country_name] = { - "oa_codes": subset["oa_code"].values, - "constituencies": subset["constituency_code"].values, - "lsoa_codes": subset["lsoa_code"].values, - "msoa_codes": subset["msoa_code"].values, - "la_codes": subset["la_code"].values, - "region_codes": subset["region_code"].values, - "probs": probs, - } + distributions[country_name] = _distribution(subset) return distributions @@ -133,22 +189,46 @@ def _normalise_country(value) -> Optional[str]: return COUNTRY_NAME_MAP.get(text.upper().replace("-", "_").replace(" ", "_")) +def _normalise_region(value) -> Optional[str]: + """Map an FRS region name or crosswalk region code to a region code. + + Returns None when the region says nothing below the country + (missing or ``UNKNOWN``). Raises on anything unrecognised. + """ + if isinstance(value, bytes): + value = value.decode() + if value is None or (not isinstance(value, str) and pd.isna(value)): + return None + text = str(value).strip().upper() + if text in _UNKNOWN_REGIONS: + return None + if text in FRS_REGION_TO_CODE: + return FRS_REGION_TO_CODE[text] + if text in FRS_REGION_TO_CODE.values(): + return text + raise ValueError(f"Unrecognised household region value: {value!r}") + + def assign_random_geography( household_countries: np.ndarray, n_clones: int = 10, seed: int = 42, crosswalk_path: Optional[str] = None, + household_regions: Optional[np.ndarray] = None, ) -> GeographyAssignment: """Assign random OA geography to cloned FRS records. Each of n_records * n_clones total records gets a random - Output Area sampled from the country-specific - population-weighted distribution. LA, constituency, - region are derived from the OA. + Output Area sampled from the population-weighted + distribution of its household's region (or of its country, + when no region is given). LA, constituency and region are + derived from the OA, so they always agree with the + household's region. Constituency collision avoidance: each clone of the same household gets a different constituency where possible - (up to 50 retry iterations). + (up to 50 retry iterations), drawing only from the + household's own region. Args: household_countries: Array of length n_records with @@ -156,6 +236,11 @@ def assign_random_geography( n_clones: Number of clones per household. seed: Random seed for reproducibility. crosswalk_path: Override crosswalk file path. + household_regions: Optional array of length n_records + with FRS region names (e.g. ``LONDON``) or crosswalk + region codes (e.g. ``E12000007``). Households with a + region draw OAs from that region only; missing or + ``UNKNOWN`` regions fall back to the country. Returns: GeographyAssignment with arrays of length @@ -178,9 +263,84 @@ def assign_random_geography( "Unrecognised household country values: " + ", ".join(bad_values) ) - distributions = _load_country_distributions( - str(crosswalk_path) if crosswalk_path else None + if household_regions is None: + regions = np.full(n_records, None, dtype=object) + else: + if len(household_regions) != n_records: + raise ValueError( + f"household_regions has {len(household_regions)} values, " + f"expected {n_records}." + ) + regions = np.array( + [_normalise_region(value) for value in household_regions], + dtype=object, + ) + mismatched = sorted( + { + f"{region} in {country}" + for region, country in zip(regions, countries) + if region is not None + and _REGION_CODE_PREFIX_TO_COUNTRY[region[0]] != country + } + ) + if mismatched: + raise ValueError( + "Household regions disagree with household countries: " + + ", ".join(mismatched) + ) + n_unknown = sum(region is None for region in regions) + if n_unknown: + logger.warning( + "%d households have no region below the country; " + "sampling their OAs country-wide", + n_unknown, + ) + + path_key = str(crosswalk_path) if crosswalk_path else None + country_distributions = _load_country_distributions(path_key) + region_distributions = ( + _load_region_distributions(path_key) if household_regions is not None else {} + ) + # A region that holds every OA of its country (Wales and Scotland in + # the crosswalk) samples from the same pool as the country, so it + # shares the country's stratum: its households then draw exactly as a + # country-only call would draw them. + sole_region_country = {} + for country, dist in country_distributions.items(): + codes = set(pd.Series(dist["region_codes"]).fillna("").astype(str).str.strip()) + if len(codes) == 1: + (code,) = codes + region_dist = region_distributions.get(code) + if region_dist is not None and len(region_dist["oa_codes"]) == len( + dist["oa_codes"] + ): + sole_region_country[code] = country + + # Sampling stratum per household: its region code where known, + # otherwise its country name. + strata = np.array( + [ + country if region is None else sole_region_country.get(region, region) + for region, country in zip(regions, countries) + ], + dtype=object, ) + distributions = {} + missing_distributions = [] + for stratum in sorted(set(strata)): + is_country = stratum in COUNTRY_NAME_MAP.values() + source = country_distributions if is_country else region_distributions + if stratum in source: + distributions[stratum] = source[stratum] + elif is_country: + missing_distributions.append(stratum) + else: + country = _REGION_CODE_PREFIX_TO_COUNTRY[stratum[0]] + missing_distributions.append(f"{stratum} ({country})") + if missing_distributions: + raise ValueError( + "No OA distribution available for: " + ", ".join(missing_distributions) + ) rng = np.random.default_rng(seed) @@ -193,29 +353,17 @@ def assign_random_geography( region_codes = np.empty(n_total, dtype=object) country_names = np.empty(n_total, dtype=object) - # Group households by country for efficient sampling - unique_countries = np.unique(countries) - # Track assigned constituencies per record for # collision avoidance assigned_const = np.full((n_clones, n_records), "", dtype=object) - missing_distributions = sorted(set(unique_countries) - set(distributions)) - if missing_distributions: - raise ValueError( - "No OA distribution available for: " + ", ".join(missing_distributions) - ) for clone_idx in range(n_clones): start = clone_idx * n_records - for country_name in unique_countries: - dist = distributions[country_name] - hh_mask = countries == country_name + for stratum, dist in distributions.items(): + hh_mask = strata == stratum n_hh = hh_mask.sum() - if n_hh == 0: - continue - # Sample OAs indices = rng.choice( len(dist["oa_codes"]), @@ -260,7 +408,7 @@ def assign_random_geography( msoa_codes[store_idx] = dist["msoa_codes"][indices] la_codes[store_idx] = dist["la_codes"][indices] region_codes[store_idx] = dist["region_codes"][indices] - country_names[store_idx] = country_name + country_names[store_idx] = countries[positions] assigned_const[clone_idx, positions] = sampled_const diff --git a/policyengine_uk_data/datasets/brma.py b/policyengine_uk_data/datasets/brma.py new file mode 100644 index 000000000..9aec9bc62 --- /dev/null +++ b/policyengine_uk_data/datasets/brma.py @@ -0,0 +1,106 @@ +"""Broad Rental Market Area (BRMA) assignment for FRS benefit units. + +The FRS identifies only a household's region, but Local Housing Allowance +rates vary by BRMA. Each benefit unit is given a BRMA drawn within its +region, in proportion to the number of private-rented households in each +BRMA with the number of bedrooms matching its LHA category (shared +accommodation and one-bedroom categories both use one-bedroom homes). + +The counts come from the censuses (England and Wales 2021, Scotland 2022, +Northern Ireland 2021) mapped to BRMAs; ``storage/BRMA_DATA_SOURCES.md`` +gives the sources, method and validation. A BRMA that crosses a region +boundary appears once per region, holding the households on that side. + +These weights replace the row counts of the 2019-20 LHA list of rents. Its +Scottish, Welsh and Northern Ireland rows were copies of English BRMAs' +lists, so their counts said nothing about those nations' rental markets. +""" + +from pathlib import Path + +import numpy as np +import pandas as pd + +from policyengine_uk_data.storage import STORAGE_FOLDER + +BRMA_HOUSEHOLDS_PATH = STORAGE_FOLDER / "brma_private_rented_households.csv" + +# Census bedroom band whose private-rented households weight each LHA category. +LHA_CATEGORY_BEDROOMS = {"A": "1", "B": "1", "C": "2", "D": "3", "E": "4+"} + + +def load_brma_weights(path: Path = BRMA_HOUSEHOLDS_PATH) -> pd.DataFrame: + """Return BRMA sampling weights by region and LHA category. + + Columns: ``region``, ``lha_category``, ``brma``, ``weight`` (private-rented + households). Every row has a positive weight. Northern Ireland's census + has no bedrooms question, so its rows (bedrooms ``all``) weight every + category. + """ + households = pd.read_csv(path, dtype={"bedrooms": str}) + bands = pd.DataFrame( + LHA_CATEGORY_BEDROOMS.items(), columns=["lha_category", "bedrooms"] + ) + by_band = bands.merge(households, on="bedrooms") + any_band = households[households.bedrooms == "all"].merge( + bands[["lha_category"]], how="cross" + ) + weights = pd.concat([by_band, any_band], ignore_index=True) + weights = weights[weights.households > 0] + return weights.rename(columns={"households": "weight"})[ + ["region", "lha_category", "brma", "weight"] + ].reset_index(drop=True) + + +def assign_brmas( + region: np.ndarray, + lha_category: np.ndarray, + rng: np.random.Generator, + weights: pd.DataFrame | None = None, +) -> np.ndarray: + """Draw a BRMA for each benefit unit from its region × LHA category cell. + + Args: + region: Region name of each benefit unit's household. + lha_category: LHA category (A-E) of each benefit unit. + rng: Generator for the draws. + weights: Output of ``load_brma_weights``; loaded from storage if omitted. + + Raises: + ValueError: If a benefit unit's cell has no BRMA with a positive weight. + """ + if weights is None: + weights = load_brma_weights() + if hasattr(region, "decode_to_str"): # EnumArray of region codes + region = region.decode_to_str() + region = np.asarray(region).astype(str) + lha_category = np.asarray(lha_category).astype(str) + brma = np.empty(len(region), dtype=object) + for (cell_region, category), cell in weights.groupby( + ["region", "lha_category"], sort=True + ): + mask = (region == cell_region) & (lha_category == category) + if mask.any(): + p = cell.weight.to_numpy(dtype=float) + brma[mask] = rng.choice( + cell.brma.to_numpy(), size=mask.sum(), p=p / p.sum() + ) + missing = pd.isna(brma) + if missing.any(): + cells = sorted(set(zip(region[missing], lha_category[missing]))) + raise ValueError(f"No BRMA weights for region × LHA category cells {cells}.") + return brma + + +def pick_household_brmas( + brma: np.ndarray, household_id: np.ndarray, rng: np.random.Generator +) -> pd.Series: + """Give each household the BRMA of one of its benefit units, chosen at random. + + ``brma`` and ``household_id`` are per benefit unit; returns a Series + indexed by household ID. + """ + units = pd.DataFrame({"brma": brma, "household_id": household_id}) + return units.groupby("household_id").brma.aggregate( + lambda x: x.sample(n=1, random_state=rng).iloc[0] + ) diff --git a/policyengine_uk_data/datasets/create_datasets.py b/policyengine_uk_data/datasets/create_datasets.py index 29a73028b..310081839 100644 --- a/policyengine_uk_data/datasets/create_datasets.py +++ b/policyengine_uk_data/datasets/create_datasets.py @@ -7,6 +7,15 @@ logging.basicConfig(level=logging.INFO) +# Local targets the calibration logs but does not train on. HMRC's counts of +# income-tax payers with employment income by area (SPI table 3.15) are annual: +# they include people with pay for part of the year whose FRS status at +# interview is out of work, and who therefore have no pay in the FRS. Training +# on them moves weight from people out of work to employees, away from the LFS +# employee count (targets/sources/ons_labour_market.py). The area amounts of +# employment income, and the national counts by income band, still train. +VALIDATION_ONLY_LOCAL_TARGETS = ["hmrc/employment_income/count"] + def _get_positive_int_env(name: str, default: int) -> int: raw_value = os.environ.get(name) @@ -100,6 +109,7 @@ def main(): "Impute capital gains", "Impute salary sacrifice", "Impute student loan plan", + "Assign Pension Credit take-up", "Clone and assign OA geography", "Calibrate constituency weights", "Calibrate local authority weights", @@ -202,6 +212,18 @@ def main(): ) update_dataset("Impute student loan plan", "completed") + # Pension Credit entitlement needs the imputed capital, so its + # take-up is solved here rather than in the FRS build. + update_dataset("Assign Pension Credit take-up", "processing") + from policyengine_uk_data.datasets.pension_credit_takeup import ( + assign_pension_credit_takeup, + ) + + frs, pension_credit_takeup = assign_pension_credit_takeup( + frs, year=frs_release.calibration_year + ) + update_dataset("Assign Pension Credit take-up", "completed") + # Clone households and assign OA geography update_dataset("Clone and assign OA geography", "processing") from policyengine_uk_data.calibration.clone_and_assign import ( @@ -252,7 +274,7 @@ def main(): area_count=650, weight_file="parliamentary_constituency_weights.h5", dataset_key=str(frs_release.calibration_year), - excluded_training_targets=[], + excluded_training_targets=VALIDATION_ONLY_LOCAL_TARGETS, log_csv="constituency_calibration_log.csv", verbose=True, # Enable nested progress display area_name="Constituency", @@ -279,7 +301,7 @@ def main(): area_count=360, weight_file="local_authority_weights.h5", dataset_key=str(frs_release.calibration_year), - excluded_training_targets=[], + excluded_training_targets=VALIDATION_ONLY_LOCAL_TARGETS, log_csv="la_calibration_log.csv", verbose=True, # Enable nested progress display area_name="Local Authority", @@ -397,6 +419,7 @@ def main(): "long_geography_weights": "local_geography_weights.csv.gz", "imputations_applied": "consumption, wealth, VAT, services, income, capital_gains, cgt_band_donors, salary_sacrifice, student_loan_plan", "calibration": "national, LA and constituency targets", + "pension_credit_take_up": pension_credit_takeup, }, ) diff --git a/policyengine_uk_data/datasets/enhanced_cps.py b/policyengine_uk_data/datasets/enhanced_cps.py index 490f89cae..b4e038e3c 100644 --- a/policyengine_uk_data/datasets/enhanced_cps.py +++ b/policyengine_uk_data/datasets/enhanced_cps.py @@ -559,6 +559,15 @@ def _build_base_dataset( adults = scenario["adults"] children = scenario["children"] people = adults + children + # Each source household is one tax unit with its head listed first and, + # for joint filers, the spouse second: the benefit unit's claimant and + # partner. + claimants_or_partners = 2 if row["filing_status"] == "joint" else 1 + if len(adults) < claimants_or_partners: + raise ValueError( + f"Source household {source_household_id} files " + f"{row['filing_status']} but lists {len(adults)} adult(s)." + ) has_mortgage = any( "deductible_mortgage_interest" in person.get("inputs", {}) @@ -663,6 +672,7 @@ def _build_base_dataset( "person_id": person_id, "person_household_id": household_id, "person_benunit_id": benunit_id, + "is_claimant_or_partner": person_index <= claimants_or_partners, "age": int(person["age"]), "gender": "MALE" if (household_id + person_index) % 2 else "FEMALE", "employment_income_before_lsr": _gbp( diff --git a/policyengine_uk_data/datasets/frs.py b/policyengine_uk_data/datasets/frs.py index e440d4869..711127b51 100644 --- a/policyengine_uk_data/datasets/frs.py +++ b/policyengine_uk_data/datasets/frs.py @@ -19,11 +19,13 @@ from policyengine_uk.variables.household.income.employment_status import ( EmploymentStatus, ) +from policyengine_uk_data.datasets.brma import assign_brmas, pick_household_brmas from policyengine_uk_data.datasets.disability_benefits import ( add_disability_benefit_categories_from_reported_amounts, add_disability_benefit_flags_from_reported_amounts, drop_internal_disability_reported_amounts, ) +from policyengine_uk_data.utils.benefit_units import claimant_or_partner_variable from policyengine_uk_data.utils.datasets import ( sum_to_entity, categorical, @@ -33,6 +35,7 @@ STORAGE_FOLDER, ) from policyengine_uk_data.parameters import load_take_up_rate, load_parameter +from policyengine_uk_data.utils.takeup import assign_takeup_with_reported_anchors from policyengine_uk_data.datasets.childcare.assumptions import ( EXTENDED_HOURS_MEAN, EXTENDED_HOURS_SD, @@ -49,10 +52,30 @@ LEGACY_JOBSEEKER_MIN_AGE = 18 HOURS_WORKED_WEEKS_PER_YEAR = 52 ESA_MIN_AGE = 16 +# Adult-table EMPSTATI ("Adult - Employment Status - ILO definition") value +# labels from the UKDS FRS 2024-25 data dictionary, mapped to PolicyEngine +# statuses. Children have no EMPSTATI and are CHILD. +FRS_EMPSTATI_EMPLOYMENT_STATUS = { + 1: EmploymentStatus.FT_EMPLOYED.name, # Full-time employee + 2: EmploymentStatus.PT_EMPLOYED.name, # Part-time employee + 3: EmploymentStatus.FT_SELF_EMPLOYED.name, # Full-time self-employed + 4: EmploymentStatus.PT_SELF_EMPLOYED.name, # Part-time self-employed + 5: EmploymentStatus.UNEMPLOYED.name, # Unemployed + 6: EmploymentStatus.RETIRED.name, # Retired + 7: EmploymentStatus.STUDENT.name, # Student + 8: EmploymentStatus.CARER.name, # Looking after family/home + 9: EmploymentStatus.LONG_TERM_DISABLED.name, # Permanently sick/disabled + 10: EmploymentStatus.SHORT_TERM_DISABLED.name, # Temporarily sick/injured + 11: EmploymentStatus.OTHER_INACTIVE.name, # Other inactive +} ESA_HEALTH_EMPLOYMENT_STATUSES = ( EmploymentStatus.LONG_TERM_DISABLED.name, EmploymentStatus.SHORT_TERM_DISABLED.name, ) +SELF_EMPLOYED_STATUSES = ( + EmploymentStatus.FT_SELF_EMPLOYED.name, + EmploymentStatus.PT_SELF_EMPLOYED.name, +) FORMULA_MODELED_EDUCATION_GRANT_VARIABLES = ( "childcare_grant", "parents_learning_allowance", @@ -74,6 +97,14 @@ "esa_contrib_reported", "esa_income_reported", ) +# Take-up flags anchored on reported receipt: flag -> (take-up rate +# parameter, person-level report column). A benefit unit with any member +# reporting receipt claims with certainty; the rest are filled at random. +REPORTED_TAKEUP_ANCHORS = { + "would_claim_child_benefit": ("child_benefit", "child_benefit_reported"), + "would_claim_pc": ("pension_credit", "pension_credit_reported"), + "would_claim_uc": ("universal_credit", "universal_credit_reported"), +} NON_ADVANCED_EDUCATION_LEVELS = ( "PRE_PRIMARY", "PRIMARY", @@ -84,6 +115,8 @@ # FRS government-training question variants use 10 or 13 for "None of these". FRS_APPROVED_TRAINING_CODES = tuple(range(1, 10)) UNKNOWN_QUALIFYING_EDUCATION_OR_TRAINING_ENTRY_AGE = 1000 +# FRS RENTPROF: whether ROYYR1 is a profit (1) or a loss (2). +FRS_RENTPROF_LOSS = 2 @lru_cache(maxsize=None) @@ -112,6 +145,33 @@ def require_variable(name: str, description: str) -> None: ) +def derive_employment_status_from_frs(empstati, is_adult_record) -> np.ndarray: + """Map FRS EMPSTATI codes to ``employment_status``. + + Rows from the child table are CHILD. Every adult must carry a code in + ``FRS_EMPSTATI_EMPLOYMENT_STATUS``: a missing or unknown adult code fails + the build rather than falling back to a guessed status, as the old + fallback silently made code 11 (Other inactive) LONG_TERM_DISABLED. + """ + + codes = pd.Series(np.asarray(empstati, dtype=float)) + is_adult_record = np.asarray(is_adult_record, dtype=bool) + adult_status = codes.map(FRS_EMPSTATI_EMPLOYMENT_STATUS).to_numpy() + unknown = is_adult_record & pd.isna(adult_status) + if unknown.any(): + # Build logs are public, and adults are not survey households, so + # name the codes and never a count. + bad = codes[unknown] + listed = [f"{code:g}" for code in sorted(bad.dropna().unique())] + listed += ["blank"] if bad.isna().any() else [] + raise ValueError( + "FRS adults have EMPSTATI codes missing from " + f"FRS_EMPSTATI_EMPLOYMENT_STATUS: {', '.join(listed)}. Map them " + "from the release's data dictionary." + ) + return np.where(is_adult_record, adult_status, EmploymentStatus.CHILD.name) + + def derive_legacy_jobseeker_proxy( age, employment_status, @@ -211,6 +271,34 @@ def derive_esa_support_group_proxy( ) +def reported_benunit_mask( + person: pd.DataFrame, benunit: pd.DataFrame, person_column: str +) -> np.ndarray: + """Benefit units with any member reporting a positive ``person_column``.""" + reporter_benunits = set( + person.loc[person[person_column] > 0, "person_benunit_id"].values + ) + return benunit["benunit_id"].isin(reporter_benunits).values + + +def assign_reported_takeup( + person: pd.DataFrame, + benunit: pd.DataFrame, + flag: str, + year: int, + draws: np.ndarray, +) -> np.ndarray: + """Take-up for one ``REPORTED_TAKEUP_ANCHORS`` flag: benefit units + reporting receipt claim, and the rest are filled at random so the share + claiming matches the take-up rate (see ``utils/takeup.py``).""" + rate_name, report_column = REPORTED_TAKEUP_ANCHORS[flag] + return assign_takeup_with_reported_anchors( + draws, + load_take_up_rate(rate_name, year), + reported_mask=reported_benunit_mask(person, benunit, report_column), + ) + + def derive_receives_benefits_in_own_right(pe_person: pd.DataFrame) -> pd.Series: """Identify people reporting adult benefits that end QYP status.""" @@ -220,6 +308,47 @@ def derive_receives_benefits_in_own_right(pe_person: pd.DataFrame) -> pd.Series: ) +def frs_property_income(person: pd.DataFrame, household: pd.DataFrame) -> np.ndarray: + """Annual property income each person reports in the FRS. + + Two FRS amounts, both weekly in the released data: + + - SUBRENT, rent the household received for letting part of its home to + someone outside the household. The FRS asks every household (SubLet), + whatever its tenure, so renting and rent-free households count too. It + goes to the household reference person. ``household`` must be indexed + by ``household_id``. + - ROYYR1, the person's rent from other property, before tax and after + allowable expenses. The questionnaire cannot take a negative amount, + so a loss is entered as a positive amount with RENTPROF = 2 (question + RentProf, "Is that a profit or a loss from the property?"). A loss + counts as zero: it is not income, policyengine-uk has no property loss + input, and it is not set against the household's SUBRENT. + + SUBRENT is used as reported. SUBALLOW records whether it is before (1) + or after (2) allowable expenses, but the FRS collects no expense amount + to take off the before-expenses answers. + + Negative values are FRS missing-value codes (-1 to -9), not amounts, so + each amount is floored at zero before the two are added. + + CVPAY is not included. It is the rent that a boarder or lodger pays the + householder, and it sits on the boarder's or lodger's own adult record. + The FRS question (CvPay) asks how much rent [name] paid for board and + lodging, after deducting any state benefits to help with rent. + """ + is_head = person.hrpid == 1 + persons_household_subrent = ( + household.subrent.clip(lower=0).reindex(person.household_id).fillna(0).values + ) + rent_from_other_property = person.royyr1.clip(lower=0).where( + person.rentprof != FRS_RENTPROF_LOSS, 0 + ) + return ( + (is_head * persons_household_subrent + rent_from_other_property) * WEEKS_IN_YEAR + ).values + + def derive_is_in_non_advanced_education( current_education, is_apprentice=None, @@ -392,6 +521,97 @@ def derive_is_parent_from_frs_microdata( return is_adult_record & has_dependent_children +def derive_all_claimants_over_state_pension_age( + person_benunit_ids, + is_claimant_or_partner, + is_over_state_pension_age, + benunit_ids, +) -> np.ndarray: + """Identify benefit units whose claimant and any partner have all reached + State Pension age. + + Such a unit cannot claim Universal Credit (Welfare Reform Act 2012 + s.4(1)(b)). ``is_claimant_or_partner`` should be the variable that + ``claimant_or_partner_variable`` names, so the rule matches the + pension-age route of policyengine-uk's ``housing_benefit_eligible``. + """ + + claimant = np.asarray(is_claimant_or_partner, dtype=bool) + over = claimant & np.asarray(is_over_state_pension_age, dtype=bool) + counts = ( + pd.DataFrame( + { + "benunit": np.asarray(person_benunit_ids), + "claimants": claimant.astype(int), + "over": over.astype(int), + } + ) + .groupby("benunit")[["claimants", "over"]] + .sum() + .reindex(np.asarray(benunit_ids), fill_value=0) + ) + return ((counts.claimants > 0) & (counts.over == counts.claimants)).to_numpy() + + +def derive_is_claimant_or_partner_from_frs_microdata( + person_ids, + person_benunit_ids, + adult_person_ids, +) -> np.ndarray: + """Identify each FRS benefit unit's single adult or couple. + + An FRS benefit unit is one adult or a couple plus any dependent children. + The adult table holds the head (UPERSON 1) and any partner (UPERSON 2); + the child table holds the dependent children. Any other adult in the + household, such as a grown-up son or daughter, forms and heads their own + benefit unit. So adult-table membership is policyengine-uk's + `is_claimant_or_partner`, which the model otherwise infers from ages. + """ + + is_adult_record = np.isin(np.asarray(person_ids), np.asarray(adult_person_ids)) + per_benunit = pd.Series(is_adult_record).groupby(np.asarray(person_benunit_ids)) + counts = per_benunit.sum() + if not counts.between(1, 2).all(): + raise ValueError( + "Every FRS benefit unit needs one or two adult-table records; " + f"{int((~counts.between(1, 2)).sum())} benefit units do not." + ) + return is_adult_record + + +def derive_uc_is_in_gainful_self_employment( + employment_status, self_employment_income, employment_income +) -> np.ndarray: + """Whether each person is in gainful self-employment for Universal Credit. + + UC Regs 2013 reg 64(a) asks whether the person carries on a trade as their + main employment. DWP's Advice for Decision Making starts from hours + (H4031) but lets earnings outweigh them: someone who works more hours as + an employee but earns more from self-employment is likely to be gainfully + self-employed (H4034). So the flag is true for: + + - a self-employed main job (FRS EMPSTATI, the job the respondent names as + their dominant activity, else the one with more hours), whatever its + profit: a trade can make a loss or break even and still be carried on + in expectation of profit (ADM H4013, H4054, H4503); + - a side trade whose profit is above the person's employment income. + + policyengine-uk's default reads any self-employment income other than + zero as gainful self-employment, and none as none. + + The flag is fixed from survey-year (or SPI-imputed) incomes. Uprating + reprices the two incomes by different indices, so in a projected year a + flagged side trade can earn less than the job, and the reverse; the flag + does not follow. + """ + self_employed_main_job = np.isin( + np.asarray(employment_status, dtype=object), SELF_EMPLOYED_STATUSES + ) + profit = np.asarray(self_employment_income, dtype=float) + pay = np.asarray(employment_income, dtype=float) + return self_employed_main_job | ((profit > 0) & (profit > pay)) + + def _as_non_negative_array(values) -> np.ndarray: values = np.asarray(values, dtype=float) return np.maximum(np.nan_to_num(values, nan=0.0), 0.0) @@ -570,6 +790,41 @@ def validate_frs_survey_year(raw_frs_folder, year: int) -> None: ) +def derive_pension_credit_reported_capital(benunit: pd.DataFrame) -> np.ndarray: + """Each benefit unit's capital as the FRS records it, for Pension Credit. + + Uses ``TOTCAPB4``, DWP's derived benefit-unit total of the adults' savings + and investments, which its below-average-resources statistics use in place + of ``TOTCAPB3`` since it became available in 2019/20; ``TOTCAPB3`` is the + fallback for earlier survey years. Pension Credit counts the claimant's + capital and, under the State Pension Credit Act 2002 s. 5, the partner's, + and this is a benefit-unit measure. It is an approximation of Pension + Credit capital, not the assessed figure: it covers financial assets only + (second homes and land, which Pension Credit also counts, are not in it), + and no Schedule V disregard or reg. 19 valuation is applied to it. The + household wealth imputation instead draws a household's wealth from Wealth + and Assets Survey households with similar income, composition, tenure and + region, with no information on means-tested receipt, and policyengine-uk + spreads it over the household's pension-age adults. + + A missing or negative value gives -1, so policyengine-uk falls back to the + household proxy. + """ + capital = np.full(len(benunit), np.nan) + for column in ("totcapb3", "totcapb4"): # later columns take precedence + if column in benunit.columns: + # Plain float64, so nullable (pd.NA) inputs become NaN and the + # validity mask is a plain bool array with no missing entries. + values = ( + pd.to_numeric(benunit[column], errors="coerce") + .astype("float64") + .to_numpy(dtype=float, na_value=np.nan) + ) + valid = np.isfinite(values) & (values >= 0) + capital = np.where(valid, values, capital) + return np.where(np.isfinite(capital) & (capital >= 0), capital, -1.0) + + def create_frs( raw_frs_folder: str, year: int, @@ -728,6 +983,13 @@ def create_frs( benunit_ids=pe_benunit.benunit_id, dependent_children=dependent_children, ) + pe_person["is_claimant_or_partner"] = ( + derive_is_claimant_or_partner_from_frs_microdata( + person_ids=pe_person.person_id, + person_benunit_ids=pe_person.person_benunit_id, + adult_person_ids=frs["adult"].person_id, + ) + ) MARITAL = [ "MARRIED", "SINGLE", @@ -854,23 +1116,9 @@ def determine_education_level(fted_val, typeed2_val, age_val): "UPPER_SECONDARY" ) - # Add employment status - EMPLOYMENTS = [ - "CHILD", - "FT_EMPLOYED", - "PT_EMPLOYED", - "FT_SELF_EMPLOYED", - "PT_SELF_EMPLOYED", - "UNEMPLOYED", - "RETIRED", - "STUDENT", - "CARER", - "LONG_TERM_DISABLED", - "SHORT_TERM_DISABLED", - ] - pe_person["employment_status"] = categorical( - person.empstati, 1, range(12), EMPLOYMENTS - ).fillna("LONG_TERM_DISABLED") + pe_person["employment_status"] = derive_employment_status_from_frs( + person.empstati, person.person_id.isin(frs["adult"].person_id) + ) # Add employer sector of the main job from FRS `mjobsect` # (1 = private, 2 = public; missing/blank = not in paid work). @@ -1045,6 +1293,15 @@ def determine_education_level(fted_val, typeed2_val, age_val): ) * WEEKS_IN_YEAR pe_person["self_employment_income"] = np.maximum(0, person.seincam2) * WEEKS_IN_YEAR + # policyengine-uk releases without this input skip the column and keep + # their formula. + pe_person["uc_is_in_gainful_self_employment"] = ( + derive_uc_is_in_gainful_self_employment( + pe_person.employment_status, + pe_person.self_employment_income, + pe_person.employment_income, + ) + ) INVERTED_BASIC_RATE = 1.25 @@ -1083,25 +1340,7 @@ def determine_education_level(fted_val, typeed2_val, age_val): ) * 52, ) - is_head = person.hrpid == 1 - household_property_income = ( - household.tentyp2.isin((5, 6)) * household.subrent - ) # Owned and subletting - persons_household_property_income = ( - pd.Series( - household_property_income[person.household_id].values, - index=person.person_id, - ) - .fillna(0) - .values - ) - pe_person["property_income"] = ( - np.maximum( - 0, - is_head * persons_household_property_income + person.cvpay + person.royyr1, - ) - * WEEKS_IN_YEAR - ) + pe_person["property_income"] = frs_property_income(person, household) maintenance_to_self = np.maximum( pd.Series(np.where(person.mntus1 == 2, person.mntusam1, person.mntamt1)).fillna( 0 @@ -1409,43 +1648,20 @@ def determine_education_level(fted_val, typeed2_val, age_val): sim = Microsimulation(dataset=dataset) region = sim.populations["benunit"].household("region", dataset.time_period) - lha_category = sim.calculate("LHA_category", year) - brma = np.empty(len(region), dtype=object) - - # Sample from a random BRMA in the region, weighted by the number of observations in each BRMA. - # Use a seeded generator so the assignment is reproducible across builds; - # pandas .sample() otherwise draws from the unseeded global numpy RNG. - lha_list_of_rents = pd.read_csv(STORAGE_FOLDER / "lha_list_of_rents.csv.gz") - lha_list_of_rents = lha_list_of_rents.copy() - brma_rng = np.random.default_rng(0) + lha_category = np.asarray(sim.calculate("LHA_category", year)) - for possible_region in lha_list_of_rents.region.unique(): - for possible_lha_category in lha_list_of_rents.lha_category.unique(): - lor_mask = (lha_list_of_rents.region == possible_region) & ( - lha_list_of_rents.lha_category == possible_lha_category - ) - mask = (region == possible_region) & (lha_category == possible_lha_category) - brma[mask] = lha_list_of_rents[lor_mask].brma.sample( - n=len(region[mask]), replace=True, random_state=brma_rng - ) - - # Convert benunit-level BRMAs to household-level BRMAs (pick a random one) - - df = pd.DataFrame( - { - "brma": brma, - "household_id": sim.populations["benunit"].household( - "household_id", sim.dataset.time_period - ), - } - ) + # Draw each benefit unit's BRMA in proportion to the private-rented + # households in each of its region's BRMAs with the matching number of + # bedrooms. Use a seeded generator so the assignment is reproducible. + brma_rng = np.random.default_rng(0) + brma = assign_brmas(region, lha_category, brma_rng) - df = df.groupby("household_id").brma.aggregate( - lambda x: x.sample(n=1, random_state=brma_rng).iloc[0] + household_brma = pick_household_brmas( + brma, + sim.populations["benunit"].household("household_id", dataset.time_period), + brma_rng, ) - brmas = df[sim.calculate("household_id")].values - - pe_household["brma"] = brmas + pe_household["brma"] = household_brma[sim.calculate("household_id")].values pe_person = add_disability_benefit_flags_from_reported_amounts( pe_person, @@ -1487,9 +1703,6 @@ def determine_education_level(fted_val, typeed2_val, age_val): generator = np.random.default_rng(seed=100) # Load take-up rates from parameter files - child_benefit_rate = load_take_up_rate("child_benefit", year) - pension_credit_rate = load_take_up_rate("pension_credit", year) - universal_credit_rate = load_take_up_rate("universal_credit", year) marriage_allowance_rate = load_take_up_rate("marriage_allowance", year) child_benefit_opts_out_rate = load_take_up_rate("child_benefit_opts_out_rate", year) tfc_rate = load_take_up_rate("tax_free_childcare", year) @@ -1507,16 +1720,6 @@ def determine_education_level(fted_val, typeed2_val, age_val): # who report positive receipt of a benefit are assigned takeup=True with # certainty; the remaining non-reporters are filled probabilistically to # hit the aggregate target rate. See policyengine_uk_data/utils/takeup.py. - from policyengine_uk_data.utils.takeup import ( - assign_takeup_with_reported_anchors, - ) - - def _reported_benunit_mask(person_column: str) -> np.ndarray: - reporter_benunits = set( - pe_person.loc[pe_person[person_column] > 0, "person_benunit_id"].values - ) - return pe_benunit["benunit_id"].isin(reporter_benunits).values - # Person-level pe_person["would_claim_marriage_allowance"] = ( generator.random(len(pe_person)) < marriage_allowance_rate @@ -1524,23 +1727,36 @@ def _reported_benunit_mask(person_column: str) -> np.ndarray: # Benefit unit-level — anchor on any adult in the benefit unit having # reported positive receipt in the FRS benefits table. - pe_benunit["would_claim_child_benefit"] = assign_takeup_with_reported_anchors( + pe_benunit["would_claim_child_benefit"] = assign_reported_takeup( + pe_person, + pe_benunit, + "would_claim_child_benefit", + year, generator.random(len(pe_benunit)), - child_benefit_rate, - reported_mask=_reported_benunit_mask("child_benefit_reported"), ) pe_benunit["child_benefit_opts_out"] = ( generator.random(len(pe_benunit)) < child_benefit_opts_out_rate ) - pe_benunit["would_claim_pc"] = assign_takeup_with_reported_anchors( - generator.random(len(pe_benunit)), - pension_credit_rate, - reported_mask=_reported_benunit_mask("pension_credit_reported"), - ) - pe_benunit["would_claim_uc"] = assign_takeup_with_reported_anchors( - generator.random(len(pe_benunit)), - universal_credit_rate, - reported_mask=_reported_benunit_mask("universal_credit_reported"), + # The enhanced dataset redraws this once entitlement can be computed + # (datasets/pension_credit_takeup.py). + pe_benunit["would_claim_pc"] = assign_reported_takeup( + pe_person, pe_benunit, "would_claim_pc", year, generator.random(len(pe_benunit)) + ) + # A benefit unit whose claimant and any partner have all reached State + # Pension age cannot claim Universal Credit, so it never gets + # would_claim_uc, even if it reports UC. The draw still covers every unit, + # so the random stream and every other unit's value are unchanged. Ages + # are not rolled forward, so if State Pension age rises above a claimant's + # survey age in a later year, that unit stays without would_claim_uc there. + pe_benunit["would_claim_uc"] = assign_reported_takeup( + pe_person, pe_benunit, "would_claim_uc", year, generator.random(len(pe_benunit)) + ) & ~derive_all_claimants_over_state_pension_age( + person_benunit_ids=sim.calculate("person_benunit_id", year).values, + is_claimant_or_partner=sim.calculate( + claimant_or_partner_variable(sim.tax_benefit_system.variables), year + ).values, + is_over_state_pension_age=sim.calculate("is_SP_age", year).values, + benunit_ids=pe_benunit.benunit_id, ) pe_benunit["would_claim_tfc"] = generator.random(len(pe_benunit)) < tfc_rate @@ -1627,6 +1843,13 @@ def _reported_benunit_mask(person_column: str) -> np.ndarray: pe_benunit["is_married"] = benunit.famtypb2.isin([5, 7]) + # Pension Credit capital as the FRS records it for the benefit unit, in + # place of the household wealth proxy (policyengine-uk + # `pension_credit_reported_capital`). + pe_benunit["pension_credit_reported_capital"] = ( + derive_pension_credit_reported_capital(benunit) + ) + # Assign property_purchased to a share of households matching the UK # housing transaction rate, so only genuine purchasers are charged SDLT. # diff --git a/policyengine_uk_data/datasets/imputations/frs_only.py b/policyengine_uk_data/datasets/imputations/frs_only.py index 242fbf247..d173612b0 100644 --- a/policyengine_uk_data/datasets/imputations/frs_only.py +++ b/policyengine_uk_data/datasets/imputations/frs_only.py @@ -35,11 +35,19 @@ import numpy as np import pandas as pd -from policyengine_uk.data import UKSingleYearDataset +from policyengine_uk.data import UKMultiYearDataset, UKSingleYearDataset from policyengine_uk_data.datasets.disability_benefits import ( add_disability_benefit_categories_from_reported_amounts, add_disability_benefit_flags_from_reported_amounts, ) +from policyengine_uk_data.datasets.frs import ( + BENEFITS_IN_OWN_RIGHT_REPORTED_COLUMNS, + REPORTED_TAKEUP_ANCHORS, + assign_reported_takeup, + derive_all_claimants_over_state_pension_age, + derive_receives_benefits_in_own_right, +) +from policyengine_uk_data.utils.benefit_units import claimant_or_partner_variable logger = logging.getLogger(__name__) @@ -102,6 +110,144 @@ "esa_income_reported", ] +# The QRF draws each person's benefit reports from their age, gender, region +# and incomes. It sees nothing of their benefit unit (partner, children, +# rent, capital), their health or their history, and policyengine-uk reads a +# positive report as an existing claim. On SPI-donor rows, after the draw: +# +# Zeroed (SPI_DONOR_ZEROED_PERSON_VARIABLES): +# - Income-related awards. Entitlement turns on the unit's joint means and +# make-up, which here come from the imputed incomes. UC and Pension Credit +# keep a route: their take-up flags are redrawn below. In +# policyengine-uk 2.93.0 the others (housing benefit, income support, tax +# credits, income-related ESA and JSA) can only be claimed with a report, +# so these rows no longer receive them. Sure Start Maternity Grant needs +# one of these awards. Council tax reduction is kept (below). +# - Benefits paid only to people out of work or incapable of it: ESA and JSA +# (contributory), incapacity benefit and severe disablement allowance. On +# the 2024-25 build, 41% of SPI-row ESA (contributory) reporters by weight +# earned more than ESA's permitted-work limit. +# - Child Benefit, which the model reads only through the take-up flag. The +# draw ignores the children: 34% of SPI-row reports by weight were in +# benefit units with no child or qualifying young person (FRS rows: 0%). +# +# Restored to the donor's own value (SPI_DONOR_RESTORED_PERSON_VARIABLES): +# industrial injuries, armed forces compensation and bereavement support. +# These follow from an injury, service or a death, not income, and the QRF +# drew them at 6.2, 2.4 and 4.6 times the FRS rate by weight. +# +# Kept as drawn: state pension (paid as reported once over pension age), +# winter fuel payment (not read by the model), the disability benefits and +# carer's allowance, whose drawn rates sit below the FRS rates as the income +# gradient implies. Council tax reduction is also kept as drawn for now. In +# 2.93.0 it too can only be claimed with a report, and zeroing it cut 2025 +# CTR by 21% on the 2024-25 build. It will be zeroed together with a +# household-level CTR imputation (#499). +# +# Every column stays in the QRF chain above, so the values kept do not +# change. They were drawn alongside the values later zeroed or restored. +SPI_DONOR_ZEROED_PERSON_VARIABLES = [ + "universal_credit_reported", + "pension_credit_reported", + "housing_benefit_reported", + "income_support_reported", + "working_tax_credit_reported", + "child_tax_credit_reported", + "jsa_income_reported", + "esa_income_reported", + "ssmg_reported", + "jsa_contrib_reported", + "esa_contrib_reported", + "incapacity_benefit_reported", + "sda_reported", + "child_benefit_reported", +] +SPI_DONOR_RESTORED_PERSON_VARIABLES = [ + "iidb_reported", + "afcs_reported", + "bsp_reported", +] + +# Take-up flags redrawn on SPI-donor rows. Whether a synthetic family claims +# a means-tested benefit at its imputed income is unobserved, so these units +# draw at the take-up rate. As in create_frs, a unit whose claimant and any +# partner have all reached State Pension age never gets would_claim_uc. The +# Child Benefit flag keeps the donor's value: the award doesn't depend on the +# replaced incomes, and the donor's claim is for the same children. +SPI_DONOR_REDRAWN_TAKEUP_FLAGS = ("would_claim_uc", "would_claim_pc") +# Seed for those draws; create_frs uses 100. +SPI_DONOR_TAKEUP_SEED = 101 + + +def _all_claimants_over_state_pension_age( + dataset: UKSingleYearDataset, year: int +) -> np.ndarray: + """``create_frs``'s pension-age Universal Credit exclusion for each of + ``dataset``'s benefit units, read through policyengine-uk the same way.""" + from policyengine_uk import Microsimulation + + # Only this dataset's year is needed. A multi-year container holding that + # one year avoids extending and uprating unrelated household inputs. + sim = Microsimulation(dataset=UKMultiYearDataset(datasets=[dataset.copy()])) + return derive_all_claimants_over_state_pension_age( + person_benunit_ids=sim.calculate("person_benunit_id", year).values, + is_claimant_or_partner=sim.calculate( + claimant_or_partner_variable(sim.tax_benefit_system.variables), year + ).values, + is_over_state_pension_age=sim.calculate("is_SP_age", year).values, + benunit_ids=dataset.benunit.benunit_id, + ) + + +def apply_spi_donor_benefit_rules( + dataset: UKSingleYearDataset, + donor_person: pd.DataFrame | None = None, + uc_pension_age_excluded: np.ndarray | None = None, +) -> UKSingleYearDataset: + """Apply the SPI-donor benefit rules above to ``dataset``. + + ``donor_person`` is the person table before the QRF draw, holding the + donor's own reports; without it the restored columns are left alone. + ``receives_benefits_in_own_right`` and the redrawn take-up flags, which + ``create_frs`` built from the donor's reports, are rebuilt from the rows' + own reports. + + ``uc_pension_age_excluded`` is an optional boolean array with one entry + per benefit unit, in the dataset's benefit-unit order. When given, + ``would_claim_uc &= ~uc_pension_age_excluded`` is applied after the redraw; + when ``None``, no units are excluded. The pipeline caller always computes + and passes this mask with ``_all_claimants_over_state_pension_age`` on the + real target dataset, using the same computation as ``create_frs``. + """ + dataset = dataset.copy() + person, benunit = dataset.person, dataset.benunit + for column in SPI_DONOR_ZEROED_PERSON_VARIABLES: + if column in person.columns: + person[column] = 0.0 + if donor_person is not None: + for column in SPI_DONOR_RESTORED_PERSON_VARIABLES: + if column in person.columns and column in donor_person.columns: + person[column] = donor_person[column].values + + if "receives_benefits_in_own_right" in person.columns: + own_right = person.reindex( + columns=list(BENEFITS_IN_OWN_RIGHT_REPORTED_COLUMNS), fill_value=0.0 + ) + person["receives_benefits_in_own_right"] = ( + derive_receives_benefits_in_own_right(own_right).values + ) + + year = int(str(dataset.time_period)[:4]) + generator = np.random.default_rng(seed=SPI_DONOR_TAKEUP_SEED) + for flag in SPI_DONOR_REDRAWN_TAKEUP_FLAGS: + draws = generator.random(len(benunit)) + report_column = REPORTED_TAKEUP_ANCHORS[flag][1] + if flag in benunit.columns and report_column in person.columns: + benunit[flag] = assign_reported_takeup(person, benunit, flag, year, draws) + if uc_pension_age_excluded is not None and "would_claim_uc" in benunit.columns: + benunit["would_claim_uc"] &= ~uc_pension_age_excluded + return dataset + def _one_hot_encode(df: pd.DataFrame, columns: list[str]) -> pd.DataFrame: """Return ``df`` with object-typed ``columns`` one-hot encoded. @@ -177,11 +323,13 @@ def impute_frs_only_variables( to predict values for every row of ``target_dataset``; predictions replace the existing (donor-leaked) values in ``FRS_ONLY_PERSON_VARIABLES`` only. Variables absent from either - frame are skipped silently. + frame are skipped silently. ``apply_spi_donor_benefit_rules`` then + zeroes or restores some reports and rebuilds the flags derived from + them, before the disability categories and flags are derived from the + final reports. """ - from policyengine_uk_data.utils.qrf import QRF - target_dataset = target_dataset.copy() + donor_person = target_dataset.person.copy() train_person = train_dataset.person target_person = target_dataset.person @@ -200,13 +348,39 @@ def impute_frs_only_variables( len(missing), sorted(missing), ) - if not outputs: + if outputs: + target_dataset = _impute_outputs(train_dataset, target_dataset, outputs) + else: logger.warning( "Stage-2 FRS-only imputation: no output variables available; " - "returning target_dataset unchanged." + "applying only the SPI-donor benefit rules." ) - return target_dataset + year = int(str(target_dataset.time_period)[:4]) + target_dataset = apply_spi_donor_benefit_rules( + target_dataset, + donor_person, + uc_pension_age_excluded=_all_claimants_over_state_pension_age( + target_dataset, year + ), + ) + target_dataset.person = add_disability_benefit_categories_from_reported_amounts( + target_dataset.person, + int(str(target_dataset.time_period)[:4]), + ) + target_dataset.person = add_disability_benefit_flags_from_reported_amounts( + target_dataset.person, + int(str(target_dataset.time_period)[:4]), + ) + + return target_dataset + + +def _impute_outputs(train_dataset, target_dataset, outputs): + """Fit the stage-2 QRF and write its draws of ``outputs`` to the target.""" + from policyengine_uk_data.utils.qrf import QRF + + train_person = train_dataset.person train_inputs_raw = _build_predictor_frame(train_dataset) target_inputs_raw = _build_predictor_frame(target_dataset) @@ -240,14 +414,4 @@ def impute_frs_only_variables( # amounts or contributions and are non-negative by construction. values = np.maximum(predictions[column].values, 0.0) target_dataset.person[column] = values - - target_dataset.person = add_disability_benefit_categories_from_reported_amounts( - target_dataset.person, - int(str(target_dataset.time_period)[:4]), - ) - target_dataset.person = add_disability_benefit_flags_from_reported_amounts( - target_dataset.person, - int(str(target_dataset.time_period)[:4]), - ) - return target_dataset diff --git a/policyengine_uk_data/datasets/imputations/income.py b/policyengine_uk_data/datasets/imputations/income.py index feafcb27c..799b5f35e 100644 --- a/policyengine_uk_data/datasets/imputations/income.py +++ b/policyengine_uk_data/datasets/imputations/income.py @@ -4,11 +4,16 @@ This module imputes detailed income components (employment, self-employment, pensions, property, savings interest, dividends) using machine learning models trained on HMRC Survey of Personal Incomes (SPI) data. + +The draw is conditioned on each person's earnings group (see +``EARNINGS_GROUPS``) as well as their age, gender and region, so the incomes +agree with the employment status the FRS donor row keeps. """ import pandas as pd import numpy as np import os +import pickle from policyengine_uk_data.storage import STORAGE_FOLDER from policyengine_uk.data import UKSingleYearDataset from policyengine_uk import Microsimulation @@ -20,6 +25,11 @@ ) from policyengine_uk_data.utils.stack import stack_datasets from policyengine_uk_data.utils.subsample import subsample_dataset +from policyengine_uk_data.utils.employment_status import ( + CHILD_STATUS, + EMPLOYEE_STATUSES, + SELF_EMPLOYED_STATUSES, +) SPI_TAB_FOLDER = STORAGE_FOLDER / SPI_RELEASE_NAME SPI_RENAMES = dict( @@ -51,6 +61,103 @@ def _spi_age_bounds(age_code) -> tuple[int, int]: return AGE_RANGES[-1] +# Earnings groups that both surveys identify. The SPI has no ILO employment +# status and no hours, but it records each taxpayer's income sources: pay, and +# whether they file the self-employment pages of a tax return for a trade or +# partnership (SEINC_NUM, "Indicator for self-employed cases"). PROFITS is +# floored at zero, so SEINC_NUM is what finds break-even and loss-making +# traders. The FRS gives the same split through the main job's ILO status +# (EMPSTATI) and any second-job earnings. +NO_EARNINGS = "NO_EARNINGS" +EMPLOYEE = "EMPLOYEE" +SELF_EMPLOYED = "SELF_EMPLOYED" +EMPLOYEE_AND_SELF_EMPLOYED = "EMPLOYEE_AND_SELF_EMPLOYED" +EARNINGS_GROUPS = ( + NO_EARNINGS, + EMPLOYEE, + SELF_EMPLOYED, + EMPLOYEE_AND_SELF_EMPLOYED, +) +# FRS rows that take no SPI draw and keep their own values: the FRS child +# table (dependent children, including 16-19-year-olds in education), whose +# earnings this FRS build does not record, and anyone under 16. With no +# earnings or job information, there is nothing to tie a taxpayer's SPI +# incomes to. +NOT_IMPUTED = "NOT_IMPUTED" + + +# Every earnings group gets at least this share of the nominal training sample +# size, so the small self-employed groups are not fitted on a few thousand +# records. Floors are added on top, so the sample can exceed the nominal size. +MIN_GROUP_SAMPLE_SHARE = 0.1 + + +def earnings_group(has_pay, has_trade) -> np.ndarray: + """Earnings group from whether a person has pay and a trade.""" + has_pay = np.asarray(has_pay, dtype=bool) + has_trade = np.asarray(has_trade, dtype=bool) + return np.select( + [has_pay & has_trade, has_trade, has_pay], + [EMPLOYEE_AND_SELF_EMPLOYED, SELF_EMPLOYED, EMPLOYEE], + NO_EARNINGS, + ).astype(object) + + +def spi_earnings_group( + employment_income, self_employment_income, self_employed_indicator +) -> np.ndarray: + """Earnings group of SPI records. + + Pay is PAY + EPB + TAXTERM. A trade is SEINC_NUM = 1 (self-employment + pages filed, whatever the profit) or any assessable profit. + """ + has_trade = (np.asarray(self_employed_indicator) == 1) | ( + np.asarray(self_employment_income, dtype=float) > 0 + ) + return earnings_group(np.asarray(employment_income, dtype=float) > 0, has_trade) + + +def frs_earnings_group( + employment_status, age, employment_income, self_employment_income +) -> np.ndarray: + """Earnings group of FRS rows, or ``NOT_IMPUTED`` for children. + + An employee main job always draws pay and a self-employed main job always + draws a trade, whatever the FRS recorded for the donor. Earnings the FRS + records outside the main job (a second job or a side trade) add the other + source. Everyone else, retired, unemployed or inactive, draws from SPI + records with neither. + """ + status = np.asarray(employment_status, dtype=object) + has_pay = np.isin(status, EMPLOYEE_STATUSES) | ( + np.asarray(employment_income, dtype=float) > 0 + ) + has_trade = np.isin(status, SELF_EMPLOYED_STATUSES) | ( + np.asarray(self_employment_income, dtype=float) > 0 + ) + is_child = (status == CHILD_STATUS) | (np.asarray(age, dtype=float) < 16) + return np.where(is_child, NOT_IMPUTED, earnings_group(has_pay, has_trade)).astype( + object + ) + + +def earnings_group_sample_sizes( + group_weights: dict[str, float], sample_size: int +) -> dict[str, int]: + """Training records per earnings group: the group's weighted share of + ``sample_size``, but never under ``MIN_GROUP_SAMPLE_SHARE`` of + ``sample_size``. Groups with no weight get no records. Floored groups are + not offset elsewhere, so the total can exceed ``sample_size`` (by at most + one floor per group, plus rounding).""" + weights = {group: float(w) for group, w in group_weights.items() if w > 0} + total = sum(weights.values()) + floor = int(np.ceil(MIN_GROUP_SAMPLE_SHARE * sample_size)) + return { + group: max(int(round(sample_size * w / total)), floor) + for group, w in weights.items() + } + + def generate_spi_table( spi: pd.DataFrame, seed: int = 0, @@ -61,9 +168,13 @@ def generate_spi_table( Args: spi: Raw SPI survey data DataFrame. + seed: Seed for the age draw and the resample. + sample_size: If set, resample records with replacement, in proportion + to their weight within each earnings group, with each group's + record count from ``earnings_group_sample_sizes``. Returns: - Cleaned DataFrame with age and region mappings applied. + Cleaned DataFrame with age, region and earnings group. """ rng = np.random.default_rng(seed) age_range = spi.AGERANGE @@ -78,16 +189,25 @@ def generate_spi_table( spi[rename] = spi[SPI_RENAMES[rename]] spi["employment_income"] = spi[["PAY", "EPB", "TAXTERM"]].sum(axis=1) + spi["earnings_group"] = spi_earnings_group( + spi.employment_income, spi.self_employment_income, spi.SEINC_NUM + ) if sample_size is not None: + sizes = earnings_group_sample_sizes( + spi.groupby("earnings_group").person_weight.sum().to_dict(), + sample_size, + ) spi = pd.concat( [ - spi.sample( - sample_size, - weights=spi.person_weight, + spi[spi.earnings_group == group].sample( + sizes[group], + weights="person_weight", replace=True, - random_state=seed, - ), + random_state=seed + i, + ) + for i, group in enumerate(EARNINGS_GROUPS) + if group in sizes ] ) @@ -130,6 +250,8 @@ def generate_spi_table( "spi_release_name": SPI_RELEASE_NAME, "spi_tab_filename": SPI_TAB_FILENAME, "imputations": tuple(IMPUTATIONS), + "earnings_groups": EARNINGS_GROUPS, + "min_group_sample_share": MIN_GROUP_SAMPLE_SHARE, } INCOME_MODEL_PATH = STORAGE_FOLDER / f"income_{SPI_RELEASE_NAME}.pkl" INCOME_MODEL_SAMPLE_SIZE = 100_000 @@ -149,29 +271,98 @@ def get_income_model_metadata() -> dict: } +class EarningsGroupIncomeModel: + """One QRF per earnings group, each fitted on that group's SPI records. + + A person is drawn only from SPI records in their own earnings group, so an + employee always draws pay, a self-employed person always draws a trade + (whose profit can be zero), and someone with neither draws neither. + ``predict`` returns NaN for rows in no group (``NOT_IMPUTED``). + """ + + def __init__(self, models: dict, metadata: dict | None = None): + self.models = models + self.metadata = metadata or {} + + @property + def imputed_variables(self) -> list[str]: + return list(IMPUTATIONS) + + def predict(self, X: pd.DataFrame) -> pd.DataFrame: + groups = np.asarray(X["earnings_group"], dtype=object) + output = pd.DataFrame(np.nan, index=X.index, columns=IMPUTATIONS) + for group, model in self.models.items(): + in_group = groups == group + if in_group.any(): + inputs = pd.DataFrame( + {column: np.asarray(X[column])[in_group] for column in PREDICTORS} + ) + draws = model.predict(inputs) + output.loc[in_group, IMPUTATIONS] = draws[IMPUTATIONS].to_numpy() + return output + + def save(self, file_path): + with open(file_path, "wb") as f: + pickle.dump( + { + "models": { + group: model.model for group, model in self.models.items() + }, + "input_columns": PREDICTORS, + "metadata": self.metadata, + }, + f, + ) + + @classmethod + def load(cls, file_path): + """The cached model, or None if the file holds another format.""" + from policyengine_uk_data.utils.qrf import QRF + + with open(file_path, "rb") as f: + data = pickle.load(f) + if not isinstance(data, dict) or "models" not in data: + return None + models = {} + for group, fitted in data["models"].items(): + model = QRF() + model.model = fitted + model.input_columns = data.get("input_columns", PREDICTORS) + models[group] = model + return cls(models, data.get("metadata", {})) + + def _income_model_matches_current_release(model) -> bool: - if getattr(model, "metadata", {}) != get_income_model_metadata(): + if model is None or getattr(model, "metadata", {}) != get_income_model_metadata(): return False - cached_outputs = set(getattr(model.model, "imputed_variables", [])) - return cached_outputs == set(IMPUTATIONS) + models = getattr(model, "models", {}) + if set(models) != set(EARNINGS_GROUPS): + return False + return all( + set(getattr(group_model.model, "imputed_variables", [])) == set(IMPUTATIONS) + for group_model in models.values() + ) def save_imputation_models(): """ - Train and save income imputation model. + Train and save the income imputation model: one QRF per earnings group. Returns: - Trained QRF model for income imputation. + Trained ``EarningsGroupIncomeModel``. """ from policyengine_uk_data.utils import QRF - income = QRF() - income.metadata = get_income_model_metadata() spi = pd.read_csv(SPI_TAB_FOLDER / SPI_TAB_FILENAME, delimiter="\t") spi = generate_spi_table(spi, sample_size=get_income_model_sample_size()) - spi = spi[PREDICTORS + IMPUTATIONS] - income.fit(spi[PREDICTORS], spi[IMPUTATIONS]) + models = {} + for group in EARNINGS_GROUPS: + training = spi[spi.earnings_group == group] + model = QRF() + model.fit(training[PREDICTORS], training[IMPUTATIONS]) + models[group] = model + income = EarningsGroupIncomeModel(models, get_income_model_metadata()) income.save(INCOME_MODEL_PATH) return income @@ -180,46 +371,82 @@ def create_income_model(overwrite_existing: bool = False): """ Create or load income imputation model. - If a cached model exists and its training metadata or output columns don't - match the current SPI release and ``IMPUTATIONS`` list, the cache is - discarded and the model is retrained. + If a cached model exists and its training metadata, earnings groups or + output columns don't match the current SPI release and ``IMPUTATIONS`` + list, the cache is discarded and the model is retrained. Args: overwrite_existing: Whether to retrain model if it exists. Returns: - QRF model for income imputation. + ``EarningsGroupIncomeModel`` for income imputation. """ - from policyengine_uk_data.utils.qrf import QRF - if INCOME_MODEL_PATH.exists() and not overwrite_existing: - cached = QRF(file_path=INCOME_MODEL_PATH) + cached = EarningsGroupIncomeModel.load(INCOME_MODEL_PATH) if _income_model_matches_current_release(cached): return cached - # Cached model was trained against a different SPI release or output set. + # Cached model was trained against a different SPI release, output + # set or grouping. return save_imputation_models() +def income_model_inputs(dataset: UKSingleYearDataset) -> pd.DataFrame: + """Predictors (age, gender, region) and earnings group of each person.""" + sim = Microsimulation(dataset=dataset) + frame = sim.calculate_dataframe(["age", "gender", "region"]) + inputs = pd.DataFrame({column: np.asarray(frame[column]) for column in PREDICTORS}) + person = dataset.person + inputs["earnings_group"] = frs_earnings_group( + person.employment_status, + inputs.age, + person.employment_income, + person.self_employment_income, + ) + return inputs + + +def apply_income_draws( + person: pd.DataFrame, draws: pd.DataFrame, groups, output_variables +) -> pd.DataFrame: + """Write ``draws`` over ``output_variables`` for every person in an + earnings group (missing draws become zero). ``NOT_IMPUTED`` rows keep + their own values.""" + person = person.copy() + drawn = np.asarray(groups, dtype=object) != NOT_IMPUTED + for column in output_variables: + draw = np.nan_to_num(np.asarray(draws[column], dtype=float), nan=0.0) + own = ( + np.asarray(person[column], dtype=float) + if column in person.columns + else np.zeros(len(person)) + ) + person[column] = np.where(drawn, draw, own) + return person + + def impute_over_incomes( dataset: UKSingleYearDataset, model, output_variables: list[str] ) -> pd.DataFrame: """ Impute specified income components using trained model. + Each person draws from SPI records in their earnings group + (``frs_earnings_group``); children keep their own values. + Args: dataset: PolicyEngine UK dataset to augment with income data. + model: Fitted ``EarningsGroupIncomeModel``. output_variables: List of income components to impute. Returns: DataFrame with imputed income components. """ dataset = dataset.copy() - sim = Microsimulation(dataset=dataset) - input_df = sim.calculate_dataframe(["age", "gender", "region"]) + input_df = income_model_inputs(dataset) output_df = model.predict(input_df) - - for column in output_variables: - dataset.person[column] = output_df[column].fillna(0).values + dataset.person = apply_income_draws( + dataset.person, output_df, input_df.earnings_group, output_variables + ) # Housing costs (rent, mortgage interest, mortgage capital) used to be # rescaled here by new_income_total / original_income_total across @@ -234,13 +461,26 @@ def impute_over_incomes( return dataset +def clear_frs_reported_capital(dataset: UKSingleYearDataset) -> UKSingleYearDataset: + """Set ``pension_credit_reported_capital`` to -1 (none recorded). + + Used on the SPI-synthetic copy. The FRS benefit-unit capital belongs to the + FRS donor, whose incomes the SPI imputation replaces; keeping it would + assess an SPI-income unit on the donor's capital. With -1, policyengine-uk + uses the household capital proxy for these rows. + """ + if "pension_credit_reported_capital" in dataset.benunit.columns: + dataset.benunit["pension_credit_reported_capital"] = -1.0 + return dataset + + def impute_income(dataset: UKSingleYearDataset) -> UKSingleYearDataset: """ Impute detailed income components using trained model. Uses SPI-trained models to predict various income sources for individuals - based on age, gender, and region. Creates a synthetic population with - the imputed income data. + based on age, gender, region and earnings group. Creates a synthetic + population with the imputed income data. Args: dataset: PolicyEngine UK dataset to augment with income data. @@ -262,6 +502,7 @@ def impute_income(dataset: UKSingleYearDataset) -> UKSingleYearDataset: zero_weight_copy = dataset.copy() zero_weight_copy.household.household_weight = 0 zero_weight_copy.household["household_is_spi_synthetic"] = True + zero_weight_copy = clear_frs_reported_capital(zero_weight_copy) zero_weight_copy = subsample_dataset(zero_weight_copy, 10_000) model = create_income_model() @@ -292,6 +533,23 @@ def impute_income(dataset: UKSingleYearDataset) -> UKSingleYearDataset: target_dataset=zero_weight_copy, ) + # The copy keeps its FRS donor's employment status but now has SPI + # incomes, so derive its gainful self-employment flag again from the + # copy's own values. + if "uc_is_in_gainful_self_employment" in zero_weight_copy.person.columns: + from policyengine_uk_data.datasets.frs import ( + derive_uc_is_in_gainful_self_employment, + ) + + person = zero_weight_copy.person + person["uc_is_in_gainful_self_employment"] = ( + derive_uc_is_in_gainful_self_employment( + person.employment_status, + person.self_employment_income, + person.employment_income, + ) + ) + dataset = impute_over_incomes( dataset, model, diff --git a/policyengine_uk_data/datasets/pension_credit_takeup.py b/policyengine_uk_data/datasets/pension_credit_takeup.py new file mode 100644 index 000000000..0ba95aa7a --- /dev/null +++ b/policyengine_uk_data/datasets/pension_credit_takeup.py @@ -0,0 +1,148 @@ +"""Pension Credit take-up solved over entitled benefit units. + +The FRS build first draws ``would_claim_pc`` against an unweighted count of +every benefit unit, mostly units with no entitlement. Entitled non-reporters +then claim at about the take-up rate on top of every reporter, so take-up +among the entitled ends up well above it. This step redraws the flag once +the imputations are in; entitlement depends on imputed capital, so it has +to wait for them. + +For the calibration year it: +- finds the benefit units with positive Pension Credit entitlement; +- keeps reporters of Pension Credit as claimants, except on SPI-synthetic + households, whose reports the second-stage imputation (frs_only.py) + predicts from their SPI incomes rather than observes; +- solves the probability that makes weighted take-up among entitled units + in Great Britain equal DWP's caseload take-up rate, which covers Great + Britain only; +- applies that probability to every entitled non-reporter, in Northern + Ireland too; +- draws every unit with no entitlement in the calibration year at DWP's + take-up for Savings Credit-only awards (37% in FYE 2024), the rate at + which a unit claims once a reform or a later year entitles it. This step + finds no entitlement for them in the calibration year, so their rate only + matters under a reform, in a later year, or where the saved dataset's + calibration-year entitlement differs from this step's (#479). + +Weights are the survey grossing weights, before calibration. +""" + +import numpy as np + +from policyengine_uk_data.parameters import load_take_up_rate +from policyengine_uk_data.utils.takeup import solve_fill_probability + +# Separate from the FRS build's take-up generator, so redrawing here leaves +# every other stochastic input unchanged. +PENSION_CREDIT_TAKEUP_SEED = 1_792 + + +def spi_synthetic_benunits(dataset) -> np.ndarray: + """Benefit units in households flagged ``household_is_spi_synthetic``.""" + household = dataset.household + if "household_is_spi_synthetic" not in household.columns: + return np.zeros(len(dataset.benunit), dtype=bool) + synthetic = household.loc[ + household["household_is_spi_synthetic"].astype(bool), "household_id" + ] + person = dataset.person + benunits = person.loc[ + person["person_household_id"].isin(synthetic), "person_benunit_id" + ] + return dataset.benunit["benunit_id"].isin(benunits).to_numpy() + + +def pension_credit_takeup_flags( + draws, rate, weights, entitled, reported, great_britain, newly_entitled_rate +): + """Claim flags and the fill probability solved over GB entitled units. + + Only Great Britain enters the solve, because DWP's take-up rate covers + Great Britain. Reporters claim. Every other entitled unit, wherever it + lives, claims when its draw is below the probability; every unit that + isn't entitled claims when its draw is below ``newly_entitled_rate``. + """ + entitled = np.asarray(entitled, dtype=bool) + reported = np.asarray(reported, dtype=bool) + eligible = entitled & np.asarray(great_britain, dtype=bool) + probability = solve_fill_probability(rate, weights, eligible, reported) + claim_probability = np.where(entitled, probability, newly_entitled_rate) + claims = reported | (np.asarray(draws, dtype=np.float64) < claim_probability) + return claims, probability + + +def assign_pension_credit_takeup(dataset, year: int): + """Return a copy of ``dataset`` with ``would_claim_pc`` solved for + ``year``, and a summary of weighted aggregates. + + The probability is zero when entitled GB reporters already make up the + take-up rate of entitled GB units. The summary's GB reporter baseline + (reporters only, before any fill) is for comparison with DWP's absolute + caseload and spending, which calibration targets later and separately. + """ + from policyengine_uk import Microsimulation + + sim = Microsimulation(dataset=dataset) + entitlement = sim.calculate("pension_credit_entitlement", year).values + entitled = entitlement > 0 + reported_amount = sim.calculate( + "pension_credit_reported", year, map_to="benunit" + ).values + spi_synthetic = spi_synthetic_benunits(dataset) + reported = (reported_amount > 0) & ~spi_synthetic + weights = sim.calculate("benunit_weight", year).values + country = sim.calculate("country", year).values + in_ni = ( + sim.map_result( + sim.map_result( + (country == "NORTHERN_IRELAND").astype(float), "household", "person" + ), + "person", + "benunit", + ) + > 0 + ) + gb = ~in_ni + rate = load_take_up_rate("pension_credit", year) + newly_entitled_rate = load_take_up_rate("pension_credit_newly_entitled", year) + draws = np.random.default_rng(PENSION_CREDIT_TAKEUP_SEED).random(len(entitled)) + claims, probability = pension_credit_takeup_flags( + draws, rate, weights, entitled, reported, gb, newly_entitled_rate + ) + + dataset = dataset.copy() + dataset.benunit["would_claim_pc"] = claims + + def total(values, mask, scale): + return round(float((weights * values)[mask].sum()) / scale, 3) + + summary = { + "year": year, + "rate": rate, + "fill_probability": round(probability, 4), + "newly_entitled_rate": newly_entitled_rate, + "entitled_k": total(1, entitled, 1e3), + "gb_entitled_k": total(1, gb & entitled, 1e3), + "reporters_k": total(1, reported, 1e3), + "spi_synthetic_units_not_anchored": int( + (spi_synthetic & (reported_amount > 0)).sum() + ), + "entitled_reporters_k": total(1, entitled & reported, 1e3), + "gb_reporters_k": total(1, gb & reported, 1e3), + "gb_entitled_reporters_k": total(1, gb & entitled & reported, 1e3), + "gb_reported_pension_credit_bn": total(reported_amount, gb, 1e9), + "gb_reporters_modelled_pension_credit_bn": total( + entitlement, gb & reported, 1e9 + ), + "gb_claims_after_fill_k": total(1, gb & entitled & claims, 1e3), + "gb_pension_credit_after_fill_bn": total(entitlement, gb & claims, 1e9), + "gb_not_entitled_k": total(1, gb & ~entitled, 1e3), + "gb_not_entitled_flagged_share": round( + float( + weights[gb & ~entitled & claims].sum() + / max(weights[gb & ~entitled].sum(), 1) + ), + 4, + ), + } + return dataset, summary diff --git a/policyengine_uk_data/datasets/spi.py b/policyengine_uk_data/datasets/spi.py index e22be675c..41de96410 100644 --- a/policyengine_uk_data/datasets/spi.py +++ b/policyengine_uk_data/datasets/spi.py @@ -112,6 +112,8 @@ def create_spi( person["person_id"] = df.SREF person["person_household_id"] = df.SREF person["person_benunit_id"] = df.SREF + # Each SPI taxpayer is their own one-person benefit unit, so its claimant. + person["is_claimant_or_partner"] = np.ones(len(df), dtype=bool) benunit["benunit_id"] = df.SREF household["household_id"] = df.SREF diff --git a/policyengine_uk_data/parameters/take_up/pension_credit.yaml b/policyengine_uk_data/parameters/take_up/pension_credit.yaml index dc31d8502..3abd278d4 100644 --- a/policyengine_uk_data/parameters/take_up/pension_credit.yaml +++ b/policyengine_uk_data/parameters/take_up/pension_credit.yaml @@ -1,4 +1,4 @@ -description: Share of eligible Pension Credit recipients that participate. +description: Share of benefit units entitled to Pension Credit that receive it (caseload take-up, Great Britain). metadata: label: Pension Credit take-up rate name: PC_takeup @@ -6,5 +6,11 @@ metadata: reference: - title: "Income-related benefits: estimates of take-up: financial year 2019 to 2020" href: https://www.gov.uk/government/statistics/income-related-benefits-estimates-of-take-up-financial-year-2019-to-2020/income-related-benefits-estimates-of-take-up-financial-year-2019-to-2020#pension-credit-2 + - title: "Income-related benefits: estimates of take-up: financial year ending 2024" + href: https://www.gov.uk/government/statistics/income-related-benefits-estimates-of-take-up-financial-year-ending-2024/income-related-benefits-estimates-of-take-up-financial-year-ending-2024 values: 2015-01-01: 0.7 + # FYE 2023 and FYE 2024 caseload take-up (65% and 62%) from the FYE 2024 + # edition; expenditure take-up was 71% in FYE 2024. + 2022-04-01: 0.65 + 2023-04-01: 0.62 diff --git a/policyengine_uk_data/parameters/take_up/pension_credit_newly_entitled.yaml b/policyengine_uk_data/parameters/take_up/pension_credit_newly_entitled.yaml new file mode 100644 index 000000000..6c1dc1bc1 --- /dev/null +++ b/policyengine_uk_data/parameters/take_up/pension_credit_newly_entitled.yaml @@ -0,0 +1,14 @@ +description: Probability that a benefit unit with no Pension Credit entitlement in the calibration year claims it once a reform or a later year entitles it. Set to DWP's caseload take-up of Savings Credit-only awards (Great Britain), the group with the smallest awards, which is closest to what a reform gives the units it newly entitles. +metadata: + label: Pension Credit take-up for newly entitled units + name: PC_takeup_newly_entitled + unit: /1 + reference: + - title: "Income-related benefits: estimates of take-up: financial year ending 2024" + href: https://www.gov.uk/government/statistics/income-related-benefits-estimates-of-take-up-financial-year-ending-2024/income-related-benefits-estimates-of-take-up-financial-year-ending-2024 +values: + # Savings Credit-only caseload take-up, FYE 2023 and FYE 2024 (42% and + # 37%, table PC1). Mean award claimed in FYE 2024: £9 a week, against £87 + # for Guarantee Credit, whose take-up was 69%. + 2022-04-01: 0.42 + 2023-04-01: 0.37 diff --git a/policyengine_uk_data/storage/BRMA_DATA_SOURCES.md b/policyengine_uk_data/storage/BRMA_DATA_SOURCES.md new file mode 100644 index 000000000..2c01e058b --- /dev/null +++ b/policyengine_uk_data/storage/BRMA_DATA_SOURCES.md @@ -0,0 +1,49 @@ +# BRMA private-rented households + +File: `brma_private_rented_households.csv`, with columns `region, brma, bedrooms, households`. It holds private-rented households (private landlord or letting agency, plus other private rented) by region, Broad Rental Market Area (BRMA) and number of bedrooms (`1`, `2`, `3`, `4+`). Northern Ireland's 2021 census has no bedrooms question, so its rows have `bedrooms = all`. A BRMA that crosses a region boundary has one row per region, for the households on each side. + +`datasets/brma.py` uses it to draw each FRS benefit unit's BRMA within its region. LHA categories A and B (shared and one-bedroom) use one-bedroom homes, C uses two-bedroom, D three-bedroom and E four-or-more. + +## Sources + +| Nation | Households | BRMA geography | +|---|---|---| +| England | Census 2021 (ONS). Private-rented households per LSOA from TS054, split by bedrooms using the LSOA's mix for "private rented or lives rent free" (custom table `hh_tenure_5a` × `number_bedrooms_5a`). This is the only tenure × bedrooms split ONS releases for every LSOA; rent-free households are 0.3-1.2% of the category. | VOA BRMA boundary layer, May 2020. Each LSOA is assigned by its population-weighted centroid. Five border LSOAs fall outside the English layer and take the Welsh BRMA that VOA's LHA lookup returns near their centroids. | +| Wales | As for England. | Rent Officers Wales BRMA layer (2012 boundaries, 2014 release). 11 LSOAs fall in West Cheshire. Cardiff Bay (W01002024) lies outside the layer and is assigned to Cardiff, as VOA's lookup returns for its postcodes. | +| Scotland | Scotland's Census 2022 (NRS), tenure by bedrooms by 2022 electoral ward. This is the finest geography at which the table is not suppressed. | Scottish Government BRMA polygons. Each ward is split across BRMAs by its output areas' household counts, placing each output area by its population-weighted centroid. Rent Service Scotland's postcode-to-BRMA lookup (FOI 202300368850) moves 61 Balloch output areas to West Dunbartonshire: 59 on unique postcode matches and 2 via the council's own lookup. | +| Northern Ireland | NISRA Census 2021 households by postcode district, times Northern Ireland's private-rented share (NISRA tenure by Data Zone, 17.2%). | NIHE's definition of each BRMA as a set of postcode districts. | + +Northern Ireland's private-rented households are not split by area within the nation. Linking NISRA's tenure areas to postcode districts would need the ONS Postcode Directory's Northern Ireland records, whose licence (LPS end user licence) does not clearly allow publishing derived figures. Using every BRMA's all-tenure households instead moves BRMA shares by 1.5 percentage points on average; for example, Belfast gets 20.1% of NI's private renters where the private-rented split would give 23.5%. + +Totals reconcile with the published national private-rented counts to within 0.03%: + +| Nation | Private-rented households in this file | Published | +|---|---|---| +| England | 4,795,158 | 4,794,889 | +| Wales | 228,601 | 228,642 | +| Scotland | 323,001 | 323,042 | +| Northern Ireland | 132,449 | 132,436 | + +Small-area census counts are perturbed for disclosure control, so their sums differ slightly from national tables. + +## Validation + +The check below correlates, within each region, the BRMA shares of each candidate weight with DWP's count of Universal Credit households whose housing costs are assessed under LHA ("LHA covers rent" plus "does not cover rent"). The DWP figures are the mean of April 2019 to November 2020 (UC statistics supplementary table 3.2, February 2021). + +To make the comparison, each BRMA's households from this file are summed across bedroom bands and region parts, then placed in policyengine-uk's single region for that BRMA. + +| Nation | This file | `lha_list_of_rents.csv.gz` row counts (previous weights) | +|---|---|---| +| England | 0.94 | 0.82 | +| Wales | 0.95 | 0.23 | +| Scotland | 0.82 | 0.21 | + +The previous weights' Scottish, Welsh and Northern Ireland lists were copies of English BRMAs' lists (issue #515). + +Against the genuine list-of-rents category counts, the census bedroom bands correlate as follows (VOA 2019-20 for England; Scottish Government FOI 202200303624 for Scotland): +- categories B-E: 0.82 to 0.94 in England, 0.95 to 0.98 in Scotland; +- category A, shared accommodation: 0.51 in England, 0.95 in Scotland. The census has no measure of room lets, and no bedroom band does better than 0.59. + +## Rebuilding + +`tools/brma_households/` (run from the repository root) downloads every source, checks each one against a pinned sha256 and rebuilds this file byte for byte; see its `README.md`. diff --git a/policyengine_uk_data/storage/brma_private_rented_households.csv b/policyengine_uk_data/storage/brma_private_rented_households.csv new file mode 100644 index 000000000..fd84570e3 --- /dev/null +++ b/policyengine_uk_data/storage/brma_private_rented_households.csv @@ -0,0 +1,937 @@ +region,brma,bedrooms,households +EAST_MIDLANDS,CHERWELL_VALLEY,1,356 +EAST_MIDLANDS,CHERWELL_VALLEY,2,878 +EAST_MIDLANDS,CHERWELL_VALLEY,3,812 +EAST_MIDLANDS,CHERWELL_VALLEY,4+,339 +EAST_MIDLANDS,CHESTERFIELD,1,1745 +EAST_MIDLANDS,CHESTERFIELD,2,5822 +EAST_MIDLANDS,CHESTERFIELD,3,4064 +EAST_MIDLANDS,CHESTERFIELD,4+,724 +EAST_MIDLANDS,COVENTRY,1,12 +EAST_MIDLANDS,COVENTRY,2,49 +EAST_MIDLANDS,COVENTRY,3,54 +EAST_MIDLANDS,COVENTRY,4+,23 +EAST_MIDLANDS,DERBY,1,5006 +EAST_MIDLANDS,DERBY,2,14355 +EAST_MIDLANDS,DERBY,3,10808 +EAST_MIDLANDS,DERBY,4+,2805 +EAST_MIDLANDS,DONCASTER,1,60 +EAST_MIDLANDS,DONCASTER,2,129 +EAST_MIDLANDS,DONCASTER,3,342 +EAST_MIDLANDS,DONCASTER,4+,43 +EAST_MIDLANDS,EASTERN_STAFFORDSHIRE,1,577 +EAST_MIDLANDS,EASTERN_STAFFORDSHIRE,2,2367 +EAST_MIDLANDS,EASTERN_STAFFORDSHIRE,3,2099 +EAST_MIDLANDS,EASTERN_STAFFORDSHIRE,4+,492 +EAST_MIDLANDS,GRANTHAM_NEWARK,1,1613 +EAST_MIDLANDS,GRANTHAM_NEWARK,2,5029 +EAST_MIDLANDS,GRANTHAM_NEWARK,3,4791 +EAST_MIDLANDS,GRANTHAM_NEWARK,4+,1266 +EAST_MIDLANDS,GRIMSBY,1,71 +EAST_MIDLANDS,GRIMSBY,2,556 +EAST_MIDLANDS,GRIMSBY,3,596 +EAST_MIDLANDS,GRIMSBY,4+,186 +EAST_MIDLANDS,KINGS_LYNN,1,9 +EAST_MIDLANDS,KINGS_LYNN,2,38 +EAST_MIDLANDS,KINGS_LYNN,3,43 +EAST_MIDLANDS,KINGS_LYNN,4+,9 +EAST_MIDLANDS,LEICESTER,1,14418 +EAST_MIDLANDS,LEICESTER,2,30198 +EAST_MIDLANDS,LEICESTER,3,28635 +EAST_MIDLANDS,LEICESTER,4+,9210 +EAST_MIDLANDS,LINCOLN,1,2702 +EAST_MIDLANDS,LINCOLN,2,10111 +EAST_MIDLANDS,LINCOLN,3,9875 +EAST_MIDLANDS,LINCOLN,4+,3753 +EAST_MIDLANDS,LINCOLNSHIRE_FENS,1,1705 +EAST_MIDLANDS,LINCOLNSHIRE_FENS,2,4998 +EAST_MIDLANDS,LINCOLNSHIRE_FENS,3,5108 +EAST_MIDLANDS,LINCOLNSHIRE_FENS,4+,1279 +EAST_MIDLANDS,MILTON_KEYNES,1,69 +EAST_MIDLANDS,MILTON_KEYNES,2,174 +EAST_MIDLANDS,MILTON_KEYNES,3,211 +EAST_MIDLANDS,MILTON_KEYNES,4+,88 +EAST_MIDLANDS,NORTHAMPTON,1,4425 +EAST_MIDLANDS,NORTHAMPTON,2,10324 +EAST_MIDLANDS,NORTHAMPTON,3,9119 +EAST_MIDLANDS,NORTHAMPTON,4+,3311 +EAST_MIDLANDS,NORTHANTS_CENTRAL,1,3023 +EAST_MIDLANDS,NORTHANTS_CENTRAL,2,9453 +EAST_MIDLANDS,NORTHANTS_CENTRAL,3,10617 +EAST_MIDLANDS,NORTHANTS_CENTRAL,4+,2766 +EAST_MIDLANDS,NORTH_NOTTINGHAM,1,2835 +EAST_MIDLANDS,NORTH_NOTTINGHAM,2,10961 +EAST_MIDLANDS,NORTH_NOTTINGHAM,3,13873 +EAST_MIDLANDS,NORTH_NOTTINGHAM,4+,2250 +EAST_MIDLANDS,NOTTINGHAM,1,10467 +EAST_MIDLANDS,NOTTINGHAM,2,27504 +EAST_MIDLANDS,NOTTINGHAM,3,22810 +EAST_MIDLANDS,NOTTINGHAM,4+,8473 +EAST_MIDLANDS,PEAKS_DALES,1,1203 +EAST_MIDLANDS,PEAKS_DALES,2,2995 +EAST_MIDLANDS,PEAKS_DALES,3,2435 +EAST_MIDLANDS,PEAKS_DALES,4+,798 +EAST_MIDLANDS,PETERBOROUGH,1,735 +EAST_MIDLANDS,PETERBOROUGH,2,2653 +EAST_MIDLANDS,PETERBOROUGH,3,2816 +EAST_MIDLANDS,PETERBOROUGH,4+,886 +EAST_MIDLANDS,RUGBY_EAST,1,530 +EAST_MIDLANDS,RUGBY_EAST,2,1509 +EAST_MIDLANDS,RUGBY_EAST,3,1740 +EAST_MIDLANDS,RUGBY_EAST,4+,588 +EAST_MIDLANDS,SCUNTHORPE,1,38 +EAST_MIDLANDS,SCUNTHORPE,2,121 +EAST_MIDLANDS,SCUNTHORPE,3,125 +EAST_MIDLANDS,SCUNTHORPE,4+,43 +EAST_MIDLANDS,SHEFFIELD,1,254 +EAST_MIDLANDS,SHEFFIELD,2,774 +EAST_MIDLANDS,SHEFFIELD,3,637 +EAST_MIDLANDS,SHEFFIELD,4+,142 +EAST_MIDLANDS,SOUTHERN_GREATER_MANCHESTER,1,303 +EAST_MIDLANDS,SOUTHERN_GREATER_MANCHESTER,2,975 +EAST_MIDLANDS,SOUTHERN_GREATER_MANCHESTER,3,574 +EAST_MIDLANDS,SOUTHERN_GREATER_MANCHESTER,4+,130 +EAST_MIDLANDS,TAMESIDE_GLOSSOP,1,295 +EAST_MIDLANDS,TAMESIDE_GLOSSOP,2,1293 +EAST_MIDLANDS,TAMESIDE_GLOSSOP,3,617 +EAST_MIDLANDS,TAMESIDE_GLOSSOP,4+,140 +EAST_MIDLANDS,WOLDS_AND_COAST,1,2058 +EAST_MIDLANDS,WOLDS_AND_COAST,2,5120 +EAST_MIDLANDS,WOLDS_AND_COAST,3,3688 +EAST_MIDLANDS,WOLDS_AND_COAST,4+,972 +EAST_OF_ENGLAND,AYLESBURY,1,236 +EAST_OF_ENGLAND,AYLESBURY,2,357 +EAST_OF_ENGLAND,AYLESBURY,3,227 +EAST_OF_ENGLAND,AYLESBURY,4+,93 +EAST_OF_ENGLAND,BEDFORD,1,4529 +EAST_OF_ENGLAND,BEDFORD,2,7272 +EAST_OF_ENGLAND,BEDFORD,3,6514 +EAST_OF_ENGLAND,BEDFORD,4+,2210 +EAST_OF_ENGLAND,BURY_ST_EDMUNDS,1,2437 +EAST_OF_ENGLAND,BURY_ST_EDMUNDS,2,6728 +EAST_OF_ENGLAND,BURY_ST_EDMUNDS,3,7753 +EAST_OF_ENGLAND,BURY_ST_EDMUNDS,4+,3027 +EAST_OF_ENGLAND,CAMBRIDGE,1,7659 +EAST_OF_ENGLAND,CAMBRIDGE,2,14109 +EAST_OF_ENGLAND,CAMBRIDGE,3,10553 +EAST_OF_ENGLAND,CAMBRIDGE,4+,6066 +EAST_OF_ENGLAND,CENTRAL_NORFOLK_NORWICH,1,8207 +EAST_OF_ENGLAND,CENTRAL_NORFOLK_NORWICH,2,17902 +EAST_OF_ENGLAND,CENTRAL_NORFOLK_NORWICH,3,14734 +EAST_OF_ENGLAND,CENTRAL_NORFOLK_NORWICH,4+,5793 +EAST_OF_ENGLAND,CHELMSFORD,1,4759 +EAST_OF_ENGLAND,CHELMSFORD,2,8914 +EAST_OF_ENGLAND,CHELMSFORD,3,6226 +EAST_OF_ENGLAND,CHELMSFORD,4+,2199 +EAST_OF_ENGLAND,COLCHESTER,1,6512 +EAST_OF_ENGLAND,COLCHESTER,2,14452 +EAST_OF_ENGLAND,COLCHESTER,3,9234 +EAST_OF_ENGLAND,COLCHESTER,4+,3264 +EAST_OF_ENGLAND,HARLOW_STORTFORD,1,3690 +EAST_OF_ENGLAND,HARLOW_STORTFORD,2,6282 +EAST_OF_ENGLAND,HARLOW_STORTFORD,3,4484 +EAST_OF_ENGLAND,HARLOW_STORTFORD,4+,1968 +EAST_OF_ENGLAND,HUNTINGDON,1,1935 +EAST_OF_ENGLAND,HUNTINGDON,2,3934 +EAST_OF_ENGLAND,HUNTINGDON,3,3642 +EAST_OF_ENGLAND,HUNTINGDON,4+,1367 +EAST_OF_ENGLAND,IPSWICH,1,5383 +EAST_OF_ENGLAND,IPSWICH,2,11270 +EAST_OF_ENGLAND,IPSWICH,3,10432 +EAST_OF_ENGLAND,IPSWICH,4+,2736 +EAST_OF_ENGLAND,KINGS_LYNN,1,2856 +EAST_OF_ENGLAND,KINGS_LYNN,2,7381 +EAST_OF_ENGLAND,KINGS_LYNN,3,7310 +EAST_OF_ENGLAND,KINGS_LYNN,4+,2037 +EAST_OF_ENGLAND,LOWESTOFT_GREAT_YARMOUTH,1,4036 +EAST_OF_ENGLAND,LOWESTOFT_GREAT_YARMOUTH,2,7737 +EAST_OF_ENGLAND,LOWESTOFT_GREAT_YARMOUTH,3,7199 +EAST_OF_ENGLAND,LOWESTOFT_GREAT_YARMOUTH,4+,1430 +EAST_OF_ENGLAND,LUTON,1,7601 +EAST_OF_ENGLAND,LUTON,2,9363 +EAST_OF_ENGLAND,LUTON,3,8716 +EAST_OF_ENGLAND,LUTON,4+,2566 +EAST_OF_ENGLAND,MILTON_KEYNES,1,679 +EAST_OF_ENGLAND,MILTON_KEYNES,2,1759 +EAST_OF_ENGLAND,MILTON_KEYNES,3,1185 +EAST_OF_ENGLAND,MILTON_KEYNES,4+,372 +EAST_OF_ENGLAND,NORTHANTS_CENTRAL,1,21 +EAST_OF_ENGLAND,NORTHANTS_CENTRAL,2,46 +EAST_OF_ENGLAND,NORTHANTS_CENTRAL,3,73 +EAST_OF_ENGLAND,NORTHANTS_CENTRAL,4+,30 +EAST_OF_ENGLAND,NORTH_WEST_LONDON,1,69 +EAST_OF_ENGLAND,NORTH_WEST_LONDON,2,121 +EAST_OF_ENGLAND,NORTH_WEST_LONDON,3,106 +EAST_OF_ENGLAND,NORTH_WEST_LONDON,4+,164 +EAST_OF_ENGLAND,OUTER_NORTH_EAST_LONDON,1,829 +EAST_OF_ENGLAND,OUTER_NORTH_EAST_LONDON,2,1784 +EAST_OF_ENGLAND,OUTER_NORTH_EAST_LONDON,3,1155 +EAST_OF_ENGLAND,OUTER_NORTH_EAST_LONDON,4+,524 +EAST_OF_ENGLAND,OUTER_NORTH_LONDON,1,302 +EAST_OF_ENGLAND,OUTER_NORTH_LONDON,2,666 +EAST_OF_ENGLAND,OUTER_NORTH_LONDON,3,406 +EAST_OF_ENGLAND,OUTER_NORTH_LONDON,4+,187 +EAST_OF_ENGLAND,PETERBOROUGH,1,4249 +EAST_OF_ENGLAND,PETERBOROUGH,2,10206 +EAST_OF_ENGLAND,PETERBOROUGH,3,10034 +EAST_OF_ENGLAND,PETERBOROUGH,4+,3173 +EAST_OF_ENGLAND,SOUTHEND,1,7793 +EAST_OF_ENGLAND,SOUTHEND,2,11859 +EAST_OF_ENGLAND,SOUTHEND,3,7673 +EAST_OF_ENGLAND,SOUTHEND,4+,2487 +EAST_OF_ENGLAND,SOUTH_EAST_HERTS,1,4794 +EAST_OF_ENGLAND,SOUTH_EAST_HERTS,2,7948 +EAST_OF_ENGLAND,SOUTH_EAST_HERTS,3,5030 +EAST_OF_ENGLAND,SOUTH_EAST_HERTS,4+,2432 +EAST_OF_ENGLAND,SOUTH_WEST_ESSEX,1,5865 +EAST_OF_ENGLAND,SOUTH_WEST_ESSEX,2,10993 +EAST_OF_ENGLAND,SOUTH_WEST_ESSEX,3,8363 +EAST_OF_ENGLAND,SOUTH_WEST_ESSEX,4+,2429 +EAST_OF_ENGLAND,SOUTH_WEST_HERTS,1,10481 +EAST_OF_ENGLAND,SOUTH_WEST_HERTS,2,16159 +EAST_OF_ENGLAND,SOUTH_WEST_HERTS,3,8614 +EAST_OF_ENGLAND,SOUTH_WEST_HERTS,4+,4219 +EAST_OF_ENGLAND,STEVENAGE_NORTH_HERTS,1,4123 +EAST_OF_ENGLAND,STEVENAGE_NORTH_HERTS,2,5998 +EAST_OF_ENGLAND,STEVENAGE_NORTH_HERTS,3,4462 +EAST_OF_ENGLAND,STEVENAGE_NORTH_HERTS,4+,1441 +LONDON,CENTRAL_LONDON,1,31649 +LONDON,CENTRAL_LONDON,2,27084 +LONDON,CENTRAL_LONDON,3,10829 +LONDON,CENTRAL_LONDON,4+,5695 +LONDON,INNER_EAST_LONDON,1,29027 +LONDON,INNER_EAST_LONDON,2,28767 +LONDON,INNER_EAST_LONDON,3,12062 +LONDON,INNER_EAST_LONDON,4+,7563 +LONDON,INNER_NORTH_LONDON,1,37332 +LONDON,INNER_NORTH_LONDON,2,33252 +LONDON,INNER_NORTH_LONDON,3,14675 +LONDON,INNER_NORTH_LONDON,4+,9827 +LONDON,INNER_SOUTH_EAST_LONDON,1,40493 +LONDON,INNER_SOUTH_EAST_LONDON,2,46753 +LONDON,INNER_SOUTH_EAST_LONDON,3,20074 +LONDON,INNER_SOUTH_EAST_LONDON,4+,11807 +LONDON,INNER_SOUTH_WEST_LONDON,1,15333 +LONDON,INNER_SOUTH_WEST_LONDON,2,22328 +LONDON,INNER_SOUTH_WEST_LONDON,3,10024 +LONDON,INNER_SOUTH_WEST_LONDON,4+,6956 +LONDON,INNER_WEST_LONDON,1,23310 +LONDON,INNER_WEST_LONDON,2,24269 +LONDON,INNER_WEST_LONDON,3,10527 +LONDON,INNER_WEST_LONDON,4+,8178 +LONDON,NORTH_WEST_LONDON,1,16491 +LONDON,NORTH_WEST_LONDON,2,25700 +LONDON,NORTH_WEST_LONDON,3,18202 +LONDON,NORTH_WEST_LONDON,4+,9958 +LONDON,OUTER_EAST_LONDON,1,18601 +LONDON,OUTER_EAST_LONDON,2,24731 +LONDON,OUTER_EAST_LONDON,3,16365 +LONDON,OUTER_EAST_LONDON,4+,9288 +LONDON,OUTER_NORTH_EAST_LONDON,1,14992 +LONDON,OUTER_NORTH_EAST_LONDON,2,25660 +LONDON,OUTER_NORTH_EAST_LONDON,3,21035 +LONDON,OUTER_NORTH_EAST_LONDON,4+,7919 +LONDON,OUTER_NORTH_LONDON,1,28786 +LONDON,OUTER_NORTH_LONDON,2,36966 +LONDON,OUTER_NORTH_LONDON,3,20998 +LONDON,OUTER_NORTH_LONDON,4+,11989 +LONDON,OUTER_SOUTH_EAST_LONDON,1,14580 +LONDON,OUTER_SOUTH_EAST_LONDON,2,24800 +LONDON,OUTER_SOUTH_EAST_LONDON,3,15870 +LONDON,OUTER_SOUTH_EAST_LONDON,4+,5744 +LONDON,OUTER_SOUTH_LONDON,1,19336 +LONDON,OUTER_SOUTH_LONDON,2,25983 +LONDON,OUTER_SOUTH_LONDON,3,14514 +LONDON,OUTER_SOUTH_LONDON,4+,5892 +LONDON,OUTER_SOUTH_WEST_LONDON,1,13344 +LONDON,OUTER_SOUTH_WEST_LONDON,2,18231 +LONDON,OUTER_SOUTH_WEST_LONDON,3,8832 +LONDON,OUTER_SOUTH_WEST_LONDON,4+,6150 +LONDON,OUTER_WEST_LONDON,1,13522 +LONDON,OUTER_WEST_LONDON,2,19527 +LONDON,OUTER_WEST_LONDON,3,16016 +LONDON,OUTER_WEST_LONDON,4+,7743 +NORTHERN_IRELAND,BELFAST,all,26599 +NORTHERN_IRELAND,LOUGH_NEAGH_LOWER,all,14176 +NORTHERN_IRELAND,LOUGH_NEAGH_UPPER,all,26520 +NORTHERN_IRELAND,NORTH_NI,all,6892 +NORTHERN_IRELAND,NORTH_WEST_NI,all,11924 +NORTHERN_IRELAND,SOUTH_EAST_NI,all,25164 +NORTHERN_IRELAND,SOUTH_NI,all,12101 +NORTHERN_IRELAND,SOUTH_WEST_NI,all,9073 +NORTH_EAST,DARLINGTON,1,1231 +NORTH_EAST,DARLINGTON,2,6387 +NORTH_EAST,DARLINGTON,3,4590 +NORTH_EAST,DARLINGTON,4+,1147 +NORTH_EAST,DURHAM,1,2107 +NORTH_EAST,DURHAM,2,13762 +NORTH_EAST,DURHAM,3,11383 +NORTH_EAST,DURHAM,4+,3098 +NORTH_EAST,NORTHUMBERLAND,1,1847 +NORTH_EAST,NORTHUMBERLAND,2,8457 +NORTH_EAST,NORTHUMBERLAND,3,7131 +NORTH_EAST,NORTHUMBERLAND,4+,1531 +NORTH_EAST,NORTH_CUMBRIA,1,7 +NORTH_EAST,NORTH_CUMBRIA,2,54 +NORTH_EAST,NORTH_CUMBRIA,3,57 +NORTH_EAST,NORTH_CUMBRIA,4+,40 +NORTH_EAST,SUNDERLAND,1,2816 +NORTH_EAST,SUNDERLAND,2,11273 +NORTH_EAST,SUNDERLAND,3,9723 +NORTH_EAST,SUNDERLAND,4+,1846 +NORTH_EAST,TEESSIDE,1,4281 +NORTH_EAST,TEESSIDE,2,15747 +NORTH_EAST,TEESSIDE,3,20170 +NORTH_EAST,TEESSIDE,4+,3634 +NORTH_EAST,TYNESIDE,1,8359 +NORTH_EAST,TYNESIDE,2,33772 +NORTH_EAST,TYNESIDE,3,21105 +NORTH_EAST,TYNESIDE,4+,7009 +NORTH_WEST,BARROW_IN_FURNESS,1,845 +NORTH_WEST,BARROW_IN_FURNESS,2,3675 +NORTH_WEST,BARROW_IN_FURNESS,3,2212 +NORTH_WEST,BARROW_IN_FURNESS,4+,497 +NORTH_WEST,BOLTON_AND_BURY,1,4087 +NORTH_WEST,BOLTON_AND_BURY,2,18220 +NORTH_WEST,BOLTON_AND_BURY,3,11388 +NORTH_WEST,BOLTON_AND_BURY,4+,2348 +NORTH_WEST,CENTRAL_GREATER_MANCHESTER,1,24412 +NORTH_WEST,CENTRAL_GREATER_MANCHESTER,2,48853 +NORTH_WEST,CENTRAL_GREATER_MANCHESTER,3,28015 +NORTH_WEST,CENTRAL_GREATER_MANCHESTER,4+,10083 +NORTH_WEST,CENTRAL_LANCS,1,4143 +NORTH_WEST,CENTRAL_LANCS,2,11325 +NORTH_WEST,CENTRAL_LANCS,3,10502 +NORTH_WEST,CENTRAL_LANCS,4+,2726 +NORTH_WEST,EAST_CHESHIRE,1,1562 +NORTH_WEST,EAST_CHESHIRE,2,4887 +NORTH_WEST,EAST_CHESHIRE,3,3028 +NORTH_WEST,EAST_CHESHIRE,4+,1290 +NORTH_WEST,EAST_LANCS,1,2753 +NORTH_WEST,EAST_LANCS,2,13355 +NORTH_WEST,EAST_LANCS,3,8896 +NORTH_WEST,EAST_LANCS,4+,2042 +NORTH_WEST,FYLDE_COAST,1,8378 +NORTH_WEST,FYLDE_COAST,2,12915 +NORTH_WEST,FYLDE_COAST,3,12035 +NORTH_WEST,FYLDE_COAST,4+,2668 +NORTH_WEST,GREATER_LIVERPOOL,1,13273 +NORTH_WEST,GREATER_LIVERPOOL,2,26374 +NORTH_WEST,GREATER_LIVERPOOL,3,26149 +NORTH_WEST,GREATER_LIVERPOOL,4+,7309 +NORTH_WEST,KENDAL,1,1209 +NORTH_WEST,KENDAL,2,2599 +NORTH_WEST,KENDAL,3,2134 +NORTH_WEST,KENDAL,4+,771 +NORTH_WEST,LANCASTER,1,2279 +NORTH_WEST,LANCASTER,2,5166 +NORTH_WEST,LANCASTER,3,3755 +NORTH_WEST,LANCASTER,4+,1895 +NORTH_WEST,NORTH_CHESHIRE,1,3330 +NORTH_WEST,NORTH_CHESHIRE,2,10223 +NORTH_WEST,NORTH_CHESHIRE,3,8248 +NORTH_WEST,NORTH_CHESHIRE,4+,1601 +NORTH_WEST,NORTH_CUMBRIA,1,1755 +NORTH_WEST,NORTH_CUMBRIA,2,6387 +NORTH_WEST,NORTH_CUMBRIA,3,4711 +NORTH_WEST,NORTH_CUMBRIA,4+,1660 +NORTH_WEST,OLDHAM_ROCHDALE,1,2411 +NORTH_WEST,OLDHAM_ROCHDALE,2,13261 +NORTH_WEST,OLDHAM_ROCHDALE,3,8558 +NORTH_WEST,OLDHAM_ROCHDALE,4+,1660 +NORTH_WEST,SOUTHERN_GREATER_MANCHESTER,1,5118 +NORTH_WEST,SOUTHERN_GREATER_MANCHESTER,2,14979 +NORTH_WEST,SOUTHERN_GREATER_MANCHESTER,3,10204 +NORTH_WEST,SOUTHERN_GREATER_MANCHESTER,4+,2385 +NORTH_WEST,SOUTHPORT,1,3251 +NORTH_WEST,SOUTHPORT,2,5145 +NORTH_WEST,SOUTHPORT,3,4226 +NORTH_WEST,SOUTHPORT,4+,1371 +NORTH_WEST,SOUTH_CHESHIRE,1,2341 +NORTH_WEST,SOUTH_CHESHIRE,2,7247 +NORTH_WEST,SOUTH_CHESHIRE,3,6474 +NORTH_WEST,SOUTH_CHESHIRE,4+,2197 +NORTH_WEST,STAFFORDSHIRE_NORTH,1,9 +NORTH_WEST,STAFFORDSHIRE_NORTH,2,40 +NORTH_WEST,STAFFORDSHIRE_NORTH,3,24 +NORTH_WEST,STAFFORDSHIRE_NORTH,4+,5 +NORTH_WEST,ST_HELENS,1,1019 +NORTH_WEST,ST_HELENS,2,5783 +NORTH_WEST,ST_HELENS,3,5072 +NORTH_WEST,ST_HELENS,4+,728 +NORTH_WEST,TAMESIDE_GLOSSOP,1,1773 +NORTH_WEST,TAMESIDE_GLOSSOP,2,7921 +NORTH_WEST,TAMESIDE_GLOSSOP,3,4139 +NORTH_WEST,TAMESIDE_GLOSSOP,4+,782 +NORTH_WEST,WEST_CHESHIRE,1,2847 +NORTH_WEST,WEST_CHESHIRE,2,8218 +NORTH_WEST,WEST_CHESHIRE,3,7742 +NORTH_WEST,WEST_CHESHIRE,4+,2357 +NORTH_WEST,WEST_CUMBRIA,1,821 +NORTH_WEST,WEST_CUMBRIA,2,3052 +NORTH_WEST,WEST_CUMBRIA,3,2492 +NORTH_WEST,WEST_CUMBRIA,4+,633 +NORTH_WEST,WEST_PENNINE,1,1708 +NORTH_WEST,WEST_PENNINE,2,9867 +NORTH_WEST,WEST_PENNINE,3,5878 +NORTH_WEST,WEST_PENNINE,4+,1126 +NORTH_WEST,WIGAN,1,2507 +NORTH_WEST,WIGAN,2,11518 +NORTH_WEST,WIGAN,3,10381 +NORTH_WEST,WIGAN,4+,1618 +NORTH_WEST,WIRRAL,1,4176 +NORTH_WEST,WIRRAL,2,10388 +NORTH_WEST,WIRRAL,3,10855 +NORTH_WEST,WIRRAL,4+,2651 +SCOTLAND,ABERDEEN_AND_SHIRE,1,8886 +SCOTLAND,ABERDEEN_AND_SHIRE,2,14519 +SCOTLAND,ABERDEEN_AND_SHIRE,3,5895 +SCOTLAND,ABERDEEN_AND_SHIRE,4+,2663 +SCOTLAND,ARGYLL_AND_BUTE,1,1127 +SCOTLAND,ARGYLL_AND_BUTE,2,2130 +SCOTLAND,ARGYLL_AND_BUTE,3,1301 +SCOTLAND,ARGYLL_AND_BUTE,4+,438 +SCOTLAND,AYRSHIRES,1,3355 +SCOTLAND,AYRSHIRES,2,8338 +SCOTLAND,AYRSHIRES,3,4611 +SCOTLAND,AYRSHIRES,4+,909 +SCOTLAND,DUMFRIES_AND_GALLOWAY,1,1520 +SCOTLAND,DUMFRIES_AND_GALLOWAY,2,4075 +SCOTLAND,DUMFRIES_AND_GALLOWAY,3,2990 +SCOTLAND,DUMFRIES_AND_GALLOWAY,4+,904 +SCOTLAND,DUNDEE_AND_ANGUS,1,5707 +SCOTLAND,DUNDEE_AND_ANGUS,2,10456 +SCOTLAND,DUNDEE_AND_ANGUS,3,4441 +SCOTLAND,DUNDEE_AND_ANGUS,4+,1540 +SCOTLAND,EAST_DUNBARTONSHIRE,1,599 +SCOTLAND,EAST_DUNBARTONSHIRE,2,1262 +SCOTLAND,EAST_DUNBARTONSHIRE,3,672 +SCOTLAND,EAST_DUNBARTONSHIRE,4+,173 +SCOTLAND,FIFE,1,3320 +SCOTLAND,FIFE,2,9595 +SCOTLAND,FIFE,3,5270 +SCOTLAND,FIFE,4+,1462 +SCOTLAND,FORTH_VALLEY,1,2636 +SCOTLAND,FORTH_VALLEY,2,6918 +SCOTLAND,FORTH_VALLEY,3,2937 +SCOTLAND,FORTH_VALLEY,4+,790 +SCOTLAND,GREATER_GLASGOW,1,14978 +SCOTLAND,GREATER_GLASGOW,2,26740 +SCOTLAND,GREATER_GLASGOW,3,7442 +SCOTLAND,GREATER_GLASGOW,4+,2112 +SCOTLAND,HIGHLAND_AND_ISLANDS,1,3284 +SCOTLAND,HIGHLAND_AND_ISLANDS,2,8469 +SCOTLAND,HIGHLAND_AND_ISLANDS,3,5915 +SCOTLAND,HIGHLAND_AND_ISLANDS,4+,1841 +SCOTLAND,LOTHIAN,1,18074 +SCOTLAND,LOTHIAN,2,30012 +SCOTLAND,LOTHIAN,3,10044 +SCOTLAND,LOTHIAN,4+,4327 +SCOTLAND,NORTH_LANARKSHIRE,1,2062 +SCOTLAND,NORTH_LANARKSHIRE,2,6339 +SCOTLAND,NORTH_LANARKSHIRE,3,3376 +SCOTLAND,NORTH_LANARKSHIRE,4+,669 +SCOTLAND,PERTH_AND_KINROSS,1,2098 +SCOTLAND,PERTH_AND_KINROSS,2,4029 +SCOTLAND,PERTH_AND_KINROSS,3,1965 +SCOTLAND,PERTH_AND_KINROSS,4+,668 +SCOTLAND,RENFREWSHIRE_AND_INVERCLYDE,1,4208 +SCOTLAND,RENFREWSHIRE_AND_INVERCLYDE,2,6991 +SCOTLAND,RENFREWSHIRE_AND_INVERCLYDE,3,2546 +SCOTLAND,RENFREWSHIRE_AND_INVERCLYDE,4+,503 +SCOTLAND,SCOTTISH_BORDERS,1,1461 +SCOTLAND,SCOTTISH_BORDERS,2,3189 +SCOTLAND,SCOTTISH_BORDERS,3,2271 +SCOTLAND,SCOTTISH_BORDERS,4+,646 +SCOTLAND,SOUTH_LANARKSHIRE,1,3051 +SCOTLAND,SOUTH_LANARKSHIRE,2,7569 +SCOTLAND,SOUTH_LANARKSHIRE,3,3282 +SCOTLAND,SOUTH_LANARKSHIRE,4+,689 +SCOTLAND,WEST_DUNBARTONSHIRE,1,927 +SCOTLAND,WEST_DUNBARTONSHIRE,2,1625 +SCOTLAND,WEST_DUNBARTONSHIRE,3,661 +SCOTLAND,WEST_DUNBARTONSHIRE,4+,108 +SCOTLAND,WEST_LOTHIAN,1,1247 +SCOTLAND,WEST_LOTHIAN,2,3461 +SCOTLAND,WEST_LOTHIAN,3,2186 +SCOTLAND,WEST_LOTHIAN,4+,497 +SOUTH_EAST,ASHFORD,1,1508 +SOUTH_EAST,ASHFORD,2,3832 +SOUTH_EAST,ASHFORD,3,3788 +SOUTH_EAST,ASHFORD,4+,1207 +SOUTH_EAST,AYLESBURY,1,2284 +SOUTH_EAST,AYLESBURY,2,4739 +SOUTH_EAST,AYLESBURY,3,3283 +SOUTH_EAST,AYLESBURY,4+,1325 +SOUTH_EAST,BASINGSTOKE,1,3291 +SOUTH_EAST,BASINGSTOKE,2,6246 +SOUTH_EAST,BASINGSTOKE,3,5391 +SOUTH_EAST,BASINGSTOKE,4+,1892 +SOUTH_EAST,BLACKWATER_VALLEY,1,5264 +SOUTH_EAST,BLACKWATER_VALLEY,2,9018 +SOUTH_EAST,BLACKWATER_VALLEY,3,7615 +SOUTH_EAST,BLACKWATER_VALLEY,4+,3087 +SOUTH_EAST,BOURNEMOUTH,1,594 +SOUTH_EAST,BOURNEMOUTH,2,1255 +SOUTH_EAST,BOURNEMOUTH,3,924 +SOUTH_EAST,BOURNEMOUTH,4+,270 +SOUTH_EAST,BRIGHTON_AND_HOVE,1,17890 +SOUTH_EAST,BRIGHTON_AND_HOVE,2,16622 +SOUTH_EAST,BRIGHTON_AND_HOVE,3,7918 +SOUTH_EAST,BRIGHTON_AND_HOVE,4+,5194 +SOUTH_EAST,CANTERBURY,1,3238 +SOUTH_EAST,CANTERBURY,2,6227 +SOUTH_EAST,CANTERBURY,3,4601 +SOUTH_EAST,CANTERBURY,4+,2806 +SOUTH_EAST,CHELTENHAM,1,106 +SOUTH_EAST,CHELTENHAM,2,280 +SOUTH_EAST,CHELTENHAM,3,277 +SOUTH_EAST,CHELTENHAM,4+,139 +SOUTH_EAST,CHERWELL_VALLEY,1,2457 +SOUTH_EAST,CHERWELL_VALLEY,2,4517 +SOUTH_EAST,CHERWELL_VALLEY,3,3859 +SOUTH_EAST,CHERWELL_VALLEY,4+,1424 +SOUTH_EAST,CHICHESTER,1,1944 +SOUTH_EAST,CHICHESTER,2,4293 +SOUTH_EAST,CHICHESTER,3,3781 +SOUTH_EAST,CHICHESTER,4+,1429 +SOUTH_EAST,CHILTERNS,1,4060 +SOUTH_EAST,CHILTERNS,2,6924 +SOUTH_EAST,CHILTERNS,3,5146 +SOUTH_EAST,CHILTERNS,4+,2712 +SOUTH_EAST,CRAWLEY_REIGATE,1,9830 +SOUTH_EAST,CRAWLEY_REIGATE,2,15294 +SOUTH_EAST,CRAWLEY_REIGATE,3,10364 +SOUTH_EAST,CRAWLEY_REIGATE,4+,4139 +SOUTH_EAST,DOVER_SHEPWAY,1,4422 +SOUTH_EAST,DOVER_SHEPWAY,2,7610 +SOUTH_EAST,DOVER_SHEPWAY,3,5141 +SOUTH_EAST,DOVER_SHEPWAY,4+,1335 +SOUTH_EAST,EASTBOURNE,1,4841 +SOUTH_EAST,EASTBOURNE,2,7632 +SOUTH_EAST,EASTBOURNE,3,4582 +SOUTH_EAST,EASTBOURNE,4+,1523 +SOUTH_EAST,EAST_THAMES_VALLEY,1,7656 +SOUTH_EAST,EAST_THAMES_VALLEY,2,11639 +SOUTH_EAST,EAST_THAMES_VALLEY,3,8361 +SOUTH_EAST,EAST_THAMES_VALLEY,4+,4016 +SOUTH_EAST,GUILDFORD,1,6007 +SOUTH_EAST,GUILDFORD,2,8586 +SOUTH_EAST,GUILDFORD,3,5675 +SOUTH_EAST,GUILDFORD,4+,3789 +SOUTH_EAST,HIGH_WEALD,1,5098 +SOUTH_EAST,HIGH_WEALD,2,8475 +SOUTH_EAST,HIGH_WEALD,3,5556 +SOUTH_EAST,HIGH_WEALD,4+,2542 +SOUTH_EAST,ISLE_OF_WIGHT,1,3027 +SOUTH_EAST,ISLE_OF_WIGHT,2,5853 +SOUTH_EAST,ISLE_OF_WIGHT,3,3617 +SOUTH_EAST,ISLE_OF_WIGHT,4+,881 +SOUTH_EAST,LUTON,1,18 +SOUTH_EAST,LUTON,2,49 +SOUTH_EAST,LUTON,3,45 +SOUTH_EAST,LUTON,4+,22 +SOUTH_EAST,MAIDSTONE,1,3114 +SOUTH_EAST,MAIDSTONE,2,6250 +SOUTH_EAST,MAIDSTONE,3,4337 +SOUTH_EAST,MAIDSTONE,4+,1686 +SOUTH_EAST,MEDWAY_SWALE,1,6006 +SOUTH_EAST,MEDWAY_SWALE,2,11386 +SOUTH_EAST,MEDWAY_SWALE,3,11997 +SOUTH_EAST,MEDWAY_SWALE,4+,2685 +SOUTH_EAST,MILTON_KEYNES,1,4723 +SOUTH_EAST,MILTON_KEYNES,2,9451 +SOUTH_EAST,MILTON_KEYNES,3,8472 +SOUTH_EAST,MILTON_KEYNES,4+,3758 +SOUTH_EAST,NEWBURY,1,1769 +SOUTH_EAST,NEWBURY,2,3449 +SOUTH_EAST,NEWBURY,3,2589 +SOUTH_EAST,NEWBURY,4+,957 +SOUTH_EAST,NORTH_WEST_KENT,1,4009 +SOUTH_EAST,NORTH_WEST_KENT,2,7018 +SOUTH_EAST,NORTH_WEST_KENT,3,5516 +SOUTH_EAST,NORTH_WEST_KENT,4+,1477 +SOUTH_EAST,OUTER_SOUTH_EAST_LONDON,1,6 +SOUTH_EAST,OUTER_SOUTH_EAST_LONDON,2,31 +SOUTH_EAST,OUTER_SOUTH_EAST_LONDON,3,26 +SOUTH_EAST,OUTER_SOUTH_EAST_LONDON,4+,12 +SOUTH_EAST,OUTER_SOUTH_LONDON,1,1737 +SOUTH_EAST,OUTER_SOUTH_LONDON,2,3805 +SOUTH_EAST,OUTER_SOUTH_LONDON,3,1976 +SOUTH_EAST,OUTER_SOUTH_LONDON,4+,1047 +SOUTH_EAST,OUTER_SOUTH_WEST_LONDON,1,244 +SOUTH_EAST,OUTER_SOUTH_WEST_LONDON,2,483 +SOUTH_EAST,OUTER_SOUTH_WEST_LONDON,3,264 +SOUTH_EAST,OUTER_SOUTH_WEST_LONDON,4+,240 +SOUTH_EAST,OXFORD,1,7907 +SOUTH_EAST,OXFORD,2,13914 +SOUTH_EAST,OXFORD,3,11444 +SOUTH_EAST,OXFORD,4+,6761 +SOUTH_EAST,PORTSMOUTH,1,9482 +SOUTH_EAST,PORTSMOUTH,2,17019 +SOUTH_EAST,PORTSMOUTH,3,13783 +SOUTH_EAST,PORTSMOUTH,4+,4888 +SOUTH_EAST,READING,1,10276 +SOUTH_EAST,READING,2,15769 +SOUTH_EAST,READING,3,11155 +SOUTH_EAST,READING,4+,6138 +SOUTH_EAST,SALISBURY,1,172 +SOUTH_EAST,SALISBURY,2,433 +SOUTH_EAST,SALISBURY,3,676 +SOUTH_EAST,SALISBURY,4+,253 +SOUTH_EAST,SOUTHAMPTON,1,13339 +SOUTH_EAST,SOUTHAMPTON,2,18511 +SOUTH_EAST,SOUTHAMPTON,3,12589 +SOUTH_EAST,SOUTHAMPTON,4+,5961 +SOUTH_EAST,SUSSEX_EAST,1,4988 +SOUTH_EAST,SUSSEX_EAST,2,7500 +SOUTH_EAST,SUSSEX_EAST,3,4270 +SOUTH_EAST,SUSSEX_EAST,4+,1261 +SOUTH_EAST,SWINDON,1,182 +SOUTH_EAST,SWINDON,2,475 +SOUTH_EAST,SWINDON,3,487 +SOUTH_EAST,SWINDON,4+,359 +SOUTH_EAST,THANET,1,4115 +SOUTH_EAST,THANET,2,7065 +SOUTH_EAST,THANET,3,4014 +SOUTH_EAST,THANET,4+,1143 +SOUTH_EAST,WALTON,1,6513 +SOUTH_EAST,WALTON,2,10232 +SOUTH_EAST,WALTON,3,5708 +SOUTH_EAST,WALTON,4+,4088 +SOUTH_EAST,WINCHESTER,1,1380 +SOUTH_EAST,WINCHESTER,2,2907 +SOUTH_EAST,WINCHESTER,3,2625 +SOUTH_EAST,WINCHESTER,4+,1730 +SOUTH_EAST,WORTHING,1,8491 +SOUTH_EAST,WORTHING,2,9655 +SOUTH_EAST,WORTHING,3,6394 +SOUTH_EAST,WORTHING,4+,1916 +SOUTH_WEST,BATH,1,3796 +SOUTH_WEST,BATH,2,5235 +SOUTH_WEST,BATH,3,3954 +SOUTH_WEST,BATH,4+,2984 +SOUTH_WEST,BOURNEMOUTH,1,13820 +SOUTH_WEST,BOURNEMOUTH,2,21325 +SOUTH_WEST,BOURNEMOUTH,3,11275 +SOUTH_WEST,BOURNEMOUTH,4+,5167 +SOUTH_WEST,BRISTOL,1,19319 +SOUTH_WEST,BRISTOL,2,27170 +SOUTH_WEST,BRISTOL,3,18247 +SOUTH_WEST,BRISTOL,4+,10093 +SOUTH_WEST,CHELTENHAM,1,5456 +SOUTH_WEST,CHELTENHAM,2,8481 +SOUTH_WEST,CHELTENHAM,3,6148 +SOUTH_WEST,CHELTENHAM,4+,2827 +SOUTH_WEST,EXETER,1,4847 +SOUTH_WEST,EXETER,2,8719 +SOUTH_WEST,EXETER,3,6607 +SOUTH_WEST,EXETER,4+,3900 +SOUTH_WEST,GLOUCESTER,1,5360 +SOUTH_WEST,GLOUCESTER,2,10096 +SOUTH_WEST,GLOUCESTER,3,8032 +SOUTH_WEST,GLOUCESTER,4+,2972 +SOUTH_WEST,KERNOW_WEST,1,6911 +SOUTH_WEST,KERNOW_WEST,2,13526 +SOUTH_WEST,KERNOW_WEST,3,10967 +SOUTH_WEST,KERNOW_WEST,4+,3791 +SOUTH_WEST,MENDIP,1,1847 +SOUTH_WEST,MENDIP,2,3578 +SOUTH_WEST,MENDIP,3,3006 +SOUTH_WEST,MENDIP,4+,1025 +SOUTH_WEST,MID_EAST_DEVON,1,1639 +SOUTH_WEST,MID_EAST_DEVON,2,4121 +SOUTH_WEST,MID_EAST_DEVON,3,3133 +SOUTH_WEST,MID_EAST_DEVON,4+,1020 +SOUTH_WEST,MID_WEST_DORSET,1,3157 +SOUTH_WEST,MID_WEST_DORSET,2,5978 +SOUTH_WEST,MID_WEST_DORSET,3,4562 +SOUTH_WEST,MID_WEST_DORSET,4+,1482 +SOUTH_WEST,MONMOUTHSHIRE,1,31 +SOUTH_WEST,MONMOUTHSHIRE,2,92 +SOUTH_WEST,MONMOUTHSHIRE,3,171 +SOUTH_WEST,MONMOUTHSHIRE,4+,44 +SOUTH_WEST,NEWBURY,1,16 +SOUTH_WEST,NEWBURY,2,78 +SOUTH_WEST,NEWBURY,3,156 +SOUTH_WEST,NEWBURY,4+,58 +SOUTH_WEST,NORTH_CORNWALL_DEVON_BORDERS,1,1615 +SOUTH_WEST,NORTH_CORNWALL_DEVON_BORDERS,2,3413 +SOUTH_WEST,NORTH_CORNWALL_DEVON_BORDERS,3,3317 +SOUTH_WEST,NORTH_CORNWALL_DEVON_BORDERS,4+,1092 +SOUTH_WEST,NORTH_DEVON,1,2362 +SOUTH_WEST,NORTH_DEVON,2,5261 +SOUTH_WEST,NORTH_DEVON,3,4307 +SOUTH_WEST,NORTH_DEVON,4+,1467 +SOUTH_WEST,PLYMOUTH,1,8834 +SOUTH_WEST,PLYMOUTH,2,13867 +SOUTH_WEST,PLYMOUTH,3,10467 +SOUTH_WEST,PLYMOUTH,4+,3860 +SOUTH_WEST,SALISBURY,1,2062 +SOUTH_WEST,SALISBURY,2,5481 +SOUTH_WEST,SALISBURY,3,6992 +SOUTH_WEST,SALISBURY,4+,2419 +SOUTH_WEST,SOUTH_DEVON,1,7843 +SOUTH_WEST,SOUTH_DEVON,2,11753 +SOUTH_WEST,SOUTH_DEVON,3,7949 +SOUTH_WEST,SOUTH_DEVON,4+,2431 +SOUTH_WEST,SWINDON,1,4143 +SOUTH_WEST,SWINDON,2,9206 +SOUTH_WEST,SWINDON,3,6546 +SOUTH_WEST,SWINDON,4+,2132 +SOUTH_WEST,TAUNTON_WEST_SOMERSET,1,3673 +SOUTH_WEST,TAUNTON_WEST_SOMERSET,2,7597 +SOUTH_WEST,TAUNTON_WEST_SOMERSET,3,6272 +SOUTH_WEST,TAUNTON_WEST_SOMERSET,4+,1991 +SOUTH_WEST,WARWICKSHIRE_SOUTH,1,8 +SOUTH_WEST,WARWICKSHIRE_SOUTH,2,28 +SOUTH_WEST,WARWICKSHIRE_SOUTH,3,36 +SOUTH_WEST,WARWICKSHIRE_SOUTH,4+,4 +SOUTH_WEST,WESTON_S_MARE,1,3478 +SOUTH_WEST,WESTON_S_MARE,2,5871 +SOUTH_WEST,WESTON_S_MARE,3,3644 +SOUTH_WEST,WESTON_S_MARE,4+,1233 +SOUTH_WEST,WEST_WILTSHIRE,1,2967 +SOUTH_WEST,WEST_WILTSHIRE,2,6987 +SOUTH_WEST,WEST_WILTSHIRE,3,5996 +SOUTH_WEST,WEST_WILTSHIRE,4+,1990 +SOUTH_WEST,YEOVIL,1,2667 +SOUTH_WEST,YEOVIL,2,5707 +SOUTH_WEST,YEOVIL,3,5294 +SOUTH_WEST,YEOVIL,4+,1669 +WALES,BLAENAU_GWENT,1,442 +WALES,BLAENAU_GWENT,2,1882 +WALES,BLAENAU_GWENT,3,2314 +WALES,BLAENAU_GWENT,4+,205 +WALES,BRECON_AND_RADNOR,1,745 +WALES,BRECON_AND_RADNOR,2,1721 +WALES,BRECON_AND_RADNOR,3,1981 +WALES,BRECON_AND_RADNOR,4+,664 +WALES,BRIDGEND,1,939 +WALES,BRIDGEND,2,3401 +WALES,BRIDGEND,3,4340 +WALES,BRIDGEND,4+,834 +WALES,CAERPHILLY,1,956 +WALES,CAERPHILLY,2,3130 +WALES,CAERPHILLY,3,5449 +WALES,CAERPHILLY,4+,623 +WALES,CARDIFF,1,9179 +WALES,CARDIFF,2,13254 +WALES,CARDIFF,3,8166 +WALES,CARDIFF,4+,5181 +WALES,CARMARTHENSHIRE,1,1235 +WALES,CARMARTHENSHIRE,2,3805 +WALES,CARMARTHENSHIRE,3,5734 +WALES,CARMARTHENSHIRE,4+,1396 +WALES,CEREDIGION,1,1219 +WALES,CEREDIGION,2,2254 +WALES,CEREDIGION,3,2300 +WALES,CEREDIGION,4+,1135 +WALES,FLINTSHIRE,1,859 +WALES,FLINTSHIRE,2,2467 +WALES,FLINTSHIRE,3,3140 +WALES,FLINTSHIRE,4+,574 +WALES,MERTHYR_CYNON,1,794 +WALES,MERTHYR_CYNON,2,2975 +WALES,MERTHYR_CYNON,3,4407 +WALES,MERTHYR_CYNON,4+,581 +WALES,MONMOUTHSHIRE,1,734 +WALES,MONMOUTHSHIRE,2,1985 +WALES,MONMOUTHSHIRE,3,2142 +WALES,MONMOUTHSHIRE,4+,715 +WALES,NEATH_PORT_TALBOT,1,817 +WALES,NEATH_PORT_TALBOT,2,2926 +WALES,NEATH_PORT_TALBOT,3,4813 +WALES,NEATH_PORT_TALBOT,4+,616 +WALES,NEWPORT,1,1793 +WALES,NEWPORT,2,3724 +WALES,NEWPORT,3,4407 +WALES,NEWPORT,4+,1102 +WALES,NORTH_CLWYD,1,3263 +WALES,NORTH_CLWYD,2,7575 +WALES,NORTH_CLWYD,3,5398 +WALES,NORTH_CLWYD,4+,1462 +WALES,NORTH_POWYS,1,627 +WALES,NORTH_POWYS,2,1743 +WALES,NORTH_POWYS,3,2085 +WALES,NORTH_POWYS,4+,655 +WALES,NORTH_WEST_WALES,1,1328 +WALES,NORTH_WEST_WALES,2,3942 +WALES,NORTH_WEST_WALES,3,4244 +WALES,NORTH_WEST_WALES,4+,1696 +WALES,PEMBROKESHIRE,1,1352 +WALES,PEMBROKESHIRE,2,3059 +WALES,PEMBROKESHIRE,3,3233 +WALES,PEMBROKESHIRE,4+,928 +WALES,SOUTH_GWYNEDD,1,365 +WALES,SOUTH_GWYNEDD,2,942 +WALES,SOUTH_GWYNEDD,3,855 +WALES,SOUTH_GWYNEDD,4+,292 +WALES,SWANSEA,1,3298 +WALES,SWANSEA,2,6848 +WALES,SWANSEA,3,6170 +WALES,SWANSEA,4+,2588 +WALES,TAFF_RHONDDA,1,1074 +WALES,TAFF_RHONDDA,2,4038 +WALES,TAFF_RHONDDA,3,6984 +WALES,TAFF_RHONDDA,4+,1349 +WALES,TORFAEN,1,437 +WALES,TORFAEN,2,1704 +WALES,TORFAEN,3,2046 +WALES,TORFAEN,4+,308 +WALES,VALE_OF_GLAMORGAN,1,1394 +WALES,VALE_OF_GLAMORGAN,2,3506 +WALES,VALE_OF_GLAMORGAN,3,3266 +WALES,VALE_OF_GLAMORGAN,4+,820 +WALES,WEST_CHESHIRE,1,120 +WALES,WEST_CHESHIRE,2,408 +WALES,WEST_CHESHIRE,3,535 +WALES,WEST_CHESHIRE,4+,82 +WALES,WREXHAM,1,1281 +WALES,WREXHAM,2,4708 +WALES,WREXHAM,3,3633 +WALES,WREXHAM,4+,905 +WEST_MIDLANDS,BIRMINGHAM,1,22680 +WEST_MIDLANDS,BIRMINGHAM,2,34684 +WEST_MIDLANDS,BIRMINGHAM,3,39709 +WEST_MIDLANDS,BIRMINGHAM,4+,10752 +WEST_MIDLANDS,BLACK_COUNTRY,1,12318 +WEST_MIDLANDS,BLACK_COUNTRY,2,27150 +WEST_MIDLANDS,BLACK_COUNTRY,3,32298 +WEST_MIDLANDS,BLACK_COUNTRY,4+,5565 +WEST_MIDLANDS,BRECON_AND_RADNOR,1,11 +WEST_MIDLANDS,BRECON_AND_RADNOR,2,38 +WEST_MIDLANDS,BRECON_AND_RADNOR,3,37 +WEST_MIDLANDS,BRECON_AND_RADNOR,4+,27 +WEST_MIDLANDS,CHELTENHAM,1,100 +WEST_MIDLANDS,CHELTENHAM,2,312 +WEST_MIDLANDS,CHELTENHAM,3,272 +WEST_MIDLANDS,CHELTENHAM,4+,113 +WEST_MIDLANDS,CHERWELL_VALLEY,1,31 +WEST_MIDLANDS,CHERWELL_VALLEY,2,118 +WEST_MIDLANDS,CHERWELL_VALLEY,3,94 +WEST_MIDLANDS,CHERWELL_VALLEY,4+,47 +WEST_MIDLANDS,COVENTRY,1,5822 +WEST_MIDLANDS,COVENTRY,2,16383 +WEST_MIDLANDS,COVENTRY,3,17783 +WEST_MIDLANDS,COVENTRY,4+,5144 +WEST_MIDLANDS,EASTERN_STAFFORDSHIRE,1,1338 +WEST_MIDLANDS,EASTERN_STAFFORDSHIRE,2,4054 +WEST_MIDLANDS,EASTERN_STAFFORDSHIRE,3,3551 +WEST_MIDLANDS,EASTERN_STAFFORDSHIRE,4+,912 +WEST_MIDLANDS,HEREFORDSHIRE,1,2931 +WEST_MIDLANDS,HEREFORDSHIRE,2,5518 +WEST_MIDLANDS,HEREFORDSHIRE,3,4831 +WEST_MIDLANDS,HEREFORDSHIRE,4+,1735 +WEST_MIDLANDS,MID_STAFFS,1,4290 +WEST_MIDLANDS,MID_STAFFS,2,10976 +WEST_MIDLANDS,MID_STAFFS,3,11548 +WEST_MIDLANDS,MID_STAFFS,4+,2538 +WEST_MIDLANDS,PEAKS_DALES,1,4 +WEST_MIDLANDS,PEAKS_DALES,2,64 +WEST_MIDLANDS,PEAKS_DALES,3,56 +WEST_MIDLANDS,PEAKS_DALES,4+,19 +WEST_MIDLANDS,RUGBY_EAST,1,1368 +WEST_MIDLANDS,RUGBY_EAST,2,3087 +WEST_MIDLANDS,RUGBY_EAST,3,2511 +WEST_MIDLANDS,RUGBY_EAST,4+,876 +WEST_MIDLANDS,SHROPSHIRE,1,5388 +WEST_MIDLANDS,SHROPSHIRE,2,14043 +WEST_MIDLANDS,SHROPSHIRE,3,16077 +WEST_MIDLANDS,SHROPSHIRE,4+,4798 +WEST_MIDLANDS,SOLIHULL,1,1240 +WEST_MIDLANDS,SOLIHULL,2,3784 +WEST_MIDLANDS,SOLIHULL,3,3214 +WEST_MIDLANDS,SOLIHULL,4+,1114 +WEST_MIDLANDS,STAFFORDSHIRE_NORTH,1,4115 +WEST_MIDLANDS,STAFFORDSHIRE_NORTH,2,16808 +WEST_MIDLANDS,STAFFORDSHIRE_NORTH,3,12015 +WEST_MIDLANDS,STAFFORDSHIRE_NORTH,4+,2686 +WEST_MIDLANDS,WARWICKSHIRE_SOUTH,1,3230 +WEST_MIDLANDS,WARWICKSHIRE_SOUTH,2,7718 +WEST_MIDLANDS,WARWICKSHIRE_SOUTH,3,4760 +WEST_MIDLANDS,WARWICKSHIRE_SOUTH,4+,2572 +WEST_MIDLANDS,WORCESTER_NORTH,1,3512 +WEST_MIDLANDS,WORCESTER_NORTH,2,6576 +WEST_MIDLANDS,WORCESTER_NORTH,3,6692 +WEST_MIDLANDS,WORCESTER_NORTH,4+,1470 +WEST_MIDLANDS,WORCESTER_SOUTH,1,4179 +WEST_MIDLANDS,WORCESTER_SOUTH,2,8044 +WEST_MIDLANDS,WORCESTER_SOUTH,3,5898 +WEST_MIDLANDS,WORCESTER_SOUTH,4+,2200 +YORKSHIRE,BARNSLEY,1,1719 +YORKSHIRE,BARNSLEY,2,8351 +YORKSHIRE,BARNSLEY,3,7314 +YORKSHIRE,BARNSLEY,4+,1186 +YORKSHIRE,BRADFORD_SOUTH_DALES,1,9183 +YORKSHIRE,BRADFORD_SOUTH_DALES,2,19213 +YORKSHIRE,BRADFORD_SOUTH_DALES,3,16770 +YORKSHIRE,BRADFORD_SOUTH_DALES,4+,5780 +YORKSHIRE,DARLINGTON,1,30 +YORKSHIRE,DARLINGTON,2,124 +YORKSHIRE,DARLINGTON,3,165 +YORKSHIRE,DARLINGTON,4+,69 +YORKSHIRE,DONCASTER,1,3600 +YORKSHIRE,DONCASTER,2,11032 +YORKSHIRE,DONCASTER,3,13397 +YORKSHIRE,DONCASTER,4+,2579 +YORKSHIRE,GRIMSBY,1,1781 +YORKSHIRE,GRIMSBY,2,4675 +YORKSHIRE,GRIMSBY,3,8531 +YORKSHIRE,GRIMSBY,4+,1019 +YORKSHIRE,HALIFAX,1,3065 +YORKSHIRE,HALIFAX,2,8884 +YORKSHIRE,HALIFAX,3,5210 +YORKSHIRE,HALIFAX,4+,1281 +YORKSHIRE,HARROGATE,1,2575 +YORKSHIRE,HARROGATE,2,5125 +YORKSHIRE,HARROGATE,3,3696 +YORKSHIRE,HARROGATE,4+,1516 +YORKSHIRE,HULL_EAST_RIDING,1,7563 +YORKSHIRE,HULL_EAST_RIDING,2,20021 +YORKSHIRE,HULL_EAST_RIDING,3,15550 +YORKSHIRE,HULL_EAST_RIDING,4+,4392 +YORKSHIRE,KIRKLEES,1,6427 +YORKSHIRE,KIRKLEES,2,14498 +YORKSHIRE,KIRKLEES,3,10182 +YORKSHIRE,KIRKLEES,4+,2840 +YORKSHIRE,LANCASTER,1,65 +YORKSHIRE,LANCASTER,2,175 +YORKSHIRE,LANCASTER,3,180 +YORKSHIRE,LANCASTER,4+,49 +YORKSHIRE,LEEDS,1,17077 +YORKSHIRE,LEEDS,2,30655 +YORKSHIRE,LEEDS,3,18484 +YORKSHIRE,LEEDS,4+,10626 +YORKSHIRE,RICHMOND_HAMBLETON,1,1030 +YORKSHIRE,RICHMOND_HAMBLETON,2,3523 +YORKSHIRE,RICHMOND_HAMBLETON,3,3793 +YORKSHIRE,RICHMOND_HAMBLETON,4+,1355 +YORKSHIRE,ROTHERHAM,1,1844 +YORKSHIRE,ROTHERHAM,2,6221 +YORKSHIRE,ROTHERHAM,3,7277 +YORKSHIRE,ROTHERHAM,4+,999 +YORKSHIRE,SCARBOROUGH,1,3062 +YORKSHIRE,SCARBOROUGH,2,5365 +YORKSHIRE,SCARBOROUGH,3,3776 +YORKSHIRE,SCARBOROUGH,4+,1347 +YORKSHIRE,SCUNTHORPE,1,1278 +YORKSHIRE,SCUNTHORPE,2,3523 +YORKSHIRE,SCUNTHORPE,3,5819 +YORKSHIRE,SCUNTHORPE,4+,871 +YORKSHIRE,SHEFFIELD,1,9218 +YORKSHIRE,SHEFFIELD,2,15124 +YORKSHIRE,SHEFFIELD,3,14361 +YORKSHIRE,SHEFFIELD,4+,5487 +YORKSHIRE,TEESSIDE,1,69 +YORKSHIRE,TEESSIDE,2,306 +YORKSHIRE,TEESSIDE,3,285 +YORKSHIRE,TEESSIDE,4+,116 +YORKSHIRE,WAKEFIELD,1,2901 +YORKSHIRE,WAKEFIELD,2,10410 +YORKSHIRE,WAKEFIELD,3,7924 +YORKSHIRE,WAKEFIELD,4+,1738 +YORKSHIRE,YORK,1,4416 +YORKSHIRE,YORK,2,11288 +YORKSHIRE,YORK,3,7161 +YORKSHIRE,YORK,4+,4076 diff --git a/policyengine_uk_data/storage/enhanced_cps_2025.h5 b/policyengine_uk_data/storage/enhanced_cps_2025.h5 index 77bf31c56..633cff57f 100644 Binary files a/policyengine_uk_data/storage/enhanced_cps_2025.h5 and b/policyengine_uk_data/storage/enhanced_cps_2025.h5 differ diff --git a/policyengine_uk_data/storage/hmrc_pension_relief_tables_6_1_6_2_2024_25.csv b/policyengine_uk_data/storage/hmrc_pension_relief_tables_6_1_6_2_2024_25.csv new file mode 100644 index 000000000..48b149176 --- /dev/null +++ b/policyengine_uk_data/storage/hmrc_pension_relief_tables_6_1_6_2_2024_25.csv @@ -0,0 +1,83 @@ +"tax_year","income_tax_nics","contribution_type","nics_relief_class","sector_scheme","scheme_type","tax_rate","value_of_relief" +"2024 to 2025","Income Tax","Individual contributions to net pay arrangements","Not applicable","Total","Total","Total","7000" +"2024 to 2025","Income Tax","Individual contributions to relief at source schemes","Not applicable","Total","Total","Total","5900" +"2024 to 2025","Income Tax","Salary sacrificed contributions","Not applicable","Total","Total","Total","8800" +"2024 to 2025","Income Tax","Employer contributions to net pay arrangements plus deficit reduction contributions","Not applicable","Total","Total","Total","24800" +"2024 to 2025","Income Tax","Employer contributions to relief at source schemes","Not applicable","Total","Total","Total","9200" +"2024 to 2025","Income Tax","Individual contributions to net pay arrangements","Not applicable","Public sector occupational scheme","Total","Total","4500" +"2024 to 2025","Income Tax","Individual contributions to relief at source schemes","Not applicable","Public sector occupational scheme","Total","Total","200" +"2024 to 2025","Income Tax","Salary sacrificed contributions","Not applicable","Public sector occupational scheme","Total","Total","600" +"2024 to 2025","Income Tax","Employer contributions to net pay arrangements plus deficit reduction contributions","Not applicable","Public sector occupational scheme","Total","Total","14000" +"2024 to 2025","Income Tax","Employer contributions to relief at source schemes","Not applicable","Public sector occupational scheme","Total","Total","400" +"2024 to 2025","Income Tax","Individual contributions to net pay arrangements","Not applicable","Personal or private sector occupational scheme","Total","Total","2500" +"2024 to 2025","Income Tax","Individual contributions to relief at source schemes","Not applicable","Personal or private sector occupational scheme","Total","Total","5700" +"2024 to 2025","Income Tax","Salary sacrificed contributions","Not applicable","Personal or private sector occupational scheme","Total","Total","8300" +"2024 to 2025","Income Tax","Employer contributions to net pay arrangements plus deficit reduction contributions","Not applicable","Personal or private sector occupational scheme","Total","Total","10800" +"2024 to 2025","Income Tax","Employer contributions to relief at source schemes","Not applicable","Personal or private sector occupational scheme","Total","Total","8800" +"2024 to 2025","Income Tax","Individual contributions to net pay arrangements","Not applicable","Total","Defined benefit","Total","5200" +"2024 to 2025","Income Tax","Individual contributions to relief at source schemes","Not applicable","Total","Defined benefit","Total","0" +"2024 to 2025","Income Tax","Salary sacrificed contributions","Not applicable","Total","Defined benefit","Total","1500" +"2024 to 2025","Income Tax","Employer contributions to net pay arrangements plus deficit reduction contributions","Not applicable","Total","Defined benefit","Total","16600" +"2024 to 2025","Income Tax","Employer contributions to relief at source schemes","Not applicable","Total","Defined benefit","Total","0" +"2024 to 2025","Income Tax","Individual contributions to net pay arrangements","Not applicable","Total","Defined contribution","Total","1800" +"2024 to 2025","Income Tax","Individual contributions to relief at source schemes","Not applicable","Total","Defined contribution","Total","5900" +"2024 to 2025","Income Tax","Salary sacrificed contributions","Not applicable","Total","Defined contribution","Total","7400" +"2024 to 2025","Income Tax","Employer contributions to net pay arrangements plus deficit reduction contributions","Not applicable","Total","Defined contribution","Total","8200" +"2024 to 2025","Income Tax","Employer contributions to relief at source schemes","Not applicable","Total","Defined contribution","Total","9200" +"2024 to 2025","Income Tax","Individual contributions to net pay arrangements","Not applicable","Total","Total","Basic Rate","2400" +"2024 to 2025","Income Tax","Individual contributions to relief at source schemes","Not applicable","Total","Total","Basic Rate","3200" +"2024 to 2025","Income Tax","Salary sacrificed contributions","Not applicable","Total","Total","Basic Rate","1600" +"2024 to 2025","Income Tax","Employer contributions to net pay arrangements plus deficit reduction contributions","Not applicable","Total","Total","Basic Rate","6700" +"2024 to 2025","Income Tax","Employer contributions to relief at source schemes","Not applicable","Total","Total","Basic Rate","2200" +"2024 to 2025","Income Tax","Individual contributions to net pay arrangements","Not applicable","Total","Total","Higher Rate","3800" +"2024 to 2025","Income Tax","Individual contributions to relief at source schemes","Not applicable","Total","Total","Higher Rate","1600" +"2024 to 2025","Income Tax","Salary sacrificed contributions","Not applicable","Total","Total","Higher Rate","5500" +"2024 to 2025","Income Tax","Employer contributions to net pay arrangements plus deficit reduction contributions","Not applicable","Total","Total","Higher Rate","15600" +"2024 to 2025","Income Tax","Employer contributions to relief at source schemes","Not applicable","Total","Total","Higher Rate","5200" +"2024 to 2025","Income Tax","Individual contributions to net pay arrangements","Not applicable","Total","Total","Additional Rate","900" +"2024 to 2025","Income Tax","Individual contributions to relief at source schemes","Not applicable","Total","Total","Additional Rate","1100" +"2024 to 2025","Income Tax","Salary sacrificed contributions","Not applicable","Total","Total","Additional Rate","1800" +"2024 to 2025","Income Tax","Employer contributions to net pay arrangements plus deficit reduction contributions","Not applicable","Total","Total","Additional Rate","2500" +"2024 to 2025","Income Tax","Employer contributions to relief at source schemes","Not applicable","Total","Total","Additional Rate","1700" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Primary (employee)","Total","Total","Total","3600" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Primary (employee)","Total","Total","Total","1200" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Primary (employee)","Total","Total","Total","1000" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Secondary (employer)","Total","Total","Total","10600" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Secondary (employer)","Total","Total","Total","3800" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Secondary (employer)","Total","Total","Total","3400" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Primary (employee)","Public sector occupational scheme","Total","Total","2300" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Primary (employee)","Public sector occupational scheme","Total","Total","900" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Primary (employee)","Public sector occupational scheme","Total","Total","100" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Secondary (employer)","Public sector occupational scheme","Total","Total","6300" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Secondary (employer)","Public sector occupational scheme","Total","Total","200" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Secondary (employer)","Public sector occupational scheme","Total","Total","200" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Primary (employee)","Personal or private sector occupational scheme","Total","Total","1300" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Primary (employee)","Personal or private sector occupational scheme","Total","Total","300" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Primary (employee)","Personal or private sector occupational scheme","Total","Total","900" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Secondary (employer)","Personal or private sector occupational scheme","Total","Total","4300" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Secondary (employer)","Personal or private sector occupational scheme","Total","Total","3600" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Secondary (employer)","Personal or private sector occupational scheme","Total","Total","3100" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Primary (employee)","Total","Defined benefit","Total","2500" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Primary (employee)","Total","Defined benefit","Total","0" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Primary (employee)","Total","Defined benefit","Total","200" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Secondary (employer)","Total","Defined benefit","Total","7200" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Secondary (employer)","Total","Defined benefit","Total","0" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Secondary (employer)","Total","Defined benefit","Total","600" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Primary (employee)","Total","Defined contribution","Total","1100" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Primary (employee)","Total","Defined contribution","Total","1200" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Primary (employee)","Total","Defined contribution","Total","800" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Secondary (employer)","Total","Defined contribution","Total","3300" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Secondary (employer)","Total","Defined contribution","Total","3800" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Secondary (employer)","Total","Defined contribution","Total","2800" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Primary (employee)","Total","Total","Main Rate","2800" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Primary (employee)","Total","Total","Main Rate","900" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Primary (employee)","Total","Total","Main Rate","600" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Secondary (employer)","Total","Total","Main Rate","10600" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Secondary (employer)","Total","Total","Main Rate","3800" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Secondary (employer)","Total","Total","Main Rate","3400" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Primary (employee)","Total","Total","Additional Rate","800" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Primary (employee)","Total","Total","Additional Rate","300" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Primary (employee)","Total","Total","Additional Rate","300" +"2024 to 2025","NICs","Employer contributions to net pay arrangements and on employer deficit reduction contributions","Class 1 Secondary (employer)","Total","Total","Additional Rate","[z]" +"2024 to 2025","NICs","Employer contributions to relief at source schemes","Class 1 Secondary (employer)","Total","Total","Additional Rate","[z]" +"2024 to 2025","NICs","Salary sacrificed contributions","Class 1 Secondary (employer)","Total","Total","Additional Rate","[z]" diff --git a/policyengine_uk_data/storage/lha_list_of_rents.csv.gz b/policyengine_uk_data/storage/lha_list_of_rents.csv.gz deleted file mode 100644 index af02bfda5..000000000 Binary files a/policyengine_uk_data/storage/lha_list_of_rents.csv.gz and /dev/null differ diff --git a/policyengine_uk_data/storage/uprating_factors.csv b/policyengine_uk_data/storage/uprating_factors.csv index dacd591bd..149ef19ff 100644 --- a/policyengine_uk_data/storage/uprating_factors.csv +++ b/policyengine_uk_data/storage/uprating_factors.csv @@ -54,6 +54,7 @@ other_investment_income,1.0,1.0,1.092,1.147,1.19,1.223,1.258,1.297,1.34,1.384,1. other_residential_property_value,1.0,1.0,1.092,1.147,1.19,1.223,1.258,1.297,1.34,1.384,1.384,1.384,1.384,1.384,1.384 owned_land,1.0,1.0,1.092,1.147,1.19,1.223,1.258,1.297,1.34,1.384,1.384,1.384,1.384,1.384,1.384 pension_credit_reported,1.0,1.04,1.144,1.209,1.237,1.277,1.301,1.327,1.353,1.38,1.38,1.38,1.38,1.38,1.38 +pension_credit_reported_capital,1.0,1.0,1.102,1.161,1.204,1.256,1.293,1.335,1.376,1.417,1.462,1.508,1.555,1.604,1.655 pension_income,1.0,1.0,1.092,1.147,1.19,1.223,1.258,1.297,1.34,1.384,1.384,1.384,1.384,1.384,1.384 personal_pension_contributions,1.0,1.059,1.127,1.205,1.261,1.308,1.337,1.365,1.396,1.431,1.431,1.431,1.431,1.431,1.431 petrol_spending,1.0,1.104,1.635,1.796,1.531,1.483,1.452,1.406,1.363,1.305,1.237,1.237,1.237,1.237,1.237 diff --git a/policyengine_uk_data/storage/uprating_growth_factors.csv b/policyengine_uk_data/storage/uprating_growth_factors.csv index 122b4ed69..956f740d7 100644 --- a/policyengine_uk_data/storage/uprating_growth_factors.csv +++ b/policyengine_uk_data/storage/uprating_growth_factors.csv @@ -54,6 +54,7 @@ other_investment_income,0,0.0,0.092,0.05,0.037,0.028,0.029,0.031,0.033,0.033,0.0 other_residential_property_value,0,0.0,0.092,0.05,0.037,0.028,0.029,0.031,0.033,0.033,0.0,0.0,0.0,0.0,0.0 owned_land,0,0.0,0.092,0.05,0.037,0.028,0.029,0.031,0.033,0.033,0.0,0.0,0.0,0.0,0.0 pension_credit_reported,0,0.04,0.1,0.057,0.023,0.032,0.019,0.02,0.02,0.02,0.0,0.0,0.0,0.0,0.0 +pension_credit_reported_capital,0,0.0,0.102,0.054,0.037,0.043,0.029,0.032,0.031,0.03,0.032,0.031,0.031,0.032,0.032 pension_income,0,0.0,0.092,0.05,0.037,0.028,0.029,0.031,0.033,0.033,0.0,0.0,0.0,0.0,0.0 personal_pension_contributions,0,0.059,0.064,0.069,0.046,0.037,0.022,0.021,0.023,0.025,0.0,0.0,0.0,0.0,0.0 petrol_spending,0,0.104,0.481,0.099,-0.147,-0.031,-0.021,-0.032,-0.031,-0.043,-0.052,0.0,0.0,0.0,0.0 diff --git a/policyengine_uk_data/targets/build_loss_matrix.py b/policyengine_uk_data/targets/build_loss_matrix.py index 9552aeea2..980223540 100644 --- a/policyengine_uk_data/targets/build_loss_matrix.py +++ b/policyengine_uk_data/targets/build_loss_matrix.py @@ -52,7 +52,6 @@ compute_two_child_limit, compute_uc_by_children, compute_uc_by_family_type, - compute_uc_outside_cap, compute_uc_payment_dist, compute_uk_population, compute_vehicles, @@ -113,7 +112,7 @@ def create_target_matrix( col = _compute_column(target, ctx, year) if col is None: continue - df[target.name] = col + df[target.name] = restrict_to_countries(col, ctx.country, target.countries) target_names.append(target.name) target_values.append(val) except Exception as e: @@ -122,14 +121,30 @@ def create_target_matrix( return df, pd.Series(target_values, index=target_names) +def restrict_to_countries(column, household_country, countries): + """Zero a household column outside the countries a target covers. + + ``countries`` of None means the target covers the whole UK, and the + column is returned unchanged. + """ + if countries is None: + return column + in_scope = np.isin(np.asarray(household_country), countries) + return np.asarray(column, dtype=float) * in_scope + + def _resolve_value(target: Target, year: int) -> float | None: """Get the target value for a year, falling back to nearest year. - VOA council tax targets are population-uprated when extrapolating - from their base year (2024). + A missing year takes the value of the nearest listed year (the earlier + on a tie) if that year is earlier and at most three years away, unless + the target sets ``carry_forward=False``; otherwise there is no value. VOA council tax targets are + population-uprated when extrapolating from their base year (2024). """ if year in target.values: return target.values[year] + if not target.carry_forward: + return None available = sorted(target.values.keys()) if not available: return None @@ -386,16 +401,10 @@ def _compute_column(target: Target, ctx: _SimContext, year: int) -> np.ndarray | # Salary sacrifice NI relief if name in ( "hmrc/salary_sacrifice_employee_nics_relief", - "obr/salary_sacrifice_employee_ni_relief", "hmrc/salary_sacrifice_employer_nics_relief", - "obr/salary_sacrifice_employer_ni_relief", ): return compute_ss_ni_relief(target, ctx) - # UC outside benefit cap - if name == "obr/universal_credit_outside_cap": - return compute_uc_outside_cap(target, ctx) - # Two-child limit if "two_child_limit" in name: return compute_two_child_limit(target, ctx) diff --git a/policyengine_uk_data/targets/compute/__init__.py b/policyengine_uk_data/targets/compute/__init__.py index c7000eb41..877dd8e16 100644 --- a/policyengine_uk_data/targets/compute/__init__.py +++ b/policyengine_uk_data/targets/compute/__init__.py @@ -7,7 +7,6 @@ compute_two_child_limit, compute_uc_by_children, compute_uc_by_family_type, - compute_uc_outside_cap, compute_uc_payment_dist, ) from policyengine_uk_data.targets.compute.council_tax import ( @@ -76,7 +75,6 @@ "compute_two_child_limit", "compute_uc_by_children", "compute_uc_by_family_type", - "compute_uc_outside_cap", "compute_uc_payment_dist", "compute_uk_population", "compute_vehicles", diff --git a/policyengine_uk_data/targets/compute/benefits.py b/policyengine_uk_data/targets/compute/benefits.py index 0daca8507..b1105855c 100644 --- a/policyengine_uk_data/targets/compute/benefits.py +++ b/policyengine_uk_data/targets/compute/benefits.py @@ -82,7 +82,14 @@ def ft_hh(value): def compute_uc_payment_dist(target, ctx) -> np.ndarray: - """Compute UC payment distribution band x family type.""" + """Compute UC payment distribution band x family type. + + Stat-Xplore's monthly award is the UC due after deductions (its "Monthly + Award Amount (payment bands)" metadata), as universal_credit is. Bands + are (lower, upper] annual amounts (see + utils.uc_data.parse_monthly_award_band), so consecutive bands meet and + the open top band has an infinite upper bound. + """ name = target.name.removeprefix("dwp/uc_payment_dist/") idx = name.index("_annual_payment_") family_type = name[:idx] @@ -93,22 +100,11 @@ def compute_uc_payment_dist(target, ctx) -> np.ndarray: uc_family_type = ctx.sim.calculate("family_type", map_to="benunit").values in_band = ( - (uc_payments >= lower) & (uc_payments < upper) & (uc_family_type == family_type) + (uc_payments > lower) & (uc_payments <= upper) & (uc_family_type == family_type) ) return ctx.household_from_family(in_band) -def compute_uc_outside_cap(target, ctx) -> np.ndarray: - """Compute OBR UC outside benefit cap.""" - uc = ctx.sim.calculate("universal_credit") - uc_hh = ctx.household_from_family(uc) - cap_reduction = ctx.sim.calculate( - "benefit_cap_reduction", map_to="household" - ).values - not_capped = cap_reduction == 0 - return uc_hh * not_capped - - def compute_two_child_limit(target, ctx) -> np.ndarray | None: """Compute two-child limit targets.""" name = target.name diff --git a/policyengine_uk_data/targets/compute/income.py b/policyengine_uk_data/targets/compute/income.py index f5be3e2bb..19c49ac71 100644 --- a/policyengine_uk_data/targets/compute/income.py +++ b/policyengine_uk_data/targets/compute/income.py @@ -20,34 +20,59 @@ def compute_income_band(target, ctx) -> np.ndarray: return ctx.household_from_person(income_df[variable] * in_band) -def compute_ss_it_relief(target, ctx) -> np.ndarray: - """Compute salary sacrifice IT relief by tax band.""" - it_base = ctx.sim.calculate("income_tax") - it_cf = ctx.counterfactual_sim.calculate("income_tax", ctx.time_period) - it_relief = it_cf - it_base +def tax_by_band(income: np.ndarray, thresholds, rates) -> dict: + """Tax on ``income`` within each of HMRC's rate categories. + + HMRC's Table 6.1 groups relief by marginal rate: basic (with the Scottish + starter and intermediate rates), higher and additional. Brackets taxed + below 30% are basic, brackets at the scale's top rate are additional (the + rUK additional rate, the Scottish top rate) and the rest are higher (the + Scottish higher and advanced rates, which cover the income range of the + rUK higher rate). The categories sum to the scale's tax on ``income``. + """ + income = np.asarray(income, dtype=float) + thresholds = [t for t, r in zip(thresholds, rates) if r is not None] + rates = [r for r in rates if r is not None] + uppers = thresholds[1:] + [np.inf] + bands = {"basic": 0.0, "higher": 0.0, "additional": 0.0} + for lower, upper, rate in zip(thresholds, uppers, rates): + band = ( + "basic" if rate < 0.3 else "additional" if rate == rates[-1] else "higher" + ) + bands[band] = bands[band] + rate * np.clip(income - lower, 0, upper - lower) + return bands - adj_net_income_cf = ctx.counterfactual_sim.calculate( - "adjusted_net_income", ctx.time_period - ) - params = ctx.sim.tax_benefit_system.parameters.gov.hmrc.income_tax.rates.uk - basic_thresh = params[0].threshold(ctx.time_period) - higher_thresh = params[1].threshold(ctx.time_period) - additional_thresh = params[2].threshold(ctx.time_period) +def ss_it_relief_by_band(ctx) -> dict: + """Person-level salary sacrifice income tax relief in each rate category. + + HMRC applies income tax rates to employees' pay (ASHE), so relief is the + rise in tax on earned income when the sacrifice is paid as salary, under + the person's rUK or Scottish rates. Contributions that straddle a band + boundary are relieved partly at each rate, as in HMRC's estimates. + """ + period = ctx.time_period + rates = ctx.sim.tax_benefit_system.parameters(period).gov.hmrc.income_tax.rates + scottish = np.asarray(ctx.sim.calculate("pays_scottish_income_tax", period)) + base = np.asarray(ctx.sim.calculate("earned_taxable_income", period)) + cf = np.asarray(ctx.counterfactual_sim.calculate("earned_taxable_income", period)) + relief = {} + for scale, in_scale in ((rates.uk, ~scottish), (rates.scotland.rates, scottish)): + band_cf = tax_by_band(cf, scale.thresholds, scale.rates) + band_base = tax_by_band(base, scale.thresholds, scale.rates) + for band in band_cf: + relief[band] = relief.get(band, 0) + in_scale * ( + band_cf[band] - band_base[band] + ) + return relief - name = target.name - if "basic" in name: - mask = (adj_net_income_cf > basic_thresh) & (adj_net_income_cf <= higher_thresh) - elif "higher" in name: - mask = (adj_net_income_cf > higher_thresh) & ( - adj_net_income_cf <= additional_thresh - ) - elif "additional" in name: - mask = adj_net_income_cf > additional_thresh - else: - mask = np.ones_like(it_relief, dtype=bool) - return ctx.household_from_person(it_relief * mask) +def compute_ss_it_relief(target, ctx) -> np.ndarray: + """Compute salary sacrifice income tax relief at one rate.""" + band = target.name.removeprefix("hmrc/salary_sacrifice_it_relief_") + return ctx.household_from_person( + ss_it_relief_by_band(ctx)[band.removesuffix("_rate")] + ) def compute_ss_contributions(target, ctx) -> np.ndarray: diff --git a/policyengine_uk_data/targets/schema.py b/policyengine_uk_data/targets/schema.py index 97b814678..ebdaba9d0 100644 --- a/policyengine_uk_data/targets/schema.py +++ b/policyengine_uk_data/targets/schema.py @@ -19,6 +19,11 @@ class Unit(str, Enum): RATE = "rate" +# DWP statistics cover Great Britain: benefits for Northern Ireland residents +# are the Northern Ireland Executive's responsibility. +GREAT_BRITAIN = ("ENGLAND", "SCOTLAND", "WALES") + + class Target(BaseModel): """A single calibration target from an official statistical source. @@ -41,6 +46,15 @@ class Target(BaseModel): is_count: bool = False reference_url: str | None = None forecast_vintage: str | None = None + # Countries a national source covers when that is less than the UK, as + # values of the model's `country` variable. The loss matrix column only + # counts households in these countries. None means the whole UK. + countries: tuple[str, ...] | None = None + # Whether the loss matrix may fill a year the target does not list with + # the nearest listed year's value (the earlier on a tie), used only when + # that year is earlier and at most three years away (see _resolve_value). + # False for a figure that exists only in the years it lists. + carry_forward: bool = True # For targets needing custom simulation logic (UC splits, # counterfactuals). Excluded from serialisation. diff --git a/policyengine_uk_data/targets/sources.yaml b/policyengine_uk_data/targets/sources.yaml index 5d1cd89ba..973e39567 100644 --- a/policyengine_uk_data/targets/sources.yaml +++ b/policyengine_uk_data/targets/sources.yaml @@ -11,7 +11,9 @@ hmrc: spi_geography: "https://assets.publishing.service.gov.uk/media/69f1f17cc42061e837e3ac3b/Collated_Tables_3_12_to_3_15a_2324.ods" income_tax_liabilities: "https://www.gov.uk/government/statistics/income-tax-liabilities-statistics-tax-year-2022-to-2023-to-tax-year-2025-to-2026" capital_gains_statistics: "https://www.gov.uk/government/statistics/capital-gains-tax-statistics" - salary_sacrifice_table_6: "https://assets.publishing.service.gov.uk/media/687a294e312ee8a5f0806b6d/Tables_6_1_and_6_2.csv" + # Private pension statistics, July 2026 (tax year 2024-25). Refresh with + # storage/hmrc_pension_relief_tables_6_1_6_2_*.csv (hmrc_salary_sacrifice.py). + salary_sacrifice_table_6: "https://assets.publishing.service.gov.uk/media/6a673e0e5e87122783093901/Tables_6_1_and_6_2.csv" dwp: stat_xplore_api: "https://stat-xplore.dwp.gov.uk/webapi/rest/v1" diff --git a/policyengine_uk_data/targets/sources/dwp_housing_benefit.py b/policyengine_uk_data/targets/sources/dwp_housing_benefit.py new file mode 100644 index 000000000..d822251d6 --- /dev/null +++ b/policyengine_uk_data/targets/sources/dwp_housing_benefit.py @@ -0,0 +1,294 @@ +"""DWP Housing Benefit targets by age group. + +Housing Benefit spending and caseload over and under Pension Credit +qualifying age, from DWP's benefit expenditure and caseload tables for the +Spring Forecast 2026 (Housing benefits sheet, nominal £ million and +thousands of claims, 2022-23 to 2024-25 outturn, forecast after). Financial +year 2025-26 is stored as 2025. + +Coverage is Great Britain: Housing Benefit for Northern Ireland residents is +paid under Northern Ireland legislation and sits outside DWP's figures, so +the model columns count GB households only. + +These targets replace OBR EFO table 4.9 "Housing benefit (not on JSA)". +That line is DWP-funded spending only, while DWP's tables count all +Housing Benefit paid, including the part local authorities fund (£0.79bn +in 2025-26). The age split sums to that full amount, so targeting both +would ask for two different GB totals. + +DWP splits claims by benefit rules rather than by age alone (Notes, note +5): from 2024-25 the two lines equal its Pension Credit plus State Pension +benefit groups and its ESA plus other working-age groups. Under regulation 5 of +both Housing Benefit Regulations 2006 (SI 2006/213 and 2006/214), the +pension-age rules apply when the claimant or partner has reached the +qualifying age for Pension Credit, unless either is on Universal Credit, +Income Support, income-based JSA or income-related ESA. A benefit unit is +therefore over Pension Credit qualifying age here when its claimant or +partner has reached State Pension age and it gets none of those benefits. Mixed-age +couples who kept pension-age Housing Benefit after May 2019 fall in the +older group; DWP does not publish its rule for them, and this assumes they +sit in its Pension Credit and State Pension groups. + +Working-age Housing Benefit is calibrated net of supported and temporary +accommodation. policyengine-uk has no rules or input for specified +(supported) or temporary accommodation (``housing_benefit_eligible``: such +claims "are not modelled"; policyengine-uk#1911). It pays working-age +Housing Benefit only as a continuing award to a family that reports it and +does not claim Universal Credit, computed under the general-needs rules. The +FRS does not identify those accommodation types, so a respondent in them who +reports Housing Benefit is modelled the same way; treating the model's +working-age Housing Benefit as general needs is an approximation. DWP's +Spring 2026 forecast tables split accommodation type only across all ages +(Housing Benefit by Accomodation Type). The calibrated working-age figure is +therefore DWP's working-age line less all of its supported and temporary +accommodation: £584.8m and 107k claims in 2025-26, against the full line's +£5,778.1m and 460k. Part of that accommodation is pension-age, so the figure +is a lower bound on working-age general-needs Housing Benefit. It exists +only for 2024-25 and 2025-26, and the targets do not carry it forward +(``carry_forward=False``): from 2026-27, DWP's all-age supported and +temporary accommodation exceeds its whole working-age line, so these lines +give no positive lower bound. That leaves DWP's supported and temporary +accommodation Housing Benefit, £5.2bn across all ages in 2025-26, unmodelled +as such: a known limitation. + +Seeded test calibrations on 2026-10-04 tried the full working-age line too. +With only the pension-age figures calibrated, the model pays about 17,000 +working-age claims in 2025-26. Calibrated to the full line, it came about +12% below DWP's spending by loading it onto about three effective records. +With household weights capped at 20-40 times their prior, it reached only +54-60% of that spending, and income-related ESA claimants rose to 1.6-2.0 +times DWP's count. Against the net figure, no other national target crosses +the 10% line compared with calibrating the pension-age figures only. The +full working-age lines stay here (group "under") for tests and diagnostics. + +Source: https://www.gov.uk/government/publications/benefit-expenditure-and-caseload-tables-2026 +""" + +import numpy as np + +from policyengine_uk_data.targets.schema import GREAT_BRITAIN, Target, Unit +from policyengine_uk_data.utils.benefit_units import claimant_or_partner_variable + +_REFERENCE_URL = ( + "https://www.gov.uk/government/publications/" + "benefit-expenditure-and-caseload-tables-2026" +) +_VINTAGE = "spring_2026" + +# Benefits that keep a claim under the working-age Housing Benefit rules +# when the claimant or partner has reached Pension Credit qualifying age. +_WORKING_AGE_BENEFITS = ( + "universal_credit", + "income_support", + "jsa_income", + "esa_income", +) + +# Housing benefits sheet rows "Housing Benefit over/under Pension Credit +# qualifying age": expenditure in £ million (nominal) and caseload in +# thousands (annual average, rounded to the nearest thousand by DWP). +_SPENDING_GBP_M = { + "over": { + 2022: 5_890.1, + 2023: 6_248.1, + 2024: 6_851.0, + 2025: 7_114.7, + 2026: 7_268.9, + 2027: 7_340.4, + 2028: 7_464.4, + 2029: 7_747.9, + 2030: 7_941.3, + }, + "under": { + 2022: 9_689.1, + 2023: 9_524.5, + 2024: 8_603.5, + 2025: 5_778.1, + 2026: 5_205.3, + 2027: 5_493.8, + 2028: 5_788.5, + 2029: 6_151.5, + 2030: 6_405.4, + }, +} +_CASELOAD_THOUSANDS = { + "over": { + 2022: 1_121, + 2023: 1_108, + 2024: 1_104, + 2025: 1_109, + 2026: 1_082, + 2027: 1_057, + 2028: 1_040, + 2029: 1_036, + 2030: 1_042, + }, + "under": { + 2022: 1_388, + 2023: 1_243, + 2024: 968, + 2025: 460, + 2026: 328, + 2027: 339, + 2028: 349, + 2029: 361, + 2030: 372, + }, +} + + +# Housing benefits sheet, "Housing Benefit by Accomodation Type", rows "of +# which Supported Accomodation" and "of which Temporary Accomodation": all +# ages, same units, first published for 2024-25. +_SUPPORTED_GBP_M = { + 2024: 3_262.8, + 2025: 3_631.4, + 2026: 3_936.0, + 2027: 4_164.1, + 2028: 4_399.2, + 2029: 4_687.1, + 2030: 4_892.2, +} +_TEMPORARY_GBP_M = { + 2024: 1_431.1, + 2025: 1_561.9, + 2026: 1_643.7, + 2027: 1_732.1, + 2028: 1_819.1, + 2029: 1_929.8, + 2030: 2_006.6, +} +_SUPPORTED_THOUSANDS = { + 2024: 235, + 2025: 242, + 2026: 246, + 2027: 251, + 2028: 256, + 2029: 262, + 2030: 267, +} +_TEMPORARY_THOUSANDS = { + 2024: 104, + 2025: 111, + 2026: 117, + 2027: 123, + 2028: 129, + 2029: 136, + 2030: 143, +} + + +def net_of_supported_and_temporary( + working_age: dict, supported: dict, temporary: dict +) -> dict: + """Working-age figures less all-age supported and temporary + accommodation, for the years with all three where the result is + positive.""" + net = {} + for year in sorted(working_age.keys() & supported.keys() & temporary.keys()): + value = round(working_age[year] - supported[year] - temporary[year], 1) + if value > 0: + net[year] = value + return net + + +_SPENDING_GBP_M["under_general_needs"] = net_of_supported_and_temporary( + _SPENDING_GBP_M["under"], _SUPPORTED_GBP_M, _TEMPORARY_GBP_M +) +_CASELOAD_THOUSANDS["under_general_needs"] = net_of_supported_and_temporary( + _CASELOAD_THOUSANDS["under"], _SUPPORTED_THOUSANDS, _TEMPORARY_THOUSANDS +) + +_NAMES = { + "over": "dwp/housing_benefit/over_pension_credit_age", + "under": "dwp/housing_benefit/under_pension_credit_age", + "under_general_needs": "dwp/housing_benefit/under_pension_credit_age_general_needs", +} + + +def _over_pension_credit_age(ctx) -> np.ndarray: + """Benefit units assessed under the pension-age Housing Benefit rules.""" + claimant = np.asarray( + ctx.pe_person( + claimant_or_partner_variable(ctx.sim.tax_benefit_system.variables) + ), + dtype=bool, + ) + over = claimant & np.asarray(ctx.pe_person("is_SP_age"), dtype=bool) + any_over = ( + np.asarray(ctx.sim.map_result(over.astype(float), "person", "benunit")) > 0 + ) + on_working_age_benefit = np.zeros_like(any_over) + for benefit in _WORKING_AGE_BENEFITS: + on_working_age_benefit |= np.asarray(ctx.sim.calculate(benefit).values) > 0 + return any_over & ~on_working_age_benefit + + +# Age groups the calibration targets; see the module docstring. +_CALIBRATED_AGE_GROUPS = ("over", "under_general_needs") + + +def _make_compute(age_group: str, count: bool): + def compute(ctx, target: Target, year: int) -> np.ndarray: + housing_benefit = np.asarray( + ctx.sim.calculate("housing_benefit").values, dtype=float + ) + in_group = _over_pension_credit_age(ctx) + if age_group != "over": + # The model has no supported or temporary accommodation rules, + # so both working-age groups share one column (see the module + # docstring). + in_group = ~in_group + value = (housing_benefit > 0) if count else housing_benefit + return np.asarray(ctx.household_from_family(value * in_group), dtype=float) + + return compute + + +def get_targets() -> list[Target]: + return build_targets(_CALIBRATED_AGE_GROUPS) + + +def build_targets( + age_groups=("over", "under", "under_general_needs"), +) -> list[Target]: + """Housing Benefit spending and claims targets for these age groups.""" + targets = [] + for age_group in age_groups: + name = _NAMES[age_group] + targets.append( + Target( + name=name, + variable="housing_benefit", + source="dwp", + unit=Unit.GBP, + values={ + year: value * 1e6 + for year, value in _SPENDING_GBP_M[age_group].items() + }, + reference_url=_REFERENCE_URL, + forecast_vintage=_VINTAGE, + countries=GREAT_BRITAIN, + carry_forward=age_group != "under_general_needs", + custom_compute=_make_compute(age_group, count=False), + ) + ) + targets.append( + Target( + name=f"{name}_claims", + variable="housing_benefit", + source="dwp", + unit=Unit.COUNT, + values={ + year: value * 1e3 + for year, value in _CASELOAD_THOUSANDS[age_group].items() + }, + is_count=True, + reference_url=_REFERENCE_URL, + forecast_vintage=_VINTAGE, + countries=GREAT_BRITAIN, + carry_forward=age_group != "under_general_needs", + custom_compute=_make_compute(age_group, count=True), + ) + ) + return targets diff --git a/policyengine_uk_data/targets/sources/dwp_pension_credit.py b/policyengine_uk_data/targets/sources/dwp_pension_credit.py new file mode 100644 index 000000000..776875f33 --- /dev/null +++ b/policyengine_uk_data/targets/sources/dwp_pension_credit.py @@ -0,0 +1,90 @@ +"""DWP Pension Credit targets: spending and claims in Great Britain. + +From DWP's benefit expenditure and caseload tables for the Spring Forecast +2026 (Pension Credit sheet: nominal £ million and thousands of claims, +2022-23 to 2024-25 outturn, forecast after). Financial year 2025-26 is +stored as 2025. + +DWP's figures cover Great Britain; Pension Credit in Northern Ireland is +paid by the Department for Communities, so the model columns count GB +households only. These targets replace OBR EFO table 4.9 "Pension credit", +the same GB spending series (2025-26: £6,146.4m against DWP's £6,144.5m), +which the model compared with UK-wide Pension Credit and which carries no +caseload. + +Source: https://www.gov.uk/government/publications/benefit-expenditure-and-caseload-tables-2026 +""" + +import numpy as np + +from policyengine_uk_data.targets.schema import GREAT_BRITAIN, Target, Unit + +_REFERENCE_URL = ( + "https://www.gov.uk/government/publications/" + "benefit-expenditure-and-caseload-tables-2026" +) +_VINTAGE = "spring_2026" + +# "Total Pension Credit", £ million nominal. +_SPENDING_GBP_M = { + 2022: 4_935.31, + 2023: 5_467.14, + 2024: 6_007.70, + 2025: 6_144.47, + 2026: 6_054.90, + 2027: 5_911.31, + 2028: 5_783.32, + 2029: 5_832.22, + 2030: 5_821.77, +} +# "Pension Credit" caseload, thousands (annual average, rounded by DWP). +_CASELOAD_THOUSANDS = { + 2022: 1_374, + 2023: 1_370, + 2024: 1_376, + 2025: 1_382, + 2026: 1_312, + 2027: 1_253, + 2028: 1_188, + 2029: 1_142, + 2030: 1_133, +} + + +def _make_compute(count: bool): + def compute(ctx, target: Target, year: int) -> np.ndarray: + pension_credit = np.asarray( + ctx.sim.calculate("pension_credit").values, dtype=float + ) + value = (pension_credit > 0) if count else pension_credit + return np.asarray(ctx.household_from_family(value), dtype=float) + + return compute + + +def get_targets() -> list[Target]: + return [ + Target( + name="dwp/pension_credit", + variable="pension_credit", + source="dwp", + unit=Unit.GBP, + values={year: v * 1e6 for year, v in _SPENDING_GBP_M.items()}, + reference_url=_REFERENCE_URL, + forecast_vintage=_VINTAGE, + countries=GREAT_BRITAIN, + custom_compute=_make_compute(count=False), + ), + Target( + name="dwp/pension_credit_claims", + variable="pension_credit", + source="dwp", + unit=Unit.COUNT, + values={year: v * 1e3 for year, v in _CASELOAD_THOUSANDS.items()}, + is_count=True, + reference_url=_REFERENCE_URL, + forecast_vintage=_VINTAGE, + countries=GREAT_BRITAIN, + custom_compute=_make_compute(count=True), + ), + ] diff --git a/policyengine_uk_data/targets/sources/hmrc_salary_sacrifice.py b/policyengine_uk_data/targets/sources/hmrc_salary_sacrifice.py index f9865e6f7..fd21f15c6 100644 --- a/policyengine_uk_data/targets/sources/hmrc_salary_sacrifice.py +++ b/policyengine_uk_data/targets/sources/hmrc_salary_sacrifice.py @@ -1,9 +1,23 @@ """HMRC salary sacrifice income tax and NICs relief targets. -Downloads Table 6.2 CSV from HMRC to get salary sacrifice IT relief -by tax rate band and NICs relief (employee + employer). - -Source: https://assets.publishing.service.gov.uk/media/687a294e312ee8a5f0806b6d/Tables_6_1_and_6_2.csv +Reads HMRC private pension statistics Tables 6.1 and 6.2 (tidy CSV): income +tax relief on salary-sacrificed pension contributions by marginal rate, and +the Class 1 primary (employee) and secondary (employer) NICs relief on them. + +GOV.UK withdraws a release's assets when HMRC publishes the next one: the +July 2025 CSV has returned 410 Gone since the July 2026 release, and the +broad ``except`` this module used to have turned that into calibration +builds that silently lacked these targets. A failed download now falls back +to the copy committed in storage (the same release as ``sources.yaml``), and +a table for another tax year, or without the expected rows, raises instead +of returning fewer targets. + +Targets grow 3% a year from the table's year. The NICs relief targets also +follow the Class 1 rates in PolicyEngine's parameters, so the April 2025 +rise in the employer rate from 13.8% to 15% raises the employer target in +the years the simulation charges 15%. + +Source: https://www.gov.uk/government/statistics/personal-and-stakeholder-pensions-statistics """ import io @@ -15,110 +29,193 @@ from policyengine_uk_data.targets.schema import Target, Unit from policyengine_uk_data.targets.sources._common import ( HEADERS, + STORAGE, load_config, to_float, ) logger = logging.getLogger(__name__) +# Tax year of the Tables 6.1 and 6.2 release in sources.yaml (July 2026, +# 2024-25) and of its committed copy. Refresh the URL, the copy and this +# together: a table for any other year raises. +_TAX_YEAR = 2024 +FALLBACK_CSV = ( + STORAGE + / f"hmrc_pension_relief_tables_6_1_6_2_{_TAX_YEAR}_{(_TAX_YEAR + 1) % 100:02d}.csv" +) + # Uprate 3% pa for wage growth from the base year _GROWTH = 1.03 -_BASE_YEAR = 2024 # 2023-24 tax year → calendar 2024 +_LAST_YEAR = 2031 + +# Table 6.1 rate rows → target name suffix. HMRC counts Scottish starter +# and intermediate rate relief as basic rate. The Total row is not a target: +# it is the sum of the bands, which HMRC rounds separately (2024-25: bands +# £8.9bn, total £8.8bn), so targeting both would ask for two values. +_IT_RATES = { + "Basic Rate": "basic_rate", + "Higher Rate": "higher_rate", + "Additional Rate": "additional_rate", +} +# Table 6.2 NICs classes → (target name, PolicyEngine variable) +_NICS_CLASSES = { + "Class 1 Primary (employee)": ( + "hmrc/salary_sacrifice_employee_nics_relief", + "ni_employee", + ), + "Class 1 Secondary (employer)": ( + "hmrc/salary_sacrifice_employer_nics_relief", + "ni_employer", + ), +} + +# Table 6.2 (class, rate row) → the PolicyEngine Class 1 rate it was relieved at +_NICS_RATE_PARAMETERS = { + ("Class 1 Primary (employee)", "Main Rate"): "employee.main", + ("Class 1 Primary (employee)", "Additional Rate"): "employee.additional", + ("Class 1 Secondary (employer)", "Main Rate"): "employer", +} + +# Total salary sacrifice contributions (SPP Review 2025: £24bn base) +_SS_CONTRIBUTIONS_BASE_YEAR = 2024 + + +def _read_table(url: str) -> pd.DataFrame: + try: + r = requests.get(url, headers=HEADERS, allow_redirects=True, timeout=30) + r.raise_for_status() + text = r.content.decode("utf-8-sig") + except requests.RequestException as e: + logger.warning( + "HMRC Tables 6.1 and 6.2 download failed (%s); using %s", + e, + FALLBACK_CSV.name, + ) + text = FALLBACK_CSV.read_text(encoding="utf-8-sig") + return pd.read_csv(io.StringIO(text), dtype=str) + + +def _check_tax_year(df: pd.DataFrame) -> int: + """The table's PolicyEngine year, which must be ``_TAX_YEAR``. + + PolicyEngine UK's year N is tax year N to N+1 (a year-N simulation reads + the parameter values in force from 6 April N), so "2024 to 2025" is 2024. + """ + years = list(df["tax_year"].unique()) + expected = f"{_TAX_YEAR} to {_TAX_YEAR + 1}" + if years != [expected]: + raise ValueError( + f"HMRC Tables 6.1/6.2 cover {years}, but {FALLBACK_CSV.name} and " + f"_TAX_YEAR are {expected!r}: refresh the sources.yaml URL, the " + "committed copy and _TAX_YEAR together." + ) + return _TAX_YEAR + + +def _nics_rate_factors(rows: pd.DataFrame, nics_class: str, base_year: int) -> dict: + """Class 1 rate in each year relative to the table's year, by year. + + Weighted by HMRC's split of the class's relief between the main and + additional rates (rows suppressed as [z] weigh nothing). + """ + from policyengine_uk import CountryTaxBenefitSystem + + parameters = CountryTaxBenefitSystem().parameters + weights = {} + for rate_row in ("Main Rate", "Additional Rate"): + values = rows.loc[ + (rows["nics_relief_class"] == nics_class) & (rows["tax_rate"] == rate_row), + "value_of_relief", + ].map(to_float) + if values.sum() > 0: + path = _NICS_RATE_PARAMETERS[(nics_class, rate_row)] + weights[path] = values.sum() + if not weights: + raise ValueError(f"HMRC Table 6.2: no rate split for {nics_class!r}") + + def rate(path, year): + return parameters.get_child( + f"gov.hmrc.national_insurance.class_1.rates.{path}" + )(str(year)) + + return { + year: sum(w * rate(p, year) / rate(p, base_year) for p, w in weights.items()) + / sum(weights.values()) + for year in range(base_year, _LAST_YEAR + 1) + } + + +def _relief(rows: pd.DataFrame, column: str, label: str) -> float: + """The single positive £m value in ``rows`` whose ``column`` is ``label``.""" + values = rows.loc[rows[column] == label, "value_of_relief"].map(to_float) + if len(values) != 1 or not values.iloc[0] > 0: # also rejects a blank cell + raise ValueError( + f"HMRC salary sacrifice relief: expected one positive value for " + f"{label!r}, got {values.tolist()}" + ) + return values.iloc[0] * 1e6 + + +def _relief_targets(df: pd.DataFrame, reference_url: str) -> list[Target]: + base_year = _check_tax_year(df) + ss = df[ + (df["contribution_type"] == "Salary sacrificed contributions") + & (df["sector_scheme"] == "Total") + & (df["scheme_type"] == "Total") + ] + income_tax = ss[ss["income_tax_nics"] == "Income Tax"] + nics = ss[ss["income_tax_nics"] == "NICs"] + nics_total = nics[nics["tax_rate"] == "Total"] + unchanged = {y: 1.0 for y in range(base_year, _LAST_YEAR + 1)} + + specs = [ + ( + f"hmrc/salary_sacrifice_it_relief_{suffix}", + "income_tax", + _relief(income_tax, "tax_rate", rate), + unchanged, + ) + for rate, suffix in _IT_RATES.items() + ] + [ + ( + name, + variable, + _relief(nics_total, "nics_relief_class", nics_class), + _nics_rate_factors(nics, nics_class, base_year), + ) + for nics_class, (name, variable) in _NICS_CLASSES.items() + ] + return [ + Target( + name=name, + variable=variable, + source="hmrc", + unit=Unit.GBP, + values={ + y: base * _GROWTH ** (y - base_year) * factor + for y, factor in factors.items() + }, + reference_url=reference_url, + ) + for name, variable, base, factors in specs + ] def get_targets() -> list[Target]: - config = load_config() - ref = config["hmrc"]["salary_sacrifice_table_6"] - targets = [] + ref = load_config()["hmrc"]["salary_sacrifice_table_6"] + targets = _relief_targets(_read_table(ref), ref) - try: - r = requests.get(ref, headers=HEADERS, allow_redirects=True, timeout=30) - r.raise_for_status() - df = pd.read_csv(io.StringIO(r.content.decode("utf-8-sig"))) - - ss = df[df["contribution_type"] == "Salary sacrificed contributions"] - - # IT relief by tax band - ss_it = ss[ - (ss["income_tax_nics"] == "Income Tax") - & (ss["sector_scheme"] == "Total") - & (ss["scheme_type"] == "Total") - ] - for _, row in ss_it.iterrows(): - rate = row["tax_rate"] - val = to_float(row["value_of_relief"]) - if val <= 0: - continue - rate_key = rate.lower().replace(" ", "_") - base = val * 1e6 - targets.append( - Target( - name=f"hmrc/salary_sacrifice_it_relief_{rate_key}", - variable="income_tax", - source="hmrc", - unit=Unit.GBP, - values={ - y: base * _GROWTH ** max(0, y - _BASE_YEAR) - for y in range(_BASE_YEAR, 2032) - }, - reference_url=ref, - ) - ) - - # NICs relief (employee + employer) - ss_nics = ss[ - (ss["income_tax_nics"] == "NICs") - & (ss["sector_scheme"] == "Total") - & (ss["scheme_type"] == "Total") - ] - for _, row in ss_nics.iterrows(): - nics_class = row["nics_relief_class"] - val = to_float(row["value_of_relief"]) - if val <= 0: - continue - if "employee" in str(nics_class).lower(): - name = "hmrc/salary_sacrifice_employee_nics_relief" - variable = "ni_employee" - elif "employer" in str(nics_class).lower(): - name = "hmrc/salary_sacrifice_employer_nics_relief" - variable = "ni_employer" - else: - continue - - # Only take the first (Total scheme) row for each class - existing = {t.name for t in targets} - if name in existing: - continue - - base = val * 1e6 - targets.append( - Target( - name=name, - variable=variable, - source="hmrc", - unit=Unit.GBP, - values={ - y: base * _GROWTH ** max(0, y - _BASE_YEAR) - for y in range(_BASE_YEAR, 2032) - }, - reference_url=ref, - ) - ) - - except Exception as e: - logger.error("Failed to download/parse HMRC salary sacrifice CSV: %s", e) - - # Total salary sacrifice contributions (SPP Review 2025: £24bn base) - _SS_CONTRIBUTIONS = { - y: 24e9 * _GROWTH ** max(0, y - _BASE_YEAR) for y in range(_BASE_YEAR, 2030) - } targets.append( Target( name="hmrc/salary_sacrifice_contributions", variable="pension_contributions_via_salary_sacrifice", source="hmrc", unit=Unit.GBP, - values=_SS_CONTRIBUTIONS, + values={ + y: 24e9 * _GROWTH ** (y - _SS_CONTRIBUTIONS_BASE_YEAR) + for y in range(_SS_CONTRIBUTIONS_BASE_YEAR, 2030) + }, reference_url=( "https://assets.publishing.service.gov.uk/media/" "67ce0e7c08e764d17a5d3c21/2025_SPP_Review.pdf" diff --git a/policyengine_uk_data/targets/sources/obr.py b/policyengine_uk_data/targets/sources/obr.py index ad4e3cf17..1b7d3661c 100644 --- a/policyengine_uk_data/targets/sources/obr.py +++ b/policyengine_uk_data/targets/sources/obr.py @@ -17,7 +17,7 @@ import openpyxl import requests -from policyengine_uk_data.targets.schema import Target, Unit +from policyengine_uk_data.targets.schema import GREAT_BRITAIN, Target, Unit from policyengine_uk_data.targets.sources._common import ( HEADERS, load_config, @@ -78,7 +78,8 @@ def _download_workbook(url: str) -> openpyxl.Workbook: Retries transient HTTP errors (429/5xx) and connection failures with exponential backoff, honouring a numeric Retry-After header when present. Falls back to the committed workbook in storage/obr_efo/ when - the download ultimately fails (obr.uk 403s CI runner IPs). + the download ultimately fails: obr.uk 403s CI runner IPs, and can answer + 200 with an HTML page that is not a workbook. Neither is retried. """ last_error: Exception | None = None for attempt in range(_DOWNLOAD_MAX_ATTEMPTS): @@ -89,7 +90,24 @@ def _download_workbook(url: str) -> openpyxl.Workbook: last_error = e # connection/timeout — retryable else: if r.status_code < 400: - return openpyxl.load_workbook(io.BytesIO(r.content), data_only=False) + # obr.uk can answer 200 with an HTML "No Access" page instead + # of the workbook (observed 2026-10-04). A body openpyxl cannot + # read will not become a workbook on retry, so it is permanent, + # like a 403. An openpyxl break on every workbook still + # surfaces, as the fallback goes through the same load_workbook. + try: + return openpyxl.load_workbook( + io.BytesIO(r.content), data_only=False + ) + except Exception as e: + last_error = ValueError( + f"{r.status_code} for url: {url}, but the body " + f"({r.headers.get('Content-Type', 'no Content-Type')}) " + "could not be read as an xlsx workbook " + f"({type(e).__name__}: {e})" + ) + last_error.__cause__ = e + break last_error = requests.HTTPError( f"{r.status_code} for url: {url}", response=r ) @@ -455,6 +473,32 @@ def _parse_nics(wb: openpyxl.Workbook) -> list[Target]: return targets +def _universal_credit_rows(ws) -> tuple[int, int]: + """Table 4.9's universal credit rows inside and outside the welfare cap. + + Each section has exactly one row starting "Universal credit"; the + outside-the-cap section starts at the row headed "Welfare spending + outside the welfare cap". + """ + max_row = ws.max_row + boundary = _find_row( + ws, "Welfare spending outside the welfare cap", max_row=max_row + ) + rows = [ + row + for row in range(1, max_row + 1) + if str(ws[f"B{row}"].value or "").strip().startswith("Universal credit") + ] + inside = [row for row in rows if row < boundary] + outside = [row for row in rows if row > boundary] + if len(inside) != 1 or len(outside) != 1: + raise ValueError( + f"expected one universal credit row each side of row {boundary}, " + f"found rows {rows}" + ) + return inside[0], outside[0] + + def _parse_welfare(wb: openpyxl.Workbook) -> list[Target]: """Parse Table 4.9 (welfare spending) from expenditure xlsx.""" config = load_config() @@ -482,11 +526,11 @@ def read_49(row_num: int) -> dict[int, float]: result[fy[col]] = float(val) * 1e9 return result + # "Housing benefit (not on JSA)" is not targeted: it is DWP-funded GB + # spending only, and dwp_housing_benefit.py targets total GB Housing + # Benefit split by age instead. "Pension credit" is the same DWP GB line + # that dwp_pension_credit.py targets, with its caseload. benefit_rows = { - "housing_benefit": ( - "Housing benefit (not on JSA)", - "housing_benefit", - ), "pip": ( "Disability living allowance and personal independence p", "pip", @@ -496,7 +540,6 @@ def read_49(row_num: int) -> dict[int, float]: "Attendance allowance", "attendance_allowance", ), - "pension_credit": ("Pension credit", "pension_credit"), "carers_allowance": ("Carer's allowance", "carers_allowance"), "statutory_maternity_pay": ( "Statutory maternity pay", @@ -506,10 +549,6 @@ def read_49(row_num: int) -> dict[int, float]: "Winter fuel payment", "winter_fuel_allowance", ), - "universal_credit_in_cap": ( - "Universal credit", - "universal_credit", - ), "child_benefit": ("Child benefit", "child_benefit"), "state_pension": ("State pension", "state_pension"), "jobseekers_allowance": ( @@ -539,30 +578,41 @@ def read_49(row_num: int) -> dict[int, float]: except ValueError: logger.warning("OBR welfare: row '%s' not found", label) - # Universal credit outside cap (row 43) is jobseekers UC + # Universal credit has two rows: spending inside the welfare cap (row 18 + # of the March 2026 table) and outside it (row 43). The welfare cap is the + # Charter for Budget Responsibility's limit on welfare spending, not the + # household benefit cap. It excludes the State Pension and the payments + # most sensitive to the economic cycle: JSA, associated Housing Benefit + # and their UC equivalent. DWP's benefit expenditure and caseload tables + # (Spring Forecast 2026) give the same £12.876bn for 2025-26 and define + # it (Notes) as "the total expenditure ... directed to those in the + # Intensive Work Search group"; EFO March 2026 para 4.24 n.20 uses the + # same regime. policyengine-uk has no UC conditionality regime. A proxy + # built from the legal tests and FRS employment status went from 5% under + # to 27% over DWP's 2025-26 figure when one employment-status code was + # mapped correctly, too fragile to calibrate to, so the two rows are + # targeted as one total. Both sit under "DWP social security", + # which covers Great Britain: Northern Ireland's UC is in the "NI social + # security" rows. try: - # UC outside cap = predominantly JSA-conditionality UC - uc_outside_row = _find_row(ws, "Universal credit", col="B", max_row=55) - # Find the second UC row (outside cap section) - for row in range(uc_outside_row + 1, 55): - cell_val = ws[f"B{row}"].value - if cell_val and str(cell_val).strip().startswith("Universal credit"): - values = read_49(row) - if values: - targets.append( - Target( - name="obr/universal_credit_outside_cap", - variable="universal_credit", - source="obr", - unit=Unit.GBP, - values=values, - reference_url=ref, - forecast_vintage=vintage, - ) - ) - break - except ValueError: - logger.warning("OBR welfare: UC outside cap not found") + inside_row, outside_row = _universal_credit_rows(ws) + inside, outside = read_49(inside_row), read_49(outside_row) + if not inside or inside.keys() != outside.keys(): + raise ValueError("the two universal credit rows cover different years") + targets.append( + Target( + name="obr/universal_credit", + variable="universal_credit", + source="obr", + unit=Unit.GBP, + values={year: inside[year] + outside[year] for year in inside}, + countries=GREAT_BRITAIN, + reference_url=ref, + forecast_vintage=vintage, + ) + ) + except ValueError as e: + logger.warning("OBR welfare: universal credit not parsed: %s", e) return targets @@ -617,9 +667,7 @@ def _parse_tv_licence(wb: openpyxl.Workbook) -> list[Target]: # ISC census: private school students (roughly constant at ~557k) _PRIVATE_SCHOOL = {y: 557_000 for y in range(2018, 2032)} -# SPP Review: salary sacrifice NI relief (uprated 3% pa from 2024 base) -_SS_EMPLOYEE_NI = {y: 1.2e9 * 1.03 ** max(0, y - 2024) for y in range(2024, 2032)} -_SS_EMPLOYER_NI = {y: 2.9e9 * 1.03 ** max(0, y - 2024) for y in range(2024, 2032)} +# Salary sacrifice NICs relief comes from HMRC Table 6.2 (hmrc_salary_sacrifice.py). # Salary sacrifice headcount: 7.7m total (3.3m above £2k, 4.3m below) # OBR para 1.7: SS population grows 0.9% faster than employees (~2.4%/yr) @@ -666,26 +714,6 @@ def get_targets() -> list[Target]: reference_url="https://www.isc.co.uk/research/annual-census/", ) ) - targets.append( - Target( - name="obr/salary_sacrifice_employee_ni_relief", - variable="ni_employee", - source="obr", - unit=Unit.GBP, - values=_SS_EMPLOYEE_NI, - reference_url="https://assets.publishing.service.gov.uk/media/67ce0e7c08e764d17a5d3c21/2025_SPP_Review.pdf", - ) - ) - targets.append( - Target( - name="obr/salary_sacrifice_employer_ni_relief", - variable="ni_employer", - source="obr", - unit=Unit.GBP, - values=_SS_EMPLOYER_NI, - reference_url="https://assets.publishing.service.gov.uk/media/67ce0e7c08e764d17a5d3c21/2025_SPP_Review.pdf", - ) - ) # Salary sacrifice headcount targets _SS_REF = ( diff --git a/policyengine_uk_data/targets/sources/ons_labour_market.py b/policyengine_uk_data/targets/sources/ons_labour_market.py new file mode 100644 index 000000000..bae191b88 --- /dev/null +++ b/policyengine_uk_data/targets/sources/ons_labour_market.py @@ -0,0 +1,83 @@ +"""ONS Labour Force Survey employment levels: employees and the self-employed. + +The FRS records each adult's ILO employment status for their main job +(EMPSTATI, `employment_status`), the same concept the LFS uses. Without these +targets nothing in the calibration ties the number of employees or +self-employed people to an official count. The HMRC counts of income-tax +payers with employment income (SPI tables 3.6 and 3.15) are annual: they +include people with pay for part of the year whose status at interview is +out of work. So calibration can meet them by moving weight from people out of +work to employees. + +The FRS child table (dependent children, including 16-19-year-olds in +non-advanced education) carries no employment status, so their jobs are not +counted here, while the LFS counts them. On the FRS 2024-25 grossing weights, +the FRS has 28.1m employees against the LFS's 29.1m for 2024. + +Source: ONS Labour market overview, series MGRN (LFS: Employees: UK: All, +aged 16 and over, seasonally adjusted) and MGRQ (LFS: Self-employed: UK: +All), annual four-quarter averages, release of 15 September 2026. The loss +matrix holds the latest year's value for up to three later years and drops the +targets after that (``_resolve_value``), so add each new annual average. +""" + +import numpy as np + +from policyengine_uk_data.targets.schema import ( + GeographicLevel, + Target, + Unit, +) +from policyengine_uk_data.utils.employment_status import ( + EMPLOYEE_STATUSES, + SELF_EMPLOYED_STATUSES, +) + +_REF = ( + "https://www.ons.gov.uk/employmentandlabourmarket/peopleinwork/" + "employmentandemployeetypes/timeseries/{series}/lms" +) + +# Annual four-quarter averages, people. +EMPLOYEES = { + 2022: 28_564_000.0, + 2023: 28_821_000.0, + 2024: 29_126_000.0, + 2025: 29_590_000.0, +} +SELF_EMPLOYED = { + 2022: 4_244_000.0, + 2023: 4_380_000.0, + 2024: 4_340_000.0, + 2025: 4_395_000.0, +} + + +def _status_count(statuses: tuple[str, ...]): + def compute(ctx, target, year) -> np.ndarray: + status = np.asarray(ctx.pe_person("employment_status")).astype(str) + return ctx.household_from_person(np.isin(status, statuses).astype(float)) + + return compute + + +def get_targets() -> list[Target]: + return [ + Target( + name=f"ons/lfs_{name}", + variable="employment_status", + source="ons", + unit=Unit.COUNT, + geographic_level=GeographicLevel.NATIONAL, + geo_code="K02000001", + geo_name="United Kingdom", + values=dict(values), + is_count=True, + reference_url=_REF.format(series=series), + custom_compute=_status_count(statuses), + ) + for name, values, series, statuses in ( + ("employees", EMPLOYEES, "mgrn", EMPLOYEE_STATUSES), + ("self_employed", SELF_EMPLOYED, "mgrq", SELF_EMPLOYED_STATUSES), + ) + ] diff --git a/policyengine_uk_data/tests/test_brma_assignment.py b/policyengine_uk_data/tests/test_brma_assignment.py new file mode 100644 index 000000000..8de49f999 --- /dev/null +++ b/policyengine_uk_data/tests/test_brma_assignment.py @@ -0,0 +1,218 @@ +"""BRMA assignment: census private-rented weights and the draw. + +Invariants of ``assign_brmas``, for any weights table and any benefit units +whose region × LHA category cell has a positive weight: + +1. Support: each benefit unit's BRMA has a positive weight in its cell. +2. Determinism: the same generator state gives the same BRMAs. +3. Fail closed: a benefit unit in a cell with no positive weight raises. +4. Proportionality: draws converge on the cell's weights. + +``pick_household_brmas`` gives each household one of its benefit units' BRMAs, +deterministically for a given generator state. +""" + +import numpy as np +import pandas as pd +import pytest +from hypothesis import assume, given, settings +from hypothesis import strategies as st +from policyengine_uk.variables.household.demographic.locations import BRMAName + +from policyengine_uk_data.datasets.brma import ( + BRMA_HOUSEHOLDS_PATH, + LHA_CATEGORY_BEDROOMS, + assign_brmas, + load_brma_weights, + pick_household_brmas, +) + +REGIONS = [ + "NORTH_EAST", + "NORTH_WEST", + "YORKSHIRE", + "EAST_MIDLANDS", + "WEST_MIDLANDS", + "EAST_OF_ENGLAND", + "LONDON", + "SOUTH_EAST", + "SOUTH_WEST", + "WALES", + "SCOTLAND", + "NORTHERN_IRELAND", +] +CATEGORIES = list(LHA_CATEGORY_BEDROOMS) + + +@pytest.fixture(scope="module") +def households(): + return pd.read_csv(BRMA_HOUSEHOLDS_PATH, dtype={"bedrooms": str}) + + +@pytest.fixture(scope="module") +def weights(): + return load_brma_weights() + + +def test_table_covers_every_region_band_and_brma(households): + assert list(households.columns) == ["region", "brma", "bedrooms", "households"] + assert (households.households > 0).all() + assert not households.duplicated(["region", "brma", "bedrooms"]).any() + assert set(households.region) == set(REGIONS) + assert set(households.brma) == set(BRMAName.__members__) + bands = households.groupby("region").bedrooms.agg(set) + # Northern Ireland's 2021 census has no bedrooms question. + assert bands.pop("NORTHERN_IRELAND") == {"all"} + assert all(b == {"1", "2", "3", "4+"} for b in bands) + + +# Published private-rented households (private landlord or letting agency plus +# other private rented): Census 2021 TS054 (England, Wales); Scotland's Census +# 2022 tenure by bedrooms, national cells; NISRA Census 2021 tenure, 7 categories. +CENSUS_PRIVATE_RENTED = { + "ENGLAND": 4_794_889, + "WALES": 228_642, + "SCOTLAND": 323_042, + "NORTHERN_IRELAND": 132_436, +} + + +def test_nations_reconcile_to_published_census_totals(households): + nation = households.region.where( + households.region.isin(["WALES", "SCOTLAND", "NORTHERN_IRELAND"]), "ENGLAND" + ) + totals = households.groupby(nation).households.sum() + for name, published in CENSUS_PRIVATE_RENTED.items(): + # Small-area cells are perturbed for disclosure control, so sums + # differ slightly from national tables. + assert abs(totals[name] / published - 1) < 0.001, name + + +def test_edinburgh_and_glasgow_hold_the_most_scottish_private_renters(households): + # Census 2022: Lothian 19.3%, Greater Glasgow 15.9%. The 2019-20 list of + # rents gave Greater Glasgow the fewest entries of any Scottish BRMA. + scotland = ( + households[households.region == "SCOTLAND"].groupby("brma").households.sum() + ) + assert set(scotland.nlargest(2).index) == {"LOTHIAN", "GREATER_GLASGOW"} + + +def test_every_region_category_cell_has_weights(weights): + cells = weights.groupby(["region", "lha_category"]).weight.sum() + assert len(cells) == len(REGIONS) * len(CATEGORIES) + assert (cells > 0).all() + + +def test_shared_and_one_bedroom_categories_use_the_same_weights(weights): + a = weights[weights.lha_category == "A"].drop(columns="lha_category") + b = weights[weights.lha_category == "B"].drop(columns="lha_category") + pd.testing.assert_frame_equal(a.reset_index(drop=True), b.reset_index(drop=True)) + + +def test_each_category_uses_its_bedroom_band(households, weights): + banded = households[households.bedrooms != "all"] + for category, band in LHA_CATEGORY_BEDROOMS.items(): + got = weights[weights.lha_category == category].set_index(["region", "brma"]) + want = banded[banded.bedrooms == band].set_index(["region", "brma"]) + assert ( + got.weight.drop("NORTHERN_IRELAND") + .sort_index() + .equals(want.households.sort_index().rename("weight")) + ), category + # Spot values: Lothian's two-, three- and four-plus-bedroom private renters + # (Census 2022) and Belfast, the same for every category. + lothian = weights[(weights.region == "SCOTLAND") & (weights.brma == "LOTHIAN")] + assert lothian.set_index("lha_category").weight[["C", "D", "E"]].tolist() == [ + 30012, + 10044, + 4327, + ] + belfast = weights[weights.brma == "BELFAST"] + assert len(belfast) == 5 and belfast.weight.nunique() == 1 + + +def test_large_draw_matches_scottish_two_bedroom_weights(weights): + cell = weights[(weights.region == "SCOTLAND") & (weights.lha_category == "C")] + n = 400_000 + drawn = assign_brmas( + np.full(n, "SCOTLAND"), np.full(n, "C"), np.random.default_rng(1), weights + ) + observed = pd.Series(drawn).value_counts(normalize=True) + expected = cell.set_index("brma").weight / cell.weight.sum() + assert observed.index.isin(expected.index).all() + assert ( + observed.reindex(expected.index, fill_value=0) - expected + ).abs().max() < 0.004 + + +@st.composite +def weights_and_units(draw): + regions = draw( + st.lists(st.sampled_from(REGIONS), min_size=1, max_size=3, unique=True) + ) + count = draw(st.integers(1, 5)) + # BRMA names are unique to a region, so a draw from the wrong region is caught. + rows = [ + (region, category, f"{region}_{i}", draw(st.sampled_from([0, 0.5, 1, 7, 1000]))) + for region in regions + for category in CATEGORIES + for i in range(count) + ] + table = pd.DataFrame(rows, columns=["region", "lha_category", "brma", "weight"]) + table = table[table.weight > 0] + cells = sorted(set(zip(table.region, table.lha_category))) + units = draw(st.lists(st.sampled_from(cells), max_size=40)) if cells else [] + region = np.array([u[0] for u in units], dtype=str) + category = np.array([u[1] for u in units], dtype=str) + return table, region, category, draw(st.integers(0, 2**32 - 1)) + + +@settings(max_examples=200, deadline=None, derandomize=True) +@given(weights_and_units()) +def test_draws_stay_on_positive_weights_and_are_deterministic(case): + table, region, category, seed = case + first = assign_brmas(region, category, np.random.default_rng(seed), table) + second = assign_brmas(region, category, np.random.default_rng(seed), table) + assert list(first) == list(second) + positive = set(zip(table.region, table.lha_category, table.brma)) + assert all((r, c, b) in positive for r, c, b in zip(region, category, first)) + + +@settings(max_examples=100, deadline=None, derandomize=True) +@given(weights_and_units(), st.sampled_from(REGIONS), st.sampled_from(CATEGORIES)) +def test_cells_without_positive_weight_fail_closed(case, region, category): + table, *_ = case + # Remove the cell, so every example exercises a missing one. + table = table[~((table.region == region) & (table.lha_category == category))] + assume(len(table)) + with pytest.raises(ValueError, match="No BRMA weights"): + assign_brmas( + np.array([region]), np.array([category]), np.random.default_rng(0), table + ) + + +@settings(max_examples=200, deadline=None, derandomize=True) +@given( + st.lists( + st.tuples(st.integers(0, 6), st.sampled_from(["X", "Y", "Z"])), + min_size=1, + max_size=30, + ), + st.integers(0, 2**32 - 1), +) +def test_households_take_one_of_their_benefit_units_brmas(units, seed): + household_id = np.array([h for h, _ in units]) + brma = np.array([b for _, b in units], dtype=object) + first = pick_household_brmas(brma, household_id, np.random.default_rng(seed)) + second = pick_household_brmas(brma, household_id, np.random.default_rng(seed)) + assert first.equals(second) + assert set(first.index) == set(household_id) + for household, chosen in first.items(): + assert chosen in set(brma[household_id == household]) + + +def test_built_dataset_brmas_lie_in_their_regions_weights(enhanced_frs, weights): + household = enhanced_frs.household + supported = set(zip(weights.region, weights.brma)) + pairs = zip(household.region.astype(str), household.brma.astype(str)) + assert all(pair in supported for pair in pairs) diff --git a/policyengine_uk_data/tests/test_calibrate_validation_targets.py b/policyengine_uk_data/tests/test_calibrate_validation_targets.py new file mode 100644 index 000000000..4dfba5483 --- /dev/null +++ b/policyengine_uk_data/tests/test_calibrate_validation_targets.py @@ -0,0 +1,95 @@ +"""Validation-only targets in calibrate_local_areas. + +create_datasets passes VALIDATION_ONLY_LOCAL_TARGETS, which exist only in the +local matrices. The calibration must then train on the other targets, leave +the excluded one out of training, and log a finite validation loss (an empty +national validation set used to make it NaN). +""" + +from __future__ import annotations + +import importlib.util + +import numpy as np +import pandas as pd +import pytest + +if ( + importlib.util.find_spec("torch") is None + or importlib.util.find_spec("policyengine_uk") is None +): + pytest.skip( + "torch/policyengine_uk not available in test environment", + allow_module_level=True, + ) + +from policyengine_uk_data.tests.test_calibrate_save import _StubDataset + + +def _performance(weights, _m_c, _y_c, m_n, y_n, _excluded_targets): + estimate = float((weights.sum(axis=0) @ m_n).iloc[0]) + target = float(y_n.iloc[0]) + return pd.DataFrame( + { + "name": ["UK"], + "metric": ["national_total"], + "estimate": [estimate], + "target": [target], + "error": [estimate - target], + "abs_error": [abs(estimate - target)], + "rel_abs_error": [abs(estimate - target) / target], + "validation": [False], + } + ) + + +def _calibrate(tmp_path, monkeypatch, excluded, target_b): + import h5py + import torch + + from policyengine_uk_data.utils import calibrate as calibrate_module + from policyengine_uk_data.utils.calibrate import calibrate_local_areas + + monkeypatch.setattr(calibrate_module, "STORAGE_FOLDER", tmp_path) + # Household 0 feeds local target "a", household 1 feeds "b". + matrix = pd.DataFrame({"a": [1.0, 0.0], "b": [0.0, 1.0]}) + local = pd.DataFrame({"a": [100.0], "b": [target_b]}) + national = pd.DataFrame({"national_total": [1.0, 1.0]}) + + log_csv = tmp_path / f"log_{len(excluded)}_{target_b:.0f}.csv" + # Weight dropout is random; seed it so runs differ only by their inputs. + torch.manual_seed(0) + calibrate_local_areas( + dataset=_StubDataset(np.array([50.0, 50.0])), + matrix_fn=lambda _d: (matrix.copy(), local.copy(), np.ones((1, 2))), + national_matrix_fn=lambda _d: (national.copy(), pd.Series([200.0])), + area_count=1, + weight_file=f"weights_{len(excluded)}_{target_b:.0f}.h5", + dataset_key="2025", + epochs=21, + excluded_training_targets=excluded, + log_csv=log_csv, + get_performance=_performance, + verbose=True, + ) + with h5py.File(tmp_path / f"weights_{len(excluded)}_{target_b:.0f}.h5") as f: + weights = f["2025"][:] + return weights, pd.read_csv(log_csv) + + +def test_local_only_validation_target_logs_finite_validation_loss( + tmp_path, monkeypatch +): + _, log = _calibrate(tmp_path, monkeypatch, ["b"], 100.0) + assert np.isfinite(log["validation_loss"]).all() + + +def test_validation_only_target_does_not_train(tmp_path, monkeypatch): + # Changing an excluded target's value leaves the weights unchanged; + # changing it when it trains moves them. + excluded_low, _ = _calibrate(tmp_path, monkeypatch, ["b"], 100.0) + excluded_high, _ = _calibrate(tmp_path, monkeypatch, ["b"], 10_000.0) + np.testing.assert_allclose(excluded_low, excluded_high) + trained_low, _ = _calibrate(tmp_path, monkeypatch, [], 100.0) + trained_high, _ = _calibrate(tmp_path, monkeypatch, [], 10_000.0) + assert not np.allclose(trained_low, trained_high) diff --git a/policyengine_uk_data/tests/test_cgt_band_donors.py b/policyengine_uk_data/tests/test_cgt_band_donors.py index a24e2e1d5..ec97d6519 100644 --- a/policyengine_uk_data/tests/test_cgt_band_donors.py +++ b/policyengine_uk_data/tests/test_cgt_band_donors.py @@ -158,6 +158,13 @@ def test_stack_cgt_band_donors(frs): # still catches order-of-magnitude pathologies without failing on # reduced-fidelity calibration noise. Full builds get the strict bounds. _REDUCED_BUILD_SLACK = 5.0 if os.environ.get("TESTING") == "1" else 1.0 +# Reduced builds sit far above HMRC's total gains: the seed-0 reduced builds +# of main and of #529 carry about £243bn and £267bn (relative errors 2.7 and +# 3.1, beyond the 0.5 x slack bound), while their full builds carry about +# £53bn. Under TESTING the total-gains check only guards against +# order-of-magnitude errors, in either direction: the total must lie within a +# factor of 6 of HMRC's. The full build keeps the 50% bound. +_REDUCED_GAINS_FACTOR = 6.0 def _built_with_band_donors(enhanced_frs): @@ -215,11 +222,15 @@ def test_built_total_gains(enhanced_frs): enhanced_frs = _built_with_band_donors(enhanced_frs) gains, weights = _person_gains_and_weights(enhanced_frs) total = float((gains * weights)[gains > _AEA].sum()) - assert abs(total / _HMRC_TOTAL_GAINS - 1) < 0.5 * _REDUCED_BUILD_SLACK, ( + ratio = total / _HMRC_TOTAL_GAINS + message = ( f"£{total / 1e9:.1f}bn of above-AEA gains against HMRC's " - f"£{_HMRC_TOTAL_GAINS / 1e9:.1f}bn " - f"(relative error {abs(total / _HMRC_TOTAL_GAINS - 1):.0%})." + f"£{_HMRC_TOTAL_GAINS / 1e9:.1f}bn (ratio {ratio:.2f})." ) + if _REDUCED_BUILD_SLACK > 1: + assert 1 / _REDUCED_GAINS_FACTOR < ratio < _REDUCED_GAINS_FACTOR, message + else: + assert abs(ratio - 1) < 0.5, message @pytest.mark.slow diff --git a/policyengine_uk_data/tests/test_claimant_or_partner.py b/policyengine_uk_data/tests/test_claimant_or_partner.py new file mode 100644 index 000000000..6c19317a7 --- /dev/null +++ b/policyengine_uk_data/tests/test_claimant_or_partner.py @@ -0,0 +1,181 @@ +"""The claimant-or-partner role: FRS derivation, stacking, and built datasets.""" + +import numpy as np +import pandas as pd +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st +from policyengine_uk.data import UKSingleYearDataset + +from policyengine_uk_data.datasets.frs import ( + derive_is_claimant_or_partner_from_frs_microdata, + derive_is_parent_from_frs_microdata, +) +from policyengine_uk_data.utils.stack import stack_datasets + + +def test_grown_up_child_heads_their_own_benefit_unit(): + # Lone parent and dependent child in one benefit unit; grown-up son in his own. + result = derive_is_claimant_or_partner_from_frs_microdata( + person_ids=np.array([1_001, 1_002, 1_003]), + person_benunit_ids=np.array([101, 102, 101]), + adult_person_ids=np.array([1_001, 1_002]), + ) + assert result.tolist() == [True, True, False] + + +def test_couple_with_children(): + result = derive_is_claimant_or_partner_from_frs_microdata( + person_ids=np.array([2_003, 2_001, 2_002]), + person_benunit_ids=np.array([201, 201, 201]), + adult_person_ids=np.array([2_002, 2_001]), + ) + assert result.tolist() == [False, True, True] + + +@pytest.mark.parametrize("adults", [[], [3_001, 3_002, 3_003]]) +def test_benefit_unit_without_one_or_two_adults_is_rejected(adults): + with pytest.raises(ValueError, match="one or two adult-table records"): + derive_is_claimant_or_partner_from_frs_microdata( + person_ids=np.array([3_001, 3_002, 3_003]), + person_benunit_ids=np.array([301, 301, 301]), + adult_person_ids=np.array(adults, dtype=int), + ) + + +@st.composite +def frs_households(draw): + """FRS-shaped records: benefit units of one or two adults plus children.""" + rows = [] + for household in range(1, draw(st.integers(1, 6)) + 1): + person = 0 + for benunit in range(1, draw(st.integers(1, 3)) + 1): + n_adults = draw(st.integers(1, 2)) + n_children = draw(st.integers(0, 4)) + for index in range(n_adults + n_children): + person += 1 + rows.append( + ( + household * 1_000 + person, + household * 100 + benunit, + index < n_adults, + ) + ) + order = draw(st.permutations(range(len(rows)))) + frame = pd.DataFrame(rows, columns=["person_id", "benunit_id", "adult"]) + return frame.iloc[list(order)].reset_index(drop=True) + + +def _derive(frame, adult_mask=None): + adult = frame.adult if adult_mask is None else adult_mask + return derive_is_claimant_or_partner_from_frs_microdata( + person_ids=frame.person_id, + person_benunit_ids=frame.benunit_id, + adult_person_ids=frame.person_id[adult], + ) + + +@settings(max_examples=300, deadline=None) +@given(frs_households()) +def test_every_benefit_unit_has_a_claimant_and_at_most_one_partner(frame): + result = _derive(frame) + assert (result == frame.adult.to_numpy()).all() + counts = pd.Series(result).groupby(frame.benunit_id.to_numpy()).sum() + assert counts.between(1, 2).all() + + +@settings(max_examples=200, deadline=None) +@given(frs_households(), st.randoms(use_true_random=False)) +def test_record_order_does_not_change_anyones_role(frame, rng): + order = list(range(len(frame))) + rng.shuffle(order) + shuffled = frame.iloc[order].reset_index(drop=True) + by_person = dict(zip(frame.person_id, _derive(frame))) + assert [by_person[p] for p in shuffled.person_id] == _derive(shuffled).tolist() + + +@settings(max_examples=200, deadline=None) +@given(frs_households(), st.data()) +def test_parents_are_always_claimant_or_partner(frame, data): + benunits = np.sort(frame.benunit_id.unique()) + children = data.draw( + st.lists(st.integers(0, 4), min_size=len(benunits), max_size=len(benunits)) + ) + is_parent = derive_is_parent_from_frs_microdata( + person_ids=frame.person_id, + person_benunit_ids=frame.benunit_id, + adult_person_ids=frame.person_id[frame.adult], + benunit_ids=benunits, + dependent_children=np.array(children), + ) + assert not (is_parent & ~_derive(frame)).any() + + +@settings(max_examples=200, deadline=None) +@given(frs_households(), st.data()) +def test_a_benefit_unit_with_no_adult_or_a_third_adult_is_rejected(frame, data): + benunit = data.draw(st.sampled_from(sorted(frame.benunit_id.unique()))) + members = frame.benunit_id == benunit + if data.draw(st.booleans()): + adult = frame.adult & ~members + else: + n_extra = 3 - int(frame.adult[members].sum()) + start = frame.person_id.max() + 1 + extra = pd.DataFrame( + { + "person_id": range(start, start + n_extra), + "benunit_id": benunit, + "adult": True, + } + ) + frame = pd.concat([frame, extra], ignore_index=True) + adult = frame.adult + with pytest.raises(ValueError, match="one or two adult-table records"): + _derive(frame, adult) + + +def _tiny_dataset(roles=True): + person = pd.DataFrame( + { + "person_id": [1, 2, 3], + "person_benunit_id": [1, 1, 1], + "person_household_id": [1, 1, 1], + "age": [40, 38, 10], + } + ) + if roles: + person["is_claimant_or_partner"] = [True, True, False] + return UKSingleYearDataset( + person=person, + benunit=pd.DataFrame({"benunit_id": [1]}), + household=pd.DataFrame({"household_id": [1], "household_weight": [1.0]}), + fiscal_year=2024, + ) + + +def test_stacking_keeps_roles(): + stacked = stack_datasets(_tiny_dataset(), _tiny_dataset()) + stacked.validate() + assert stacked.person.is_claimant_or_partner.dtype == bool + assert stacked.person.is_claimant_or_partner.tolist() == [True, True, False] * 2 + + +@pytest.mark.parametrize("roles", [(True, False), (False, True)]) +def test_stacking_refuses_a_table_without_roles(roles): + with pytest.raises(ValueError, match="is_claimant_or_partner"): + stack_datasets(_tiny_dataset(roles[0]), _tiny_dataset(roles[1])) + + +@pytest.mark.parametrize("fixture", ["frs", "enhanced_frs"]) +def test_built_dataset_roles(fixture, request): + person = request.getfixturevalue(fixture).person + role = person.is_claimant_or_partner + assert role.dtype == bool + counts = role.groupby(person.person_benunit_id).sum() + assert counts.between(1, 2).all() + assert not (person.is_benunit_head & ~role).any() + assert not (person.is_parent & ~role).any() + assert (person.age[role] >= 16).all() + # FRS dependent children are 19 at most, so everyone older is in the + # adult table: a claimant or partner. + assert role[person.age >= 20].all() diff --git a/policyengine_uk_data/tests/test_clone_and_assign.py b/policyengine_uk_data/tests/test_clone_and_assign.py index a6cf15637..a3c8c0e16 100644 --- a/policyengine_uk_data/tests/test_clone_and_assign.py +++ b/policyengine_uk_data/tests/test_clone_and_assign.py @@ -256,6 +256,20 @@ def test_data_preserved_across_clones(self, toy_dataset, small_crosswalk): original_regions = toy_dataset.household["region"].values np.testing.assert_array_equal(regions, original_regions) + def test_claimant_or_partner_follows_each_clone(self, toy_dataset, small_crosswalk): + roles = np.tile([True, False], len(toy_dataset.household)) + toy_dataset.person["is_claimant_or_partner"] = roles + result = clone_and_assign( + toy_dataset, + n_clones=3, + crosswalk_path=str(small_crosswalk), + ) + + assert result.person["is_claimant_or_partner"].dtype == bool + np.testing.assert_array_equal( + result.person["is_claimant_or_partner"].values, np.tile(roles, 3) + ) + def test_single_clone_is_near_identity(self, toy_dataset, small_crosswalk): """With n_clones=1, output should match input dimensions.""" result = clone_and_assign( diff --git a/policyengine_uk_data/tests/test_dwp_housing_benefit_targets.py b/policyengine_uk_data/tests/test_dwp_housing_benefit_targets.py new file mode 100644 index 000000000..53cd1c46e --- /dev/null +++ b/policyengine_uk_data/tests/test_dwp_housing_benefit_targets.py @@ -0,0 +1,446 @@ +"""Tests for the DWP Housing Benefit targets by age group and GB scope.""" + +from types import SimpleNamespace + +import numpy as np +import openpyxl +import pandas as pd +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_uk_data.targets import get_all_targets +from policyengine_uk_data.targets.build_loss_matrix import ( + _resolve_value, + restrict_to_countries, +) +from policyengine_uk_data.targets.schema import GREAT_BRITAIN, Target, Unit +from policyengine_uk_data.targets.sources import dwp_housing_benefit, obr + +# Other rows of the same DWP Housing benefits sheet, transcribed separately: +# total Housing Benefit is AME within the welfare cap + AME outside it + LA +# funded (£ million), and the total caseload (thousands). +_DWP_TOTAL_GBP_M = { + 2022: 14_839.6 + 160.8 + 578.7, + 2023: 14_946.4 + 111.0 + 715.2, + 2024: 14_593.4 + 57.2 + 804.0, + 2025: 12_103.3 + 0.0 + 789.6, + 2026: 11_643.4 + 0.0 + 830.8, + 2027: 11_969.5 + 0.0 + 864.6, + 2028: 12_352.1 + 0.0 + 900.8, + 2029: 12_950.1 + 0.0 + 949.2, + 2030: 13_368.2 + 0.0 + 978.5, +} +_DWP_TOTAL_CASELOAD_K = { + 2022: 2_509, + 2023: 2_351, + 2024: 2_072, + 2025: 1_569, + 2026: 1_410, + 2027: 1_396, + 2028: 1_390, + 2029: 1_397, + 2030: 1_414, +} + +# Same sheet, "Housing Benefit by Accomodation Type: of which General +# Needs" (£ million and thousands, all ages), transcribed separately. +_DWP_GENERAL_NEEDS_GBP_M = { + 2024: 10_760.6, + 2025: 7_699.5, + 2026: 6_894.5, + 2027: 6_938.0, + 2028: 7_034.6, + 2029: 7_282.5, + 2030: 7_448.0, +} +_DWP_GENERAL_NEEDS_K = { + 2024: 1_734, + 2025: 1_216, + 2026: 1_047, + 2027: 1_021, + 2028: 1_004, + 2029: 1_000, + 2030: 1_004, +} + +_CALIBRATED = { + "dwp/housing_benefit/over_pension_credit_age", + "dwp/housing_benefit/over_pension_credit_age_claims", + "dwp/housing_benefit/under_pension_credit_age_general_needs", + "dwp/housing_benefit/under_pension_credit_age_general_needs_claims", +} +_ALL = _CALIBRATED | { + "dwp/housing_benefit/under_pension_credit_age", + "dwp/housing_benefit/under_pension_credit_age_claims", +} + + +def _targets(): + """Every group's targets, including the full working-age line that + calibration leaves out.""" + return {t.name: t for t in dwp_housing_benefit.build_targets()} + + +def test_targets_cover_great_britain_in_pounds_and_claims(): + targets = _targets() + assert set(targets) == _ALL + for name, target in targets.items(): + assert target.countries == GREAT_BRITAIN + assert target.variable == "housing_benefit" + assert target.is_count == name.endswith("_claims") + assert target.unit == (Unit.COUNT if target.is_count else Unit.GBP) + over = targets["dwp/housing_benefit/over_pension_credit_age"] + under = targets["dwp/housing_benefit/under_pension_credit_age_claims"] + assert over.values[2025] == 7_114.7e6 + assert under.values[2025] == 460e3 + + +def test_age_split_reconciles_with_dwp_totals(): + """Over + under matches DWP's total, including LA-funded spending.""" + targets = _targets() + over = targets["dwp/housing_benefit/over_pension_credit_age"].values + under = targets["dwp/housing_benefit/under_pension_credit_age"].values + over_k = targets["dwp/housing_benefit/over_pension_credit_age_claims"].values + under_k = targets["dwp/housing_benefit/under_pension_credit_age_claims"].values + for year, total in _DWP_TOTAL_GBP_M.items(): + assert abs((over[year] + under[year]) / 1e6 - total) < 0.25, year + for year, total in _DWP_TOTAL_CASELOAD_K.items(): + # DWP rounds each caseload to the nearest thousand. + assert abs((over_k[year] + under_k[year]) / 1e3 - total) <= 1, year + + +def test_calibration_uses_working_age_net_of_supported_and_temporary(): + housing_benefit_targets = { + t.name for t in get_all_targets() if t.variable == "housing_benefit" + } + assert housing_benefit_targets == _CALIBRATED + + +def test_accommodation_types_reconcile_with_the_age_split(): + """General needs + supported + temporary is all Housing Benefit, the + same total as over + under, in every year DWP splits it.""" + h = dwp_housing_benefit + for year, general_needs in _DWP_GENERAL_NEEDS_GBP_M.items(): + by_type = general_needs + h._SUPPORTED_GBP_M[year] + h._TEMPORARY_GBP_M[year] + by_age = h._SPENDING_GBP_M["over"][year] + h._SPENDING_GBP_M["under"][year] + assert abs(by_type - by_age) <= 0.2, year + by_type_k = ( + _DWP_GENERAL_NEEDS_K[year] + + h._SUPPORTED_THOUSANDS[year] + + h._TEMPORARY_THOUSANDS[year] + ) + by_age_k = ( + h._CASELOAD_THOUSANDS["over"][year] + h._CASELOAD_THOUSANDS["under"][year] + ) + assert abs(by_type_k - by_age_k) <= 1, year + + +def test_working_age_general_needs_two_ways(): + """Working-age less supported and temporary accommodation equals all-age + general needs less pension-age Housing Benefit, since the two splits + share one total; it is defined only where positive (2024-25, 2025-26).""" + h = dwp_housing_benefit + spending = h._SPENDING_GBP_M["under_general_needs"] + caseload = h._CASELOAD_THOUSANDS["under_general_needs"] + assert set(spending) == set(caseload) == {2024, 2025} + assert spending[2025] == 584.8 and caseload[2025] == 107 + for year in spending: + other_way = _DWP_GENERAL_NEEDS_GBP_M[year] - h._SPENDING_GBP_M["over"][year] + assert abs(spending[year] - other_way) <= 0.2, year + other_way_k = _DWP_GENERAL_NEEDS_K[year] - h._CASELOAD_THOUSANDS["over"][year] + assert abs(caseload[year] - other_way_k) <= 1, year + assert spending[year] < h._SPENDING_GBP_M["under"][year] + assert caseload[year] < h._CASELOAD_THOUSANDS["under"][year] + + +def test_net_working_age_targets_are_not_carried_forward(): + """The net figure is negative from 2026-27, so the loss matrix must not + reuse 2025-26's value for later years. Other HB targets still resolve.""" + targets = _targets() + for suffix in ("", "_claims"): + net = targets[ + f"dwp/housing_benefit/under_pension_credit_age_general_needs{suffix}" + ] + assert not net.carry_forward + assert _resolve_value(net, 2024) == net.values[2024] + assert _resolve_value(net, 2025) == net.values[2025] + for year in (2023, 2026, 2027, 2028): + assert _resolve_value(net, year) is None, year + for group in ("over", "under"): + target = targets[f"dwp/housing_benefit/{group}_pension_credit_age{suffix}"] + assert target.carry_forward + for year in (2026, 2027, 2028): + assert _resolve_value(target, year) == target.values[year] + assert _resolve_value(target, 2031) == target.values[2030] + + +@settings(max_examples=300, deadline=None) +@given( + st.dictionaries( + st.integers(2015, 2035), st.floats(-1e9, 1e9), min_size=1, max_size=8 + ), + st.integers(2010, 2040), + st.booleans(), +) +def test_resolve_value_carries_forward_only_when_allowed(values, year, carry): + target = Target( + name="test/target", + variable="housing_benefit", + source="test", + unit=Unit.GBP, + values=values, + carry_forward=carry, + ) + resolved = _resolve_value(target, year) + if year in values: + assert resolved == values[year] + return + # Nearest listed year; an earlier year wins a tie. + closest = min(sorted(values), key=lambda y: abs(y - year)) + if carry and closest < year and year - closest <= 3: + assert resolved == values[closest] + else: + assert resolved is None + + +@settings(max_examples=200, deadline=None) +@given( + st.dictionaries(st.integers(2020, 2035), st.floats(0, 1e4), max_size=8), + st.dictionaries(st.integers(2020, 2035), st.floats(0, 1e4), max_size=8), + st.dictionaries(st.integers(2020, 2035), st.floats(0, 1e4), max_size=8), +) +def test_net_of_supported_and_temporary(working_age, supported, temporary): + net = dwp_housing_benefit.net_of_supported_and_temporary( + working_age, supported, temporary + ) + common = working_age.keys() & supported.keys() & temporary.keys() + assert set(net) <= common + for year in common: + difference = working_age[year] - supported[year] - temporary[year] + if year in net: + assert net[year] > 0 + assert abs(net[year] - difference) <= 0.05 + 1e-9 + else: + assert round(difference, 1) <= 0 + + +def test_obr_housing_benefit_row_is_not_parsed(): + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "4.9" + ws["B2"] = "Housing benefit (not on JSA)1" + ws["B3"] = "Attendance allowance" + for row in (2, 3): + for col in "CDEFGHI": + ws[f"{col}{row}"] = 6.0 + names = {t.name for t in obr._parse_welfare(wb)} + assert "obr/attendance_allowance" in names + assert not any("housing_benefit" in name for name in names) + + +def _ctx( + benunit_of_person, + is_adult, + is_sp_age, + benunit_hb, + household_of_benunit, + benunit_benefits=None, +): + """Minimal loss-matrix context over explicit entity mappings. + + ``benunit_benefits`` maps benefit-unit variables such as + ``universal_credit`` to amounts; any other benefit is zero. + """ + benunit_of_person = np.asarray(benunit_of_person) + household_of_benunit = np.asarray(household_of_benunit) + n_benunits = len(benunit_hb) + n_households = household_of_benunit.max() + 1 if n_benunits else 0 + + def map_result(values, source, target): + values = np.asarray(values, dtype=float) + if (source, target) == ("person", "benunit"): + return np.bincount(benunit_of_person, values, minlength=n_benunits) + if (source, target) == ("benunit", "household"): + return np.bincount(household_of_benunit, values, minlength=n_households) + raise AssertionError((source, target)) + + person = {"is_adult": np.asarray(is_adult), "is_SP_age": np.asarray(is_sp_age)} + benunit_values = {"housing_benefit": benunit_hb, **(benunit_benefits or {})} + sim = SimpleNamespace( + tax_benefit_system=SimpleNamespace(variables={"is_adult": None}), + map_result=map_result, + calculate=lambda variable: SimpleNamespace( + values=np.asarray( + benunit_values.get(variable, np.zeros(n_benunits)), dtype=float + ) + ), + ) + return SimpleNamespace( + sim=sim, + pe_person=lambda variable: person[variable], + household_from_family=lambda values: map_result(values, "benunit", "household"), + ) + + +def _column(ctx, name): + target = _targets()[name] + return target.custom_compute(ctx, target, 2025) + + +def test_benefit_rules_assign_mixed_age_couples(): + # Benefit units, one household each: pensioner couple; mixed-age couple + # on pension-age rules; working-age single; pensioner with an + # 18-year-old dependant; mixed-age couple whose younger partner gets + # income-related ESA; mixed-age couple on Universal Credit. + ctx = _ctx( + benunit_of_person=[0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5], + is_adult=[1] * 11, + is_sp_age=[1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0], + benunit_hb=[5_000, 4_000, 3_000, 2_000, 1_000, 500], + household_of_benunit=[0, 1, 2, 3, 4, 5], + benunit_benefits={ + "esa_income": [0, 0, 0, 0, 4_000, 0], + "universal_credit": [0, 0, 0, 0, 0, 6_000], + }, + ) + over = _column(ctx, "dwp/housing_benefit/over_pension_credit_age") + under = _column(ctx, "dwp/housing_benefit/under_pension_credit_age") + np.testing.assert_array_equal(over, [5_000, 4_000, 0, 2_000, 0, 0]) + np.testing.assert_array_equal(under, [0, 0, 3_000, 0, 1_000, 500]) + + +@st.composite +def _population(draw): + n_benunits = draw(st.integers(1, 12)) + n_households = draw(st.integers(1, n_benunits)) + household_of_benunit = draw( + st.lists( + st.integers(0, n_households - 1), + min_size=n_benunits, + max_size=n_benunits, + ) + ) + n_people = draw(st.integers(n_benunits, 4 * n_benunits)) + # Every benefit unit has at least one member. + benunit_of_person = list(range(n_benunits)) + draw( + st.lists( + st.integers(0, n_benunits - 1), + min_size=n_people - n_benunits, + max_size=n_people - n_benunits, + ) + ) + flags = st.lists(st.booleans(), min_size=n_people, max_size=n_people) + benunit_hb = draw( + st.lists( + st.one_of(st.just(0.0), st.floats(1, 50_000)), + min_size=n_benunits, + max_size=n_benunits, + ) + ) + benefit = st.lists( + st.sampled_from([0.0, 100.0]), min_size=n_benunits, max_size=n_benunits + ) + return dict( + benunit_of_person=benunit_of_person, + is_adult=draw(flags), + is_sp_age=draw(flags), + benunit_hb=benunit_hb, + household_of_benunit=household_of_benunit, + benunit_benefits={ + name: draw(benefit) for name in dwp_housing_benefit._WORKING_AGE_BENEFITS + }, + ) + + +@settings(max_examples=200, deadline=None) +@given(_population()) +def test_age_split_partitions_housing_benefit(population): + """Over + under is all Housing Benefit, in pounds and in claims, and + the older group is exactly the units with an adult over SPA and no + working-age income benefit.""" + ctx = _ctx(**population) + household = np.asarray(population["household_of_benunit"]) + hb = np.asarray(population["benunit_hb"]) + n_households = household.max() + 1 + over = _column(ctx, "dwp/housing_benefit/over_pension_credit_age") + under = _column(ctx, "dwp/housing_benefit/under_pension_credit_age") + over_k = _column(ctx, "dwp/housing_benefit/over_pension_credit_age_claims") + under_k = _column(ctx, "dwp/housing_benefit/under_pension_credit_age_claims") + # The model has no supported or temporary accommodation rules, so the + # calibrated working-age group shares the full line's column. + np.testing.assert_array_equal( + _column(ctx, "dwp/housing_benefit/under_pension_credit_age_general_needs"), + under, + ) + np.testing.assert_array_equal( + _column( + ctx, "dwp/housing_benefit/under_pension_credit_age_general_needs_claims" + ), + under_k, + ) + np.testing.assert_allclose( + over + under, np.bincount(household, hb, minlength=n_households) + ) + np.testing.assert_array_equal( + over_k + under_k, np.bincount(household, hb > 0, minlength=n_households) + ) + people = pd.DataFrame( + { + "benunit": population["benunit_of_person"], + "older": np.asarray(population["is_adult"]) + & np.asarray(population["is_sp_age"]), + } + ) + older_unit = ( + people.groupby("benunit").older.any().reindex(range(len(hb)), fill_value=False) + ).to_numpy() + on_working_age_benefit = np.any( + [np.asarray(v) > 0 for v in population["benunit_benefits"].values()], axis=0 + ) + expected_over = np.bincount( + household, hb * (older_unit & ~on_working_age_benefit), minlength=n_households + ) + np.testing.assert_allclose(over, expected_over) + + +_COUNTRIES = ["ENGLAND", "SCOTLAND", "WALES", "NORTHERN_IRELAND"] + + +@settings(max_examples=200, deadline=None) +@given( + st.lists( + st.tuples(st.floats(-1e9, 1e9), st.sampled_from(_COUNTRIES)), + min_size=1, + max_size=50, + ) +) +def test_country_restriction_partitions_the_uk(rows): + column = np.array([value for value, _ in rows]) + country = np.array([c for _, c in rows]) + gb = restrict_to_countries(column, country, GREAT_BRITAIN) + ni = restrict_to_countries(column, country, ("NORTHERN_IRELAND",)) + np.testing.assert_allclose(gb + ni, column) + np.testing.assert_array_equal(gb[country == "NORTHERN_IRELAND"], 0) + np.testing.assert_array_equal(restrict_to_countries(gb, country, GREAT_BRITAIN), gb) + assert restrict_to_countries(column, country, None) is column + + +def test_calibrated_columns_build_on_the_dataset(baseline, enhanced_frs): + """The loss matrix skips a target whose column raises, so check that the + calibrated columns build on a real simulation and land in range. The + working-age range is wider: a dataset calibrated without its target + carries about a sixth of it (17k of 107k claims in 2025-26).""" + from policyengine_uk_data.targets.build_loss_matrix import _SimContext + + year = 2025 + baseline.default_calculation_period = str(year) + ctx = _SimContext(baseline, str(year), enhanced_frs, None) + weight = baseline.calculate("household_weight", year).values + for target in dwp_housing_benefit.get_targets(): + column = restrict_to_countries( + target.custom_compute(ctx, target, year), ctx.country, target.countries + ) + assert np.isfinite(column).all(), target.name + ratio = (column * weight).sum() / target.values[year] + low = 0.5 if "over_pension_credit_age" in target.name else 0.1 + assert low < ratio < 2, (target.name, ratio) diff --git a/policyengine_uk_data/tests/test_frs_employment_status.py b/policyengine_uk_data/tests/test_frs_employment_status.py new file mode 100644 index 000000000..c7ccae1e8 --- /dev/null +++ b/policyengine_uk_data/tests/test_frs_employment_status.py @@ -0,0 +1,211 @@ +"""FRS EMPSTATI codes to ``employment_status``, and what the ESA proxies read.""" + +import numpy as np +import pytest +from hypothesis import given, strategies as st +from policyengine_uk.variables.household.income.employment_status import ( + EmploymentStatus, +) + +from policyengine_uk_data.datasets.frs import ( + ESA_HEALTH_EMPLOYMENT_STATUSES, + ESA_MIN_AGE, + FRS_EMPSTATI_EMPLOYMENT_STATUS, + derive_employment_status_from_frs, + derive_esa_health_condition_proxy, + derive_esa_support_group_proxy, +) + +# EMPSTATI ("Adult - Employment Status - ILO definition") value labels in the +# UKDS FRS 2024-25 data dictionary (SN 9563, adult table), with the status each +# label means. Every adult in the 2020-21, 2022-23 and 2023-24 releases also +# has a code from 1 to 11. +DATA_DICTIONARY = { + 1: ("Full-time Employee", "FT_EMPLOYED"), + 2: ("Part-time Employee", "PT_EMPLOYED"), + 3: ("Full-time Self-Employed", "FT_SELF_EMPLOYED"), + 4: ("Part-time Self-Employed", "PT_SELF_EMPLOYED"), + 5: ("Unemployed", "UNEMPLOYED"), + 6: ("Retired", "RETIRED"), + 7: ("Student", "STUDENT"), + 8: ("Looking after family/home", "CARER"), + 9: ("Permanently sick/disabled", "LONG_TERM_DISABLED"), + 10: ("Temporarily sick/injured", "SHORT_TERM_DISABLED"), + 11: ("Other Inactive", "OTHER_INACTIVE"), +} +ADULT_CODES = sorted(DATA_DICTIONARY) +HEALTH_CODES = (9, 10) + +adult_rows = st.tuples(st.just(True), st.sampled_from(ADULT_CODES)) +# Child-table rows have no EMPSTATI (0 once the person table fills blanks); the +# code must not matter for them. +child_rows = st.tuples( + st.just(False), + st.one_of(st.just(0), st.just(np.nan), st.integers(-9, 99)), +) +people = st.lists(st.one_of(adult_rows, child_rows), max_size=60) +unknown_adult_codes = st.one_of( + st.just(np.nan), + st.integers(-99, 0), + st.integers(12, 999), + st.floats(0.5, 11.5).filter(lambda x: not float(x).is_integer()), +) + + +def derive(rows): + is_adult = [adult for adult, _ in rows] + codes = [code for _, code in rows] + return derive_employment_status_from_frs(codes, is_adult) + + +def expected(adult, code): + return DATA_DICTIONARY[code][1] if adult else "CHILD" + + +@pytest.mark.parametrize("code", range(12)) +def test_every_code_from_0_to_11(code): + assert derive_employment_status_from_frs([code], [False]).tolist() == ["CHILD"] + if code == 0: + with pytest.raises(ValueError, match="EMPSTATI"): + derive_employment_status_from_frs([code], [True]) + else: + label, status = DATA_DICTIONARY[code] + assert derive_employment_status_from_frs([code], [True]).tolist() == [status], ( + label + ) + + +def test_other_inactive_is_not_long_term_disabled(): + result = derive_employment_status_from_frs([9, 11], [True, True]) + assert result.tolist() == ["LONG_TERM_DISABLED", "OTHER_INACTIVE"] + + +def test_code_table_is_the_data_dictionary(): + assert FRS_EMPSTATI_EMPLOYMENT_STATUS == { + code: status for code, (_, status) in DATA_DICTIONARY.items() + } + + +def test_adult_codes_and_child_rows_cover_each_status_once(): + statuses = [*FRS_EMPSTATI_EMPLOYMENT_STATUS.values(), "CHILD"] + assert len(set(statuses)) == len(statuses) + assert set(statuses) == set(EmploymentStatus.__members__) + + +@pytest.mark.parametrize("code", [0, -1, 12, 11.5, np.nan]) +def test_unknown_adult_code_fails_the_build(code): + with pytest.raises(ValueError, match="EMPSTATI"): + derive_employment_status_from_frs([1, code], [True, True]) + + +def listed_codes(message): + return message.split("FRS_EMPSTATI_EMPLOYMENT_STATUS: ")[1].split(". Map")[0] + + +def test_failure_message_lists_codes_in_numeric_order_with_blank_last(): + # Formatted from the float codes, not Series.astype(str), whose NaN + # handling differs between pandas 2 and 3. A string sort would put 100 + # before 11.5. + codes = [100, 12, np.nan, -1, 12, 11.5, 1] + with pytest.raises(ValueError) as error: + derive_employment_status_from_frs(codes, [True] * len(codes)) + assert listed_codes(str(error.value)) == "-1, 11.5, 12, 100, blank" + + +@given(st.lists(unknown_adult_codes, min_size=1, max_size=20)) +def test_failure_message_lists_each_unknown_code_once_in_order(bad_codes): + with pytest.raises(ValueError) as error: + derive_employment_status_from_frs(bad_codes, [True] * len(bad_codes)) + listed = listed_codes(str(error.value)).split(", ") + blank = any(np.isnan(code) for code in bad_codes) + assert (listed[-1] == "blank") == blank + numbers = [float(code) for code in listed[: len(listed) - blank]] + assert numbers == sorted(numbers) + assert set(listed) - {"blank"} == { + f"{code:g}" for code in bad_codes if not np.isnan(code) + } + + +@given(st.integers(1, 30), st.integers(1, 30)) +def test_failure_message_discloses_no_count(n_twelve, n_thirteen): + # Adults are not survey households, so no count of them is safe to print + # in a public build log: the message depends only on which codes occur. + codes = [12] * n_twelve + [13] * n_thirteen + with pytest.raises(ValueError) as error: + derive_employment_status_from_frs(codes, [True] * len(codes)) + message = str(error.value) + assert listed_codes(message) == "12, 13" + assert not any(character.isdigit() for character in message.replace("12, 13", "")) + + +@given(people) +def test_each_row_maps_on_its_own(rows): + result = derive(rows) + assert result.tolist() == [expected(adult, code) for adult, code in rows] + + +@given(people, st.randoms(use_true_random=False)) +def test_mapping_commutes_with_row_order(rows, rng): + order = list(range(len(rows))) + rng.shuffle(order) + statuses = derive(rows) + assert derive([rows[i] for i in order]).tolist() == [statuses[i] for i in order] + + +@given(people, unknown_adult_codes, st.integers(0, 60)) +def test_any_unknown_adult_code_fails_the_build(rows, bad_code, position): + rows = list(rows) + rows.insert(min(position, len(rows)), (True, bad_code)) + with pytest.raises(ValueError, match="EMPSTATI"): + derive(rows) + + +@given( + st.lists( + st.tuples( + st.integers(0, 100), # age + st.sampled_from(ADULT_CODES), + st.integers(60, 68), # State Pension age + st.integers(0, 3_000), # annual hours worked + st.booleans(), # EMPSTATI reported + ), + min_size=1, + max_size=60, + ) +) +def test_esa_proxies_read_only_the_sick_or_disabled_codes(rows): + age, codes, spa, hours, reported = map(np.array, zip(*rows)) + status = derive_employment_status_from_frs(codes, np.ones(len(rows), bool)) + health = derive_esa_health_condition_proxy( + age=age, + employment_status=status, + employment_status_reported=reported, + state_pension_age=spa, + ) + support = derive_esa_support_group_proxy( + age=age, + employment_status=status, + hours_worked=hours, + esa_health_condition_proxy=health, + employment_status_reported=reported, + state_pension_age=spa, + ) + working_age = (age >= ESA_MIN_AGE) & (age < spa) + assert ( + health.tolist() + == (reported & working_age & np.isin(codes, HEALTH_CODES)).tolist() + ) + assert not (support & ~health).any() + assert not support[codes != 9].any() + assert not health[codes == 11].any() + + +@pytest.mark.parametrize("dataset_name", ["frs", "enhanced_frs"]) +def test_built_dataset_statuses(dataset_name, request): + person = request.getfixturevalue(dataset_name).person + status = person["employment_status"].astype(str) + assert set(status) <= set(EmploymentStatus.__members__) + assert (status == "OTHER_INACTIVE").any() + not_sick = ~status.isin(ESA_HEALTH_EMPLOYMENT_STATUSES) + assert not person.loc[not_sick, "esa_health_condition_proxy"].any() + assert not person.loc[not_sick, "esa_support_group_proxy"].any() diff --git a/policyengine_uk_data/tests/test_frs_only_imputation.py b/policyengine_uk_data/tests/test_frs_only_imputation.py index 1961274e4..17ba59fd1 100644 --- a/policyengine_uk_data/tests/test_frs_only_imputation.py +++ b/policyengine_uk_data/tests/test_frs_only_imputation.py @@ -178,25 +178,26 @@ def test_frs_only_skips_missing_output_columns(): def test_frs_only_reported_values_correlate_with_training_pattern(): - """UC ``_reported`` predictions should respect the training-data pattern. + """Drawn ``_reported`` values should respect the training-data pattern. The stage-1 QRF-imputed income on the SPI-donor side gets fed back - as a stage-2 predictor. If the training data only has non-zero UC - for low-income respondents, the QRF should preferentially draw - near-zero values when predicting for high-income target rows, - compared with low-income target rows. + as a stage-2 predictor. If the training data only has non-zero + carer's allowance for low earners (it has an earnings limit), the QRF + should preferentially draw near-zero values when predicting for + high-income target rows, compared with low-income target rows. (UC + would be the obvious case, but SPI-donor rows have it zeroed.) """ from policyengine_uk_data.datasets.imputations.frs_only import ( impute_frs_only_variables, ) - # Train set with a clean employment-income → UC relationship: - # low-income respondents sometimes claim UC, high-income never do. + # Train set with a clean employment-income → carer's allowance relationship: + # low earners sometimes receive it, high earners never do. rng = np.random.default_rng(42) train = _fake_dataset(person_rows=2_000, seed=0) low_income_mask = train.person["employment_income"] < 20_000 - train.person["universal_credit_reported"] = 0.0 - train.person.loc[low_income_mask, "universal_credit_reported"] = rng.gamma( + train.person["carers_allowance_reported"] = 0.0 + train.person.loc[low_income_mask, "carers_allowance_reported"] = rng.gamma( 2, 4_000, size=int(low_income_mask.sum()) ) @@ -215,10 +216,10 @@ def test_frs_only_reported_values_correlate_with_training_pattern(): target_dataset=low_target, ) - high_mean = high_result.person["universal_credit_reported"].mean() - low_mean = low_result.person["universal_credit_reported"].mean() + high_mean = high_result.person["carers_allowance_reported"].mean() + low_mean = low_result.person["carers_allowance_reported"].mean() assert high_mean < low_mean, ( - "Stage-2 QRF should produce lower UC-receipt predictions for high-" + "Stage-2 QRF should produce lower carer's allowance predictions for high-" f"income target rows (got high={high_mean:.2f} vs low={low_mean:.2f})." ) diff --git a/policyengine_uk_data/tests/test_frs_property_income.py b/policyengine_uk_data/tests/test_frs_property_income.py new file mode 100644 index 000000000..5fb2982c5 --- /dev/null +++ b/policyengine_uk_data/tests/test_frs_property_income.py @@ -0,0 +1,301 @@ +import numpy as np +import pandas as pd +import pytest + +from policyengine_uk_data.datasets.frs import ( + FRS_RENTPROF_LOSS, + WEEKS_IN_YEAR, + frs_property_income, +) +from policyengine_uk_data.datasets.frs_release import CURRENT_FRS_RELEASE +from policyengine_uk_data.storage import STORAGE_FOLDER + +HRP, NOT_HRP = 1, 2 +NOT_ASKED, PROFIT, LOSS = 0, 1, FRS_RENTPROF_LOSS +# FRS missing-value codes run from -1 to -9. +MISSING, REFUSED = -1, -9 +OWNED_WITH_MORTGAGE, OWNED_OUTRIGHT = 5, 6 +COUNCIL_RENTED, PRIVATE_RENTED_FURNISHED = 1, 4 +TENURES = range(1, 9) +BEFORE_EXPENSES, AFTER_EXPENSES = 1, 2 + + +def make_tables(people, households): + person = pd.DataFrame( + people, + columns=["household_id", "person_id", "hrpid", "royyr1", "rentprof", "cvpay"], + ) + household = pd.DataFrame( + households, columns=["household_id", "tentyp2", "subrent", "suballow"] + ).set_index("household_id") + return person, household + + +def test_rent_paid_by_a_lodger_is_not_their_property_income(): + # Owner-occupier household with a lodger who pays £100 a week (CVPAY). + person, household = make_tables( + [(1, 1_001, HRP, 0, NOT_ASKED, 0), (1, 1_002, NOT_HRP, 0, NOT_ASKED, 100)], + [(1, OWNED_OUTRIGHT, 0, 0)], + ) + assert frs_property_income(person, household).tolist() == [0, 0] + + +def test_rent_from_other_property_counts_for_any_adult(): + person, household = make_tables( + [(1, 1_001, HRP, 0, NOT_ASKED, 0), (1, 1_002, NOT_HRP, 50, PROFIT, 0)], + [(1, COUNCIL_RENTED, 0, 0)], + ) + np.testing.assert_allclose( + frs_property_income(person, household), [0, 50 * WEEKS_IN_YEAR] + ) + + +def test_a_loss_on_other_property_is_not_income(): + # ROYYR1 holds the size of the loss as a positive amount; RENTPROF flags it. + person, household = make_tables( + [(1, 1_001, HRP, 90, LOSS, 0), (1, 1_002, NOT_HRP, 50, PROFIT, 0)], + [(1, OWNED_WITH_MORTGAGE, 0, 0)], + ) + np.testing.assert_allclose( + frs_property_income(person, household), [0, 50 * WEEKS_IN_YEAR] + ) + + +def test_a_loss_on_other_property_does_not_reduce_subletting_rent(): + person, household = make_tables( + [(1, 1_001, HRP, 30, LOSS, 0)], + [(1, OWNED_OUTRIGHT, 80, AFTER_EXPENSES)], + ) + np.testing.assert_allclose( + frs_property_income(person, household), [80 * WEEKS_IN_YEAR] + ) + + +def test_rent_with_no_profit_or_loss_answer_still_counts(): + # Only an explicit loss removes the amount. + person, household = make_tables( + [(1, 1_001, HRP, 50, NOT_ASKED, 0)], [(1, OWNED_OUTRIGHT, 0, 0)] + ) + np.testing.assert_allclose( + frs_property_income(person, household), [50 * WEEKS_IN_YEAR] + ) + + +@pytest.mark.parametrize("tenure", TENURES) +def test_subletting_rent_goes_to_the_household_reference_person(tenure): + # Every tenure is asked SubLet, so renting households count too. + person, household = make_tables( + [(1, 1_001, NOT_HRP, 0, NOT_ASKED, 0), (1, 1_002, HRP, 0, NOT_ASKED, 0)], + [(1, tenure, 80, BEFORE_EXPENSES)], + ) + np.testing.assert_allclose( + frs_property_income(person, household), [0, 80 * WEEKS_IN_YEAR] + ) + + +@pytest.mark.parametrize("suballow", [0, BEFORE_EXPENSES, AFTER_EXPENSES]) +def test_subletting_rent_is_used_as_reported_whatever_its_expenses_basis(suballow): + person, household = make_tables( + [(1, 1_001, HRP, 0, NOT_ASKED, 0)], + [(1, PRIVATE_RENTED_FURNISHED, 80, suballow)], + ) + np.testing.assert_allclose( + frs_property_income(person, household), [80 * WEEKS_IN_YEAR] + ) + + +def test_missing_value_codes_count_as_zero(): + # A missing code in one amount neither becomes income nor cancels the other. + person, household = make_tables( + [ + (1, 1_001, HRP, MISSING, MISSING, 0), + (1, 1_002, NOT_HRP, REFUSED, MISSING, 0), + (2, 2_001, HRP, 50, PROFIT, 0), + ], + [(1, COUNCIL_RENTED, 10, AFTER_EXPENSES), (2, OWNED_OUTRIGHT, MISSING, 0)], + ) + np.testing.assert_allclose( + frs_property_income(person, household), + [10 * WEEKS_IN_YEAR, 0, 50 * WEEKS_IN_YEAR], + ) + + +def test_adult_and_child_rows_sharing_index_labels(): + # create_frs stacks the adult and child tables, so index labels repeat. + adults, household = make_tables( + [(1, 1_001, HRP, 20, PROFIT, 0), (2, 2_001, HRP, 40, LOSS, 60)], + [ + (1, OWNED_OUTRIGHT, 80, BEFORE_EXPENSES), + (2, PRIVATE_RENTED_FURNISHED, 30, AFTER_EXPENSES), + ], + ) + children, _ = make_tables([(1, 1_002, 0, 0, NOT_ASKED, 0)], []) + person = pd.concat([adults, children]).sort_index(kind="stable") + assert person.index.tolist() == [0, 0, 1] + np.testing.assert_allclose( + frs_property_income(person, household), + [100 * WEEKS_IN_YEAR, 0, 30 * WEEKS_IN_YEAR], + ) + + +def random_tables(seed: int): + """Random households of one to four adults; the first is the HRP.""" + rng = np.random.default_rng(seed) + people, households = [], [] + for household_id in range(1, rng.integers(1, 30) + 1): + tenure = int(rng.integers(1, 9)) + subrent = float(rng.choice([0, MISSING, rng.uniform(0, 500)])) + suballow = int(rng.integers(1, 3)) if subrent > 0 else 0 + households.append((household_id, tenure, subrent, suballow)) + for person in range(1, rng.integers(1, 5) + 1): + royyr1 = float(rng.choice([0, MISSING, rng.uniform(0, 2_000)])) + if royyr1 > 0: + rentprof = int(rng.choice([PROFIT, PROFIT, LOSS])) + else: + rentprof = MISSING if royyr1 < 0 else NOT_ASKED + people.append( + ( + household_id, + household_id * 1_000 + person, + HRP if person == 1 else NOT_HRP, + royyr1, + rentprof, + float(rng.choice([0, rng.uniform(0, 400)])), + ) + ) + return make_tables(people, households) + + +def property_income_one_person_at_a_time(person, household): + """The same rules written as a loop, to check the vectorised helper.""" + result = [] + for row in person.itertuples(): + weekly = 0.0 + if row.rentprof != LOSS: + weekly += max(0.0, row.royyr1) + if row.hrpid == HRP and row.household_id in household.index: + weekly += max(0.0, household.subrent[row.household_id]) + result.append(weekly * WEEKS_IN_YEAR) + return np.array(result) + + +SEEDS = range(200) + + +@pytest.mark.parametrize("seed", SEEDS) +def test_property_income_matches_the_loop_version(seed): + person, household = random_tables(seed) + np.testing.assert_allclose( + frs_property_income(person, household), + property_income_one_person_at_a_time(person, household), + ) + + +@pytest.mark.parametrize("seed", SEEDS) +def test_property_income_does_not_depend_on_cvpay_tenure_or_expenses_basis(seed): + person, household = random_tables(seed) + rng = np.random.default_rng(seed) + other_person = person.assign(cvpay=rng.uniform(0, 400, len(person))) + other_household = household.assign( + tentyp2=rng.integers(1, 9, len(household)), + suballow=rng.integers(1, 3, len(household)), + ) + np.testing.assert_array_equal( + frs_property_income(person, household), + frs_property_income(other_person, other_household), + ) + + +@pytest.mark.parametrize("seed", SEEDS) +def test_property_income_conserves_reported_rent(seed): + # Each household's SUBRENT is counted once (on its HRP) and every ROYYR1 + # that is not a loss is counted on its own record, so the totals match. + person, household = random_tables(seed) + result = frs_property_income(person, household) + assert (result >= 0).all() + profits = person.royyr1.clip(lower=0)[person.rentprof != LOSS] + subrent = household.subrent.clip(lower=0) + expected = (subrent.sum() + profits.sum()) * WEEKS_IN_YEAR + assert result.sum() == pytest.approx(expected) + + +@pytest.mark.parametrize("seed", SEEDS) +def test_marking_rent_as_a_loss_removes_it_from_that_person_only(seed): + person, household = random_tables(seed) + before = frs_property_income(person, household) + row = seed % len(person) + was_counted = person.loc[row, "rentprof"] != LOSS + person.loc[row, "rentprof"] = LOSS + after = frs_property_income(person, household) + change = np.zeros(len(person)) + change[row] = -max(0, person.loc[row, "royyr1"]) * WEEKS_IN_YEAR * was_counted + np.testing.assert_allclose(after - before, change, atol=1e-6) + + +@pytest.mark.parametrize("seed", SEEDS) +def test_extra_rent_from_other_property_moves_only_that_person(seed): + person, household = random_tables(seed) + before = frs_property_income(person, household) + row = seed % len(person) + person.loc[row, "royyr1"] = max(0, person.loc[row, "royyr1"]) + 10 + after = frs_property_income(person, household) + change = np.zeros(len(person)) + change[row] = 10 * WEEKS_IN_YEAR * (person.loc[row, "rentprof"] != LOSS) + np.testing.assert_allclose(after - before, change, atol=1e-6) + + +@pytest.mark.parametrize("seed", SEEDS) +def test_extra_subletting_rent_moves_only_that_households_reference_person(seed): + person, household = random_tables(seed) + before = frs_property_income(person, household) + household_id = household.index[seed % len(household)] + household.loc[household_id, "subrent"] = ( + max(0, household.loc[household_id, "subrent"]) + 10 + ) + after = frs_property_income(person, household) + is_its_hrp = (person.household_id == household_id) & (person.hrpid == HRP) + np.testing.assert_allclose( + after - before, is_its_hrp * 10 * WEEKS_IN_YEAR, atol=1e-6 + ) + + +def test_built_frs_matches_the_raw_tables(frs): + """The built base FRS against a merge-based reading of the raw tables. + + Covers the call site in ``create_frs`` and the raw column names and codes. + The assertion carries no values, because the dataset is licensed microdata. + """ + raw_folder = STORAGE_FOLDER / CURRENT_FRS_RELEASE.name + if not (raw_folder / "adult.tab").exists(): + pytest.skip("Raw FRS tables not available") + adult = pd.read_csv( + raw_folder / "adult.tab", + sep="\t", + usecols=lambda c: ( + c.upper() in ("SERNUM", "PERSON", "HRPID", "ROYYR1", "RENTPROF") + ), + ) + househol = pd.read_csv( + raw_folder / "househol.tab", + sep="\t", + usecols=lambda c: c.upper() in ("SERNUM", "SUBRENT"), + ) + adult.columns = adult.columns.str.upper() + househol.columns = househol.columns.str.upper() + raw = adult.merge(househol, on="SERNUM", how="left").apply( + pd.to_numeric, errors="coerce" + ) + profit = raw.ROYYR1.clip(lower=0).where(raw.RENTPROF != 2, 0).fillna(0) + subrent = raw.SUBRENT.clip(lower=0).where(raw.HRPID == 1, 0).fillna(0) + expected = pd.Series( + ((profit + subrent) * WEEKS_IN_YEAR).values, + index=(raw.SERNUM * 1_000 + raw.PERSON).astype(int), + ) + built = pd.Series( + frs.person.property_income.values, index=frs.person.person_id.values + ) + adults_match = np.allclose(built.reindex(expected.index), expected, atol=0.01) + children_have_none = (built.drop(expected.index) == 0).all() + assert adults_match and children_have_none, ( + "property_income in the built FRS differs from the raw FRS tables" + ) diff --git a/policyengine_uk_data/tests/test_hmrc_salary_sacrifice_targets.py b/policyengine_uk_data/tests/test_hmrc_salary_sacrifice_targets.py new file mode 100644 index 000000000..ec01d2801 --- /dev/null +++ b/policyengine_uk_data/tests/test_hmrc_salary_sacrifice_targets.py @@ -0,0 +1,293 @@ +"""Tests for the HMRC salary sacrifice relief targets (Tables 6.1 and 6.2). + +The July 2025 CSV returned 410 Gone once HMRC published the July 2026 +release, and the source module logged the error and returned without the +relief targets, so builds calibrated without them. These tests pin: the +committed table parses to every relief target, a failed download uses it, a +changed table raises, the rate-band columns measure relief the way HMRC does +(tax on pay, relieved at each rate a contribution straddles), and on a built +dataset every relief target produces a loss-matrix column. +""" + +from unittest.mock import patch + +import numpy as np +import pandas as pd +import pytest +import requests + +from policyengine_uk_data.datasets.frs_release import CURRENT_FRS_RELEASE +from policyengine_uk_data.targets.build_loss_matrix import ( + _compute_column, + _resolve_value, + _SimContext, +) +from policyengine_uk_data.targets.compute.income import ( + compute_ss_it_relief, + tax_by_band, +) +from policyengine_uk_data.targets.schema import Target, Unit +from policyengine_uk_data.targets.sources import hmrc_salary_sacrifice as hmrc_ss + +# Tables 6.1 and 6.2, tax year 2024-25 (£m), from the committed CSV. +RELIEF_2024_25 = { + "hmrc/salary_sacrifice_it_relief_basic_rate": 1_600, + "hmrc/salary_sacrifice_it_relief_higher_rate": 5_500, + "hmrc/salary_sacrifice_it_relief_additional_rate": 1_800, + "hmrc/salary_sacrifice_employee_nics_relief": 1_000, + "hmrc/salary_sacrifice_employer_nics_relief": 3_400, +} +# Class 1 secondary (employer) rate: 13.8% in 2024-25, 15% from 6 April 2025 +# (National Insurance Contributions (Secondary Class 1 Contributions) Act +# 2025). The primary rates (8% main, 2% additional) did not change. +EMPLOYER_RATE_RISE = 0.15 / 0.138 + + +def _committed_table() -> pd.DataFrame: + return pd.read_csv(hmrc_ss.FALLBACK_CSV, dtype=str, encoding="utf-8-sig") + + +def _offline(*args, **kwargs): + raise requests.ConnectionError("offline") + + +def _offline_targets() -> list[Target]: + with patch.object(hmrc_ss.requests, "get", side_effect=_offline): + return hmrc_ss.get_targets() + + +def test_committed_table_gives_every_relief_target(): + targets = {t.name: t for t in hmrc_ss._relief_targets(_committed_table(), "x")} + assert set(targets) == set(RELIEF_2024_25) + for name, millions in RELIEF_2024_25.items(): + values = targets[name].values + rate_rise = EMPLOYER_RATE_RISE if "employer" in name else 1 + # Tax year 2024-25 is PolicyEngine year 2024. + assert min(values) == 2024 + assert values[2024] == pytest.approx(millions * 1e6) + assert values[2025] == pytest.approx(millions * 1e6 * 1.03 * rate_rise) + assert values[2026] == pytest.approx(millions * 1e6 * 1.03**2 * rate_rise) + assert targets["hmrc/salary_sacrifice_employee_nics_relief"].variable == ( + "ni_employee" + ) + assert targets["hmrc/salary_sacrifice_employer_nics_relief"].variable == ( + "ni_employer" + ) + + +def test_fallback_file_matches_the_tax_year_it_is_checked_against(): + assert list(_committed_table()["tax_year"].unique()) == [ + f"{hmrc_ss._TAX_YEAR} to {hmrc_ss._TAX_YEAR + 1}" + ] + + +def _gone(*args, **kwargs): + response = requests.Response() + response.status_code = 410 + response.url = args[0] + return response + + +@pytest.mark.parametrize("get", [_gone, _offline], ids=["410", "offline"]) +def test_failed_download_uses_committed_table(get): + with patch.object(hmrc_ss.requests, "get", side_effect=get): + names = {t.name for t in hmrc_ss.get_targets()} + assert set(RELIEF_2024_25) <= names + assert "hmrc/salary_sacrifice_contributions" in names + + +@pytest.mark.parametrize( + "change", + [ + lambda df: df[df["tax_rate"] != "Higher Rate"], + lambda df: df[df["nics_relief_class"] != "Class 1 Secondary (employer)"], + lambda df: df[ + ~( + (df["nics_relief_class"] == "Class 1 Secondary (employer)") + & (df["tax_rate"] == "Main Rate") + ) + ], + lambda df: pd.concat( + [df.iloc[:-1], df.iloc[-1:].assign(tax_year="2025 to 2026")] + ), + lambda df: df.assign(tax_year="2025 to 2026"), + lambda df: df.drop(columns="sector_scheme"), + lambda df: df.assign( + value_of_relief=df["value_of_relief"].where(df["tax_rate"] != "Basic Rate") + ), + ], + ids=[ + "no-higher-rate", + "no-employer-nics", + "no-employer-rate-split", + "two-years", + "next-release", + "no-column", + "blank-cell", + ], +) +def test_changed_table_raises(change): + with pytest.raises((ValueError, KeyError)): + hmrc_ss._relief_targets(change(_committed_table()), "x") + + +def _scales(year): + from policyengine_uk import CountryTaxBenefitSystem + + rates = CountryTaxBenefitSystem().parameters(year).gov.hmrc.income_tax.rates + return {"uk": rates.uk, "scotland": rates.scotland.rates} + + +@pytest.mark.parametrize("year", ["2023", "2024", "2025"]) +@pytest.mark.parametrize("schedule", ["uk", "scotland"]) +def test_tax_by_band_adds_up_to_the_schedule_and_rises_with_income(year, schedule): + """Differential against PolicyEngine's own scale, plus monotonicity.""" + scale = _scales(year)[schedule] + rng = np.random.default_rng(0) + income = np.concatenate( + [rng.uniform(-1_000, 200_000, 5_000), np.asarray(scale.thresholds[1:])] + ) + bands = tax_by_band(income, scale.thresholds, scale.rates) + np.testing.assert_allclose(sum(bands.values()), scale.calc(income), atol=1e-6) + more = tax_by_band( + income + rng.uniform(0, 20_000, income.size), scale.thresholds, scale.rates + ) + for band in bands: + assert np.all(more[band] >= bands[band] - 1e-9) + + +def test_tax_by_band_groups_scottish_rates_into_hmrc_categories(): + """Starter, basic and intermediate → basic; higher and advanced → higher; + top → additional. Thresholds come from PolicyEngine's 2025 schedule + (policyengine-uk#2130: its top-rate threshold is £112,570 rather than + the statutory £125,140).""" + scale = _scales("2025")["scotland"] + (_, _, _, higher, advanced, top) = scale.thresholds + assert list(scale.rates) == [0.19, 0.20, 0.21, 0.42, 0.45, 0.48] + income = np.array( + [higher - 1, (higher + advanced) / 2, (advanced + top) / 2, top + 10_000] + ) + bands = tax_by_band(income, scale.thresholds, scale.rates) + assert bands["higher"][0] == 0 and bands["additional"][0] == 0 + assert bands["higher"][1] == pytest.approx(0.42 * (income[1] - higher)) + # The advanced rate counts as higher rate. + assert bands["higher"][2] == pytest.approx( + 0.42 * (advanced - higher) + 0.45 * (income[2] - advanced) + ) + assert bands["additional"][2] == 0 + assert bands["additional"][3] == pytest.approx(0.48 * 10_000) + + +class _Ctx: + """The parts of build_loss_matrix._SimContext the compute function reads.""" + + time_period = 2025 + + def __init__(self, base_pay, sacrifice, region="LONDON", dividends=0): + from policyengine_uk import Simulation + + def sim(pay): + return Simulation( + situation={ + "people": { + "a": { + "age": {2025: 40}, + "employment_income": {2025: pay}, + "dividend_income": {2025: dividends}, + } + }, + "benunits": {"b": {"members": ["a"]}}, + "households": {"h": {"members": ["a"], "region": {2025: region}}}, + } + ) + + # The counterfactual adds the sacrifice back to pay, as + # _SimContext.counterfactual_sim does. + self.sim = sim(base_pay) + self.counterfactual_sim = sim(base_pay + sacrifice) + + @staticmethod + def household_from_person(values): + return np.asarray(values) + + +def _relief(ctx, band): + target = Target( + name=f"hmrc/salary_sacrifice_it_relief_{band}_rate", + variable="income_tax", + source="hmrc", + unit=Unit.GBP, + values={2025: 1.0}, + ) + return float(compute_ss_it_relief(target, ctx)[0]) + + +@pytest.mark.parametrize( + "base_pay, sacrifice, region, expected", + [ + # rUK, £48k after a £4k sacrifice: taxable income £35,430 → £39,430, + # 40% only above £37,700. + (48_000, 4_000, "LONDON", {"basic": 454, "higher": 692, "additional": 0}), + # Scotland, £42k after £4k: taxable £29,430 → £33,430, 21% to £31,092 + # (basic category), 42% above. + ( + 42_000, + 4_000, + "SCOTLAND", + {"basic": 349.02, "higher": 981.96, "additional": 0}, + ), + # rUK, £100k after £10k: £5k of personal allowance is withdrawn, so + # relief is 60% of the sacrifice, all in the higher band. + (100_000, 10_000, "LONDON", {"basic": 0, "higher": 6_000, "additional": 0}), + ], + ids=["ruk-straddle", "scotland-straddle", "allowance-taper"], +) +def test_compute_relieves_each_slice_at_its_rate(base_pay, sacrifice, region, expected): + ctx = _Ctx(base_pay, sacrifice, region) + by_band = {band: _relief(ctx, band) for band in expected} + assert by_band == pytest.approx(expected, abs=0.01) + # With pay as the only income, the bands add up to the fall in income tax. + income_tax = [ + float(s.calculate("income_tax", 2025)[0]) + for s in (ctx.counterfactual_sim, ctx.sim) + ] + assert sum(by_band.values()) == pytest.approx( + income_tax[0] - income_tax[1], abs=0.01 + ) + + +def test_relief_targets_produce_loss_matrix_columns(enhanced_frs): + """create_target_matrix skips a target, with only a warning, when + _resolve_value or _compute_column raises or returns None. On the built + dataset neither may happen for these targets.""" + from policyengine_uk import Microsimulation + + year = CURRENT_FRS_RELEASE.calibration_year + sim = Microsimulation(dataset=enhanced_frs) + sim.default_calculation_period = year + ctx = _SimContext(sim, year, enhanced_frs, None) + weights = np.asarray(sim.calculate("household_weight", year)) + targets = [t for t in _offline_targets() if t.name in RELIEF_2024_25] + assert len(targets) == len(RELIEF_2024_25) + for target in targets: + assert _resolve_value(target, year) is not None, target.name + column = np.asarray(_compute_column(target, ctx, year), dtype=float) + assert np.isfinite(column).all(), target.name + assert column @ weights > 0, target.name + + +def test_relief_is_tax_on_pay_not_on_other_income(): + """HMRC applies income tax rates to pay. Paying the sacrifice as salary + also moves the dividends that were taxed in the basic rate band into the + higher dividend band; that extra dividend tax is not salary sacrifice + relief.""" + ctx = _Ctx(48_000, 4_000, dividends=10_000) + by_band = {band: _relief(ctx, band) for band in ("basic", "higher", "additional")} + assert by_band == pytest.approx( + {"basic": 454, "higher": 692, "additional": 0}, abs=0.01 + ) + income_tax = [ + float(s.calculate("income_tax", 2025)[0]) + for s in (ctx.counterfactual_sim, ctx.sim) + ] + assert income_tax[0] - income_tax[1] > sum(by_band.values()) + 100 diff --git a/policyengine_uk_data/tests/test_legacy_benefit_proxies.py b/policyengine_uk_data/tests/test_legacy_benefit_proxies.py index 5f1acd85f..64bf5670a 100644 --- a/policyengine_uk_data/tests/test_legacy_benefit_proxies.py +++ b/policyengine_uk_data/tests/test_legacy_benefit_proxies.py @@ -1,9 +1,11 @@ import numpy as np import pandas as pd +import pytest import policyengine_uk import policyengine_uk_data.datasets.frs as frs_module from policyengine_uk_data.datasets.frs import ( + FRS_EMPSTATI_EMPLOYMENT_STATUS, add_legacy_benefit_proxies, attach_legacy_benefit_proxies_from_frs_person, apply_legacy_benefit_proxies, @@ -299,6 +301,7 @@ def __init__(self, dataset): "FakeTaxBenefitSystem", (), { + "variables": {"is_adult": None}, "parameters": lambda self, year: type( "FakeParametersRoot", (), @@ -349,7 +352,7 @@ def __init__(self, dataset): }, )() }, - )() + )(), }, )() @@ -359,7 +362,13 @@ def calculate(self, variable, year=None): if variable == "household_id": return np.array([100]) if variable == "state_pension_age": - return pd.Series([66]) + return pd.Series([66] * len(self.dataset.person)) + if variable == "person_benunit_id": + return pd.Series(self.dataset.person.person_benunit_id.values) + if variable == "is_adult": + return pd.Series(self.dataset.person.age.values >= 18) + if variable == "is_SP_age": + return pd.Series(self.dataset.person.age.values >= 66) if variable in ( "childcare_grant", "parents_learning_allowance", @@ -373,18 +382,8 @@ def calculate(self, variable, year=None): raise KeyError(variable) -def test_create_frs_smoke_includes_legacy_proxy_columns(tmp_path, monkeypatch): - original_read_csv = frs_module.pd.read_csv - - def fake_read_csv(path, *args, **kwargs): - if str(path).endswith("lha_list_of_rents.csv.gz"): - return pd.DataFrame( - {"region": ["LONDON"], "lha_category": ["A"], "brma": ["BRMA1"]} - ) - return original_read_csv(path, *args, **kwargs) - +def create_single_adult_frs(tmp_path, monkeypatch, empstati=8, with_child=False): monkeypatch.setattr(policyengine_uk, "Microsimulation", FakeMicrosimulation) - monkeypatch.setattr(frs_module.pd, "read_csv", fake_read_csv) monkeypatch.setattr(frs_module, "load_take_up_rate", lambda *args, **kwargs: 0.0) monkeypatch.setattr(frs_module, "load_parameter", lambda *args, **kwargs: 0.0) monkeypatch.setattr( @@ -426,7 +425,7 @@ def fake_read_csv(path, *args, **kwargs): "educqual": 0, "eduma": 0, "edumaamt": 0, - "empstati": 8, + "empstati": empstati, "mjobsect": 0, "sic": 0, "fsbval": 0, @@ -442,6 +441,7 @@ def fake_read_csv(path, *args, **kwargs): "mntus1": 0, "mntusam1": 0, "redamt": 0, + "rentprof": 0, "royyr1": 0, "seincam2": 0, "sex": 1, @@ -468,7 +468,13 @@ def fake_read_csv(path, *args, **kwargs): } ] ) - child = pd.DataFrame(columns=adult.columns) + # The FRS child table has no EMPSTATI column. + child_columns = adult.columns.drop("empstati") + child = pd.DataFrame(columns=child_columns) + if with_child: + child = pd.DataFrame( + [{**dict.fromkeys(child_columns, 0), "sernum": 100, "benunit": 1}] + ).assign(person=2, age=5, uperson=2) benunit = pd.DataFrame([{"sernum": 100, "benunit": 1, "famtypb2": 1}]) househol = pd.DataFrame( [ @@ -536,7 +542,11 @@ def fake_read_csv(path, *args, **kwargs): for name, table in raw_tables.items(): table.to_csv(tmp_path / f"{name}.tab", sep="\t", index=False) - dataset = create_frs(tmp_path, 2025) + return create_frs(tmp_path, 2025) + + +def test_create_frs_smoke_includes_legacy_proxy_columns(tmp_path, monkeypatch): + dataset = create_single_adult_frs(tmp_path, monkeypatch) assert { "legacy_jobseeker_proxy", @@ -548,8 +558,10 @@ def fake_read_csv(path, *args, **kwargs): "age_started_or_accepted_current_education_or_training", "is_before_universal_credit_qualifying_young_person_terminal_date", "is_parent", + "is_claimant_or_partner", }.issubset(dataset.person.columns) assert not dataset.person["is_parent"].iloc[0] + assert dataset.person["is_claimant_or_partner"].iloc[0] assert not dataset.person["is_in_non_advanced_education"].iloc[0] assert not dataset.person["is_in_approved_training"].iloc[0] assert ( @@ -561,3 +573,31 @@ def fake_read_csv(path, *args, **kwargs): ].iloc[0] assert dataset.person["education_grants"].iloc[0] == 100 assert dataset.person["disabled_students_allowance_eligible_expenses"].iloc[0] == 0 + + +@pytest.mark.parametrize("empstati", sorted(FRS_EMPSTATI_EMPLOYMENT_STATUS)) +def test_create_frs_maps_every_empstati_code(tmp_path, monkeypatch, empstati): + person = create_single_adult_frs(tmp_path, monkeypatch, empstati).person + + status = person["employment_status"].iloc[0] + assert status == FRS_EMPSTATI_EMPLOYMENT_STATUS[empstati] + # A working-age adult reporting no hours: only the sick/disabled codes + # (9 permanently, 10 temporarily) are ESA health states. + assert person["esa_health_condition_proxy"].iloc[0] == (empstati in (9, 10)) + assert person["esa_support_group_proxy"].iloc[0] == (empstati == 9) + + +def test_create_frs_child_rows_are_child(tmp_path, monkeypatch): + person = create_single_adult_frs( + tmp_path, monkeypatch, empstati=11, with_child=True + ).person.set_index("person_id") + + assert person["employment_status"].to_dict() == { + 100_001: "OTHER_INACTIVE", + 100_002: "CHILD", + } + + +def test_create_frs_rejects_unknown_adult_empstati(tmp_path, monkeypatch): + with pytest.raises(ValueError, match="EMPSTATI"): + create_single_adult_frs(tmp_path, monkeypatch, empstati=12) diff --git a/policyengine_uk_data/tests/test_lfs_employment_targets.py b/policyengine_uk_data/tests/test_lfs_employment_targets.py new file mode 100644 index 000000000..ae49b68aa --- /dev/null +++ b/policyengine_uk_data/tests/test_lfs_employment_targets.py @@ -0,0 +1,125 @@ +"""ONS LFS employee and self-employed calibration targets. + +Invariants: + +1. Two national count targets, with the ONS values (held here independently + of the source module, so a wrong value is caught) for every year listed. +2. The calibration and base years resolve to a value. +3. The household column counts the household's members whose main-job status + is in the target's group, for any statuses and household layout + (Hypothesis), so the weighted column total is the weighted head count. +4. On a built enhanced FRS, the weighted counts are near the targets. + This also catches the local HMRC employment counts going back into + training (``VALIDATION_ONLY_LOCAL_TARGETS``), which moves employees up. +""" + +from __future__ import annotations + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk_data.datasets.frs_release import CURRENT_FRS_RELEASE +from policyengine_uk_data.targets.build_loss_matrix import _resolve_value +from policyengine_uk_data.targets.registry import discover_source_modules +from policyengine_uk_data.targets.sources.ons_labour_market import get_targets +from policyengine_uk_data.utils.employment_status import ( + EMPLOYEE_STATUSES, + SELF_EMPLOYED_STATUSES, +) + +# ONS Labour market overview, 15 September 2026: MGRN and MGRQ, annual +# four-quarter averages, thousands. +ONS_THOUSANDS = { + "ons/lfs_employees": {2022: 28_564, 2023: 28_821, 2024: 29_126, 2025: 29_590}, + "ons/lfs_self_employed": {2022: 4_244, 2023: 4_380, 2024: 4_340, 2025: 4_395}, +} +STATUS_GROUPS = { + "ons/lfs_employees": EMPLOYEE_STATUSES, + "ons/lfs_self_employed": SELF_EMPLOYED_STATUSES, +} +ALL_STATUSES = ( + EMPLOYEE_STATUSES + + SELF_EMPLOYED_STATUSES + + ("CHILD", "UNEMPLOYED", "RETIRED", "STUDENT", "CARER", "OTHER_INACTIVE") +) +# Calibration only partly pulls national counts in (see the public sector +# employment test); the built-data check guards against the 4m drift seen +# without these targets, not against small misses. +BUILT_RELATIVE_TOLERANCE = 0.08 + + +def _by_name(): + return {target.name: target for target in get_targets()} + + +def test_targets_and_values(): + targets = _by_name() + assert set(targets) == set(ONS_THOUSANDS) + for name, values in ONS_THOUSANDS.items(): + target = targets[name] + assert target.is_count + assert target.source == "ons" + assert target.variable == "employment_status" + assert target.values == {year: v * 1e3 for year, v in values.items()} + + +def test_source_module_is_discovered(): + # Discovery only imports the source modules; collecting every target + # would download the other sources' tables. + modules = {module.__name__ for module in discover_source_modules()} + assert "policyengine_uk_data.targets.sources.ons_labour_market" in modules + + +@pytest.mark.parametrize( + "year", + sorted({CURRENT_FRS_RELEASE.base_year, CURRENT_FRS_RELEASE.calibration_year}), +) +def test_model_years_resolve(year): + for name, target in _by_name().items(): + assert _resolve_value(target, year) == ONS_THOUSANDS[name][year] * 1e3 + + +class _FakeContext: + def __init__(self, status, household): + self.status = np.array(status, dtype=object) + self.household = np.array(household) + + def pe_person(self, variable): + assert variable == "employment_status" + return self.status + + def household_from_person(self, values): + return np.bincount(self.household, weights=values, minlength=10) + + +@settings(deadline=None, suppress_health_check=[HealthCheck.too_slow]) +@given( + st.lists( + st.tuples(st.sampled_from(ALL_STATUSES), st.integers(0, 9)), + min_size=1, + max_size=60, + ) +) +def test_column_counts_household_members_in_group(people): + status, household = zip(*people) + ctx = _FakeContext(status, household) + for name, target in _by_name().items(): + column = target.custom_compute(ctx, target, 2025) + expected = np.zeros(10) + for s, h in people: + expected[h] += s in STATUS_GROUPS[name] + np.testing.assert_array_equal(column, expected) + + +def test_built_enhanced_frs_near_lfs(enhanced_frs, baseline): + year = CURRENT_FRS_RELEASE.calibration_year + status = baseline.calculate("employment_status", year).values.astype(str) + weight = baseline.calculate("person_weight", year).values + for name, statuses in STATUS_GROUPS.items(): + estimate = weight[np.isin(status, statuses)].sum() + target = ONS_THOUSANDS[name][year] * 1e3 + assert abs(estimate / target - 1) < BUILT_RELATIVE_TOLERANCE, ( + f"{name}: {estimate / 1e6:.2f}m against {target / 1e6:.2f}m" + ) diff --git a/policyengine_uk_data/tests/test_oa_region_assignment.py b/policyengine_uk_data/tests/test_oa_region_assignment.py new file mode 100644 index 000000000..c350bfc6e --- /dev/null +++ b/policyengine_uk_data/tests/test_oa_region_assignment.py @@ -0,0 +1,624 @@ +"""Invariants for region-constrained Output Area assignment. + +A household's OA is drawn from its own FRS region, so the OA's region, +LA and constituency can never contradict ``household.region``. The +property tests state that for every input; the example tests pin the +edge cases and the real crosswalk's shape. +""" + +import itertools +import tempfile +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest +from policyengine_uk.data import UKSingleYearDataset + +from policyengine_uk_data.calibration.clone_and_assign import ( + _household_country_codes, + clone_and_assign, +) +from policyengine_uk_data.calibration.oa_assignment import ( + FRS_REGION_TO_CODE, + _normalise_region, + assign_random_geography, +) +from policyengine_uk_data.calibration.oa_crosswalk import ( + CROSSWALK_PATH, + load_oa_crosswalk, +) +from policyengine_uk_data.storage import STORAGE_FOLDER + +hypothesis = pytest.importorskip("hypothesis") +from hypothesis import given, settings # noqa: E402 +from hypothesis import strategies as st # noqa: E402 + +REGION_TO_COUNTRY = { + region: {"E": "England", "W": "Wales", "S": "Scotland", "N": "Northern Ireland"}[ + code[0] + ] + for region, code in FRS_REGION_TO_CODE.items() +} +COUNTRY_TO_FRS_CODE = {"England": 1, "Wales": 2, "Scotland": 3, "Northern Ireland": 4} +GEOGRAPHY_FIELDS = { + "lsoa_code": "lsoa_code", + "msoa_code": "msoa_code", + "la_code": "la_code", + "constituency_code": "constituency_code", + "region_code": "region_code", +} +_file_counter = itertools.count() + + +def _crosswalk_rows(region: str, constituency_populations: list[list[int]]) -> list: + """OAs for one region: one LA per constituency, one OA per population.""" + code = FRS_REGION_TO_CODE[region] + rows = [] + for c, populations in enumerate(constituency_populations): + for o, population in enumerate(populations): + rows.append( + { + "oa_code": f"{code[0]}00{code[-2:]}{c:02d}{o:02d}", + "lsoa_code": f"{code[0]}01{code[-2:]}{c:02d}{o:02d}", + "msoa_code": f"{code[0]}02{code[-2:]}{c:02d}", + "la_code": f"{code[0]}06{code[-2:]}{c:02d}", + "constituency_code": f"{code[0]}14{code[-2:]}{c:02d}", + "region_code": code, + "country": REGION_TO_COUNTRY[region], + "population": population, + } + ) + return rows + + +def _write_crosswalk(directory: Path, rows: list) -> tuple[pd.DataFrame, str]: + """Write to a fresh path: the loaders cache on the path string.""" + frame = pd.DataFrame(rows) + path = Path(directory) / f"crosswalk_{next(_file_counter)}.csv.gz" + frame.to_csv(path, index=False, compression="gzip") + return frame, str(path) + + +def _countries(countries: list[str]) -> np.ndarray: + return np.array([COUNTRY_TO_FRS_CODE[c] for c in countries]) + + +# Ways a region can arrive: FRS name, crosswalk code, bytes, untidy text. +REPRESENTATIONS = { + "name": lambda region: region, + "code": lambda region: FRS_REGION_TO_CODE[region], + "bytes": lambda region: region.encode(), + "untidy": lambda region: f" {region.lower()} ", +} +UNKNOWN_VALUES = ["UNKNOWN", "", None, np.nan, b"UNKNOWN"] + + +def _toy_dataset(regions: list, weights: list[float]) -> UKSingleYearDataset: + ids = np.arange(1, len(regions) + 1) + return UKSingleYearDataset( + person=pd.DataFrame( + { + "person_id": ids * 1000 + 1, + "person_household_id": ids, + "person_benunit_id": ids * 100 + 1, + "age": 40, + } + ), + benunit=pd.DataFrame({"benunit_id": ids * 100 + 1}), + household=pd.DataFrame( + { + "household_id": ids, + "household_weight": weights, + "region": pd.Series(regions, dtype=object), + } + ), + fiscal_year=2024, + ) + + +@st.composite +def households(draw, crosswalk_regions: list[str], unknown_countries: list[str]): + """Households as (FRS region or None, country, raw region value).""" + known = [(region, REGION_TO_COUNTRY[region]) for region in crosswalk_regions] + unknown = [(None, country) for country in unknown_countries] + region, country = draw(st.sampled_from(known + unknown)) + if region is None: + raw = draw(st.sampled_from(UNKNOWN_VALUES)) + else: + raw = REPRESENTATIONS[draw(st.sampled_from(sorted(REPRESENTATIONS)))](region) + return region, country, raw + + +@st.composite +def crosswalk_and_households(draw): + """A synthetic crosswalk plus households the sampler can serve. + + A household with no region can belong to any country in the crosswalk. + """ + crosswalk_regions = draw( + st.lists( + st.sampled_from(sorted(FRS_REGION_TO_CODE)), + min_size=1, + max_size=6, + unique=True, + ) + ) + rows = [] + for region in crosswalk_regions: + rows += _crosswalk_rows( + region, + draw( + st.lists( + st.lists(st.integers(0, 500), min_size=1, max_size=4), + min_size=1, + max_size=4, + ) + ), + ) + countries = sorted({REGION_TO_COUNTRY[r] for r in crosswalk_regions}) + sample = draw( + st.lists(households(crosswalk_regions, countries), min_size=1, max_size=12) + ) + return rows, sample + + +def _as_tuple(geography) -> tuple: + return tuple( + tuple(getattr(geography, field)) + for field in ["oa_code", "country", *GEOGRAPHY_FIELDS] + ) + + +class TestRegionConstraintProperties: + @given( + case=crosswalk_and_households(), + n_clones=st.integers(1, 4), + seed=st.integers(0, 2**32 - 1), + ) + @settings(max_examples=80, deadline=None) + def test_assignment_invariants(self, case, n_clones, seed): + rows, sample = case + regions, countries, raw = map(list, zip(*sample)) + with tempfile.TemporaryDirectory() as directory: + crosswalk, path = _write_crosswalk(directory, rows) + kwargs = dict( + household_countries=_countries(countries), + household_regions=np.array(raw, dtype=object), + n_clones=n_clones, + seed=seed, + crosswalk_path=path, + ) + geography = assign_random_geography(**kwargs) + repeat = assign_random_geography(**kwargs) + + # Determinism: same inputs and seed, same assignment. + assert _as_tuple(geography) == _as_tuple(repeat) + + n = len(sample) + assert len(geography.oa_code) == n * n_clones + by_oa = crosswalk.set_index("oa_code") + region_population = crosswalk.groupby("region_code")["population"].sum() + country_population = crosswalk.groupby("country")["population"].sum() + for i, oa in enumerate(geography.oa_code): + region, country = regions[i % n], countries[i % n] + row = by_oa.loc[oa] + # The OA's region is the household's region; a household with + # no region stays in its country. + assert row["country"] == country == geography.country[i] + if region is None: + stratum_population = country_population[country] + else: + assert geography.region_code[i] == FRS_REGION_TO_CODE[region] + stratum_population = region_population[FRS_REGION_TO_CODE[region]] + # Every other code is the crosswalk's own row for that OA. + for field, column in GEOGRAPHY_FIELDS.items(): + assert getattr(geography, field)[i] == row[column] + # Unpopulated OAs are never drawn while the stratum has people. + if stratum_population > 0: + assert row["population"] > 0 + + @given( + case=crosswalk_and_households(), + uncovered=st.lists( + st.sampled_from( + [("NORTHERN_IRELAND", "Northern Ireland"), (None, "England")] + ), + max_size=3, + ), + weights=st.lists( + st.floats(0, 5_000, allow_nan=False), min_size=15, max_size=15 + ), + n_clones=st.integers(1, 4), + seed=st.integers(0, 2**32 - 1), + ) + @settings(max_examples=50, deadline=None) + def test_clone_and_assign_invariants( + self, case, uncovered, weights, n_clones, seed + ): + rows, sample = case + # clone_and_assign reads a household without a region as English. + sample = [(r, "England" if r is None else c, raw) for r, c, raw in sample] + covered_countries = {row["country"] for row in rows} + sample += [ + (region, country, region or "UNKNOWN") + for region, country in uncovered + if country not in covered_countries + ] + regions, countries, raw = map(list, zip(*sample)) + dataset = _toy_dataset(raw, weights[: len(sample)]) + with tempfile.TemporaryDirectory() as directory: + _, path = _write_crosswalk(directory, rows) + kwargs = dict(n_clones=n_clones, seed=seed, crosswalk_path=path) + household = clone_and_assign(dataset, **kwargs).household + repeat = clone_and_assign(dataset, **kwargs).household + + pd.testing.assert_frame_equal(household, repeat) + + original = dataset.household + # Weights are preserved in total and for every source household. + np.testing.assert_allclose( + household["household_weight"].sum(), + original["household_weight"].sum(), + rtol=1e-12, + atol=1e-9, + ) + np.testing.assert_allclose( + household.groupby("source_household_id")["household_weight"].sum().values, + original.set_index("household_id")["household_weight"].values, + rtol=1e-12, + atol=1e-9, + ) + + # Cloning never rewrites the FRS region (a missing value may come + # back as None or NaN; both are missing). + def tidy(values): + return [None if pd.isna(v) else v for v in values] + + assert tidy(household["region"]) == tidy(original["region"]) * n_clones + + for i, (region, country) in enumerate( + zip(regions * n_clones, countries * n_clones) + ): + assigned = household.iloc[i] + if country not in covered_countries: + # A country the crosswalk does not cover gets no geography. + for column in ["oa_code", "la_code_oa", "constituency_code_oa"]: + assert assigned[column] == "" + elif region is None: + assert assigned["region_code_oa"].startswith("E12") + else: + assert assigned["region_code_oa"] == FRS_REGION_TO_CODE[region] + assert assigned["oa_code"] != "" + + @given( + unknown_countries=st.lists( + st.sampled_from(["England", "Wales", "Scotland", "Northern Ireland"]), + max_size=6, + ), + known=st.lists( + st.sampled_from(["LONDON", "WALES", "SCOTLAND", "NORTHERN_IRELAND"]), + max_size=6, + ), + order=st.randoms(use_true_random=False), + n_clones=st.integers(1, 4), + seed=st.integers(0, 2**32 - 1), + ) + @settings(max_examples=60, deadline=None) + def test_matches_country_sampling_when_region_is_the_country( + self, unknown_countries, known, order, n_clones, seed + ): + """Differential: when each country is one region, giving regions + changes nothing, even with households of unknown region mixed in.""" + sample = [(None, c) for c in unknown_countries] + [ + (r, REGION_TO_COUNTRY[r]) for r in known + ] or [("LONDON", "England")] + order.shuffle(sample) + rows = [] + for region in ["LONDON", "WALES", "SCOTLAND", "NORTHERN_IRELAND"]: + rows += _crosswalk_rows(region, [[30, 10, 0], [5, 55], [20]]) + with tempfile.TemporaryDirectory() as directory: + _, path = _write_crosswalk(directory, rows) + kwargs = dict( + household_countries=_countries([c for _, c in sample]), + n_clones=n_clones, + seed=seed, + crosswalk_path=path, + ) + by_country = assign_random_geography(**kwargs) + by_region = assign_random_geography( + household_regions=np.array( + [r or "UNKNOWN" for r, _ in sample], dtype=object + ), + **kwargs, + ) + assert _as_tuple(by_country) == _as_tuple(by_region) + + +class TestRegionConstraintExamples: + @pytest.fixture(scope="class") + def two_region_crosswalk(self, tmp_path_factory): + rows = _crosswalk_rows("LONDON", [[100, 300]]) + _crosswalk_rows( + "NORTH_EAST", [[50_000], [50_000]] + ) + return _write_crosswalk(tmp_path_factory.mktemp("two_region"), rows) + + def test_sampling_is_population_weighted_within_the_region( + self, two_region_crosswalk + ): + """London's two OAs split 1:3 however large the North East is.""" + crosswalk, path = two_region_crosswalk + n = 20_000 + geography = assign_random_geography( + household_countries=np.ones(n, dtype=int), + household_regions=np.array(["LONDON"] * n, dtype=object), + n_clones=1, + seed=0, + crosswalk_path=path, + ) + assert set(geography.region_code) == {"E12000007"} + larger = crosswalk.loc[crosswalk["population"] == 300, "oa_code"].iloc[0] + share = (geography.oa_code == larger).mean() + # Binomial(20,000, 0.75): four standard errors is 0.012. + assert abs(share - 0.75) < 0.012 + + def test_country_sampling_without_regions_is_unchanged(self, two_region_crosswalk): + """Callers that know only the country still sample country-wide.""" + _, path = two_region_crosswalk + geography = assign_random_geography( + household_countries=np.ones(2_000, dtype=int), + n_clones=1, + seed=0, + crosswalk_path=path, + ) + assert (geography.region_code == "E12000001").mean() > 0.9 + + def test_unknown_region_falls_back_to_the_country(self, two_region_crosswalk): + _, path = two_region_crosswalk + for unknown in ["UNKNOWN", "", None, np.nan]: + geography = assign_random_geography( + household_countries=np.ones(500, dtype=int), + household_regions=np.array([unknown] * 500, dtype=object), + n_clones=1, + seed=0, + crosswalk_path=path, + ) + assert set(geography.region_code) == {"E12000001", "E12000007"} + + def test_region_names_codes_and_bytes_are_equivalent(self, two_region_crosswalk): + _, path = two_region_crosswalk + results = [ + assign_random_geography( + household_countries=np.ones(50, dtype=int), + household_regions=np.array([value] * 50, dtype=object), + n_clones=2, + seed=3, + crosswalk_path=path, + ) + for value in ["LONDON", "E12000007", b"LONDON", " london "] + ] + assert len({_as_tuple(result) for result in results}) == 1 + + def test_region_without_oas_raises(self, two_region_crosswalk): + _, path = two_region_crosswalk + with pytest.raises(ValueError, match=r"E12000009 \(England\)"): + assign_random_geography( + household_countries=np.array([1]), + household_regions=np.array(["SOUTH_WEST"], dtype=object), + n_clones=1, + crosswalk_path=path, + ) + + def test_region_in_the_wrong_country_raises(self, two_region_crosswalk): + _, path = two_region_crosswalk + with pytest.raises(ValueError, match="disagree"): + assign_random_geography( + household_countries=np.array([2]), + household_regions=np.array(["LONDON"], dtype=object), + n_clones=1, + crosswalk_path=path, + ) + + def test_unrecognised_region_raises(self, two_region_crosswalk): + _, path = two_region_crosswalk + with pytest.raises(ValueError, match="Unrecognised household region"): + assign_random_geography( + household_countries=np.array([1]), + household_regions=np.array(["MERCIA"], dtype=object), + n_clones=1, + crosswalk_path=path, + ) + + def test_region_length_mismatch_raises(self, two_region_crosswalk): + _, path = two_region_crosswalk + with pytest.raises(ValueError, match="expected 2"): + assign_random_geography( + household_countries=np.array([1, 1]), + household_regions=np.array(["LONDON"], dtype=object), + n_clones=1, + crosswalk_path=path, + ) + + def test_clones_take_different_constituencies_within_the_region(self, tmp_path): + """Collision avoidance still works when the pool is one region.""" + rows = _crosswalk_rows("NORTH_EAST", [[100]] * 12) + _crosswalk_rows( + "LONDON", [[100]] * 40 + ) + _, path = _write_crosswalk(tmp_path, rows) + n, n_clones = 200, 10 + geography = assign_random_geography( + household_countries=np.ones(n, dtype=int), + household_regions=np.array(["NORTH_EAST"] * n, dtype=object), + n_clones=n_clones, + seed=42, + crosswalk_path=path, + ) + constituencies = geography.constituency_code.reshape(n_clones, n) + assert set(geography.region_code) == {"E12000001"} + assert all(len(set(constituencies[:, i])) == n_clones for i in range(n)) + + def test_single_constituency_region_terminates(self, tmp_path): + """Unavoidable collisions exhaust the retries and still assign.""" + _, path = _write_crosswalk(tmp_path, _crosswalk_rows("WALES", [[10, 20]])) + geography = assign_random_geography( + household_countries=np.full(5, 2), + household_regions=np.array(["WALES"] * 5, dtype=object), + n_clones=3, + seed=1, + crosswalk_path=path, + ) + assert len(set(geography.constituency_code)) == 1 + + def test_zero_population_region_samples_uniformly(self, tmp_path): + """A region with no recorded population draws its OAs uniformly.""" + rows = _crosswalk_rows("WALES", [[0, 0], [0, 0]]) + _crosswalk_rows( + "LONDON", [[100]] + ) + crosswalk, path = _write_crosswalk(tmp_path, rows) + n = 8_000 + geography = assign_random_geography( + household_countries=np.full(n, 2), + household_regions=np.array(["WALES"] * n, dtype=object), + n_clones=1, + seed=5, + crosswalk_path=path, + ) + shares = pd.Series(geography.oa_code).value_counts(normalize=True) + welsh = set(crosswalk.loc[crosswalk["country"] == "Wales", "oa_code"]) + assert set(shares.index) == welsh + # Binomial(8,000, 0.25): four standard errors is 0.019. + assert (shares - 0.25).abs().max() < 0.02 + + def test_mixed_unknown_and_sole_region_draw_like_the_country(self, tmp_path): + """Review counterexample: an UNKNOWN and a London household, London + being England's only region, draw exactly as a country-only call.""" + rows = ( + _crosswalk_rows("LONDON", [[100], [100]]) + + _crosswalk_rows("WALES", [[100]]) + + _crosswalk_rows("SCOTLAND", [[100]]) + + _crosswalk_rows("NORTHERN_IRELAND", [[100]]) + ) + _, path = _write_crosswalk(tmp_path, rows) + kwargs = dict( + household_countries=np.array([1, 1]), + n_clones=1, + seed=0, + crosswalk_path=path, + ) + by_country = assign_random_geography(**kwargs) + by_region = assign_random_geography( + household_regions=np.array(["UNKNOWN", "LONDON"], dtype=object), + **kwargs, + ) + assert _as_tuple(by_country) == _as_tuple(by_region) + + def test_clone_and_assign_reads_untidy_regions_like_the_sampler(self, tmp_path): + """Review counterexample: bytes, codes and untidy text give the + same country in cloning as in the sampler.""" + rows = _crosswalk_rows("WALES", [[100]]) + _crosswalk_rows("LONDON", [[100]]) + _, path = _write_crosswalk(tmp_path, rows) + raw = [b"WALES", " wales ", "W99999999", b"NORTHERN_IRELAND", "UNKNOWN"] + assert _household_country_codes( + _toy_dataset(raw, [1.0] * len(raw)) + ).tolist() == [2, 2, 2, 4, 1] + household = clone_and_assign( + _toy_dataset(raw, [1.0] * len(raw)), n_clones=1, crosswalk_path=path + ).household + assert household["region_code_oa"].tolist() == [ + "W99999999", + "W99999999", + "W99999999", + "", + "E12000007", + ] + + +@pytest.fixture(scope="module") +def real_crosswalk() -> pd.DataFrame: + if not CROSSWALK_PATH.exists(): + pytest.skip("OA crosswalk not built") + return load_oa_crosswalk() + + +class TestRealCrosswalk: + def test_region_codes_name_the_right_places(self, real_crosswalk): + """FRS_REGION_TO_CODE agrees with where well-known LAs are.""" + names = pd.read_csv(STORAGE_FOLDER / "local_authorities_2021.csv") + la_region = ( + real_crosswalk[["la_code", "region_code"]] + .drop_duplicates() + .merge(names, left_on="la_code", right_on="code") + .set_index("name")["region_code"] + ) + expected = { + "Hartlepool": "NORTH_EAST", + "Manchester": "NORTH_WEST", + "Leeds": "YORKSHIRE", + "Nottingham": "EAST_MIDLANDS", + "Birmingham": "WEST_MIDLANDS", + "Cambridge": "EAST_OF_ENGLAND", + "Westminster": "LONDON", + "Brighton and Hove": "SOUTH_EAST", + "Bristol, City of": "SOUTH_WEST", + "Cardiff": "WALES", + "Glasgow City": "SCOTLAND", + } + for name, region in expected.items(): + assert la_region[name] == FRS_REGION_TO_CODE[region], name + + def test_every_crosswalk_region_is_an_frs_region(self, real_crosswalk): + assert set(real_crosswalk["region_code"]) <= set(FRS_REGION_TO_CODE.values()) + + def test_local_areas_nest_in_regions(self, real_crosswalk): + """No LA or constituency straddles a region, so sampling within the + region can still reach every OA of every local area.""" + for column in ["la_code", "constituency_code"]: + regions_per_area = real_crosswalk.groupby(column)["region_code"].nunique() + assert (regions_per_area == 1).all(), column + + def test_every_region_has_a_constituency_per_production_clone(self, real_crosswalk): + constituencies = real_crosswalk.groupby("region_code")[ + "constituency_code" + ].nunique() + assert constituencies.min() >= 10 + + def test_assignment_on_the_real_crosswalk(self, real_crosswalk): + covered = sorted( + region + for region, code in FRS_REGION_TO_CODE.items() + if code in set(real_crosswalk["region_code"]) + ) + regions = np.array(covered * 40, dtype=object) + n_clones = 10 + geography = assign_random_geography( + household_countries=_countries([REGION_TO_COUNTRY[r] for r in regions]), + household_regions=regions, + n_clones=n_clones, + seed=42, + ) + expected = np.tile([FRS_REGION_TO_CODE[r] for r in regions], n_clones) + assert (geography.region_code == expected).all() + constituencies = geography.constituency_code.reshape(n_clones, len(regions)) + distinct = np.array( + [len(set(constituencies[:, i])) for i in range(len(regions))] + ) + assert (distinct == n_clones).all() + + +def test_built_dataset_oa_region_matches_frs_region(enhanced_frs): + """Every household of the built enhanced FRS sits in its own region. + + A household with no region below the country is English to + clone_and_assign, so its OA must be English. + """ + household = enhanced_frs.household + region_code = household["region_code_oa"].map( + lambda value: value.decode() if isinstance(value, bytes) else str(value) + ) + expected = household["region"].map(_normalise_region) + has_oa = region_code != "" + assert has_oa.any() + known = has_oa & expected.notna() + assert int((region_code[known] != expected[known]).sum()) == 0 + unknown = has_oa & expected.isna() + assert region_code[unknown].str.startswith("E12").all() diff --git a/policyengine_uk_data/tests/test_obr_efo_fallback.py b/policyengine_uk_data/tests/test_obr_efo_fallback.py index 309e8d7c4..5884642cc 100644 --- a/policyengine_uk_data/tests/test_obr_efo_fallback.py +++ b/policyengine_uk_data/tests/test_obr_efo_fallback.py @@ -2,11 +2,18 @@ obr.uk serves 403 Forbidden to GitHub Actions runner IPs, and losing the workbooks silently drops 28 OBR targets and calibrates a degraded dataset -(observed on the 2026-07-21 push builds). These tests pin: the fallback -workbooks are committed and parseable, a failed download uses them instead -of raising, and a 403 does not burn the retry budget. +(observed on the 2026-07-21 push builds). obr.uk can also answer 200 with +an HTML "No Access" page, which once escaped the fallback as a BadZipFile +and dropped the nine receipts and NICs targets (observed 2026-10-04). These +tests pin: the fallback workbooks are committed and parseable, every kind of +failed download uses them instead of raising, and permanent failures do not +burn the retry budget. """ +import io +import logging +import zipfile +from contextlib import contextmanager from types import SimpleNamespace from unittest.mock import patch @@ -15,6 +22,7 @@ from policyengine_uk_data.storage import STORAGE_FOLDER from policyengine_uk_data.targets.sources import obr +from policyengine_uk_data.targets.sources._common import load_config @pytest.fixture(autouse=True) @@ -24,8 +32,104 @@ def _clear_workbook_cache(): obr._download_workbook.cache_clear() -def _forbidden(*args, **kwargs): - return SimpleNamespace(status_code=403, headers={}, content=b"") +def _response(status_code, content=b"", content_type=None): + headers = {"Content-Type": content_type} if content_type else {} + return SimpleNamespace(status_code=status_code, headers=headers, content=content) + + +def _zip(members: dict[str, str]) -> bytes: + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w") as archive: + for name, data in members.items(): + archive.writestr(name, data) + return buffer.getvalue() + + +def _connection_error(): + raise requests.ConnectionError("no route to obr.uk") + + +_NO_ACCESS_PAGE = ( + b"" + b"No Access - Office for Budget Responsibility" + b"

No Access

" +) + +# Every way the download can fail, with the number of requests it should +# cost: transient failures use the full retry budget, permanent ones one. +_FAILED_DOWNLOADS = { + "200-html-page": ( + lambda: _response(200, _NO_ACCESS_PAGE, "text/html; charset=UTF-8"), + 1, + ), + "200-empty-body": (lambda: _response(200), 1), + "200-truncated-zip": (lambda: _response(200, b"PK\x03\x04garbage"), 1), + "200-zip-without-xlsx-manifest": ( + lambda: _response(200, _zip({"readme.txt": "not a workbook"})), + 1, + ), + "200-zip-without-workbook-part": ( + lambda: _response( + 200, + _zip( + { + "[Content_Types].xml": '' + } + ), + ), + 1, + ), + "200-zip-with-malformed-xml": ( + lambda: _response(200, _zip({"[Content_Types].xml": "= 30 + } <= offline_names + + +@pytest.mark.parametrize("url_key", _EFO_URL_KEYS) +@pytest.mark.parametrize("kind", list(_FAILED_DOWNLOADS)) +def test_every_failed_download_uses_committed_workbook(kind, url_key): + make_response, expected_requests = _FAILED_DOWNLOADS[kind] + url = load_config()["obr"][url_key] + with _obr_answering(make_response) as (requests_made, fallbacks_used): + wb = obr._download_workbook(url) + assert fallbacks_used == [(url, wb)], "the committed workbook was not returned" + assert len(requests_made) == expected_requests + + +@pytest.mark.parametrize("kind", list(_FAILED_DOWNLOADS)) +def test_every_failed_download_keeps_receipts_and_nics_targets(kind): + make_response, _ = _FAILED_DOWNLOADS[kind] + with _obr_answering(make_response): + names = {t.name for t in obr.get_targets()} + assert _RECEIPTS_AND_NICS_TARGETS <= names, sorted( + _RECEIPTS_AND_NICS_TARGETS - names + ) + + +def test_workbook_response_is_parsed_without_fallback(): + """Positive control: a real xlsx body is used as served.""" + body = (STORAGE_FOLDER / "obr_efo" / "efo_receipts.xlsx").read_bytes() + with _obr_answering(lambda: _response(200, body)) as ( + requests_made, + fallbacks_used, + ): + wb = obr._download_workbook(load_config()["obr"]["efo_receipts"]) + assert fallbacks_used == [] + assert len(requests_made) == 1 + assert obr._find_receipts_sheet(wb) is not None + + +def test_non_workbook_200_warning_names_url_status_and_parse_error(caplog): + caplog.set_level(logging.WARNING, logger=obr.logger.name) + url = load_config()["obr"]["efo_receipts"] + with _obr_answering(_FAILED_DOWNLOADS["200-html-page"][0]): + obr._download_workbook(url) + assert ( + f"200 for url: {url}, but the body (text/html; charset=UTF-8) could " + "not be read as an xlsx workbook (BadZipFile: File is not a zip " + "file)); using committed workbook fallback" + ) in caplog.text diff --git a/policyengine_uk_data/tests/test_obr_universal_credit_target.py b/policyengine_uk_data/tests/test_obr_universal_credit_target.py new file mode 100644 index 000000000..8fcdb51e0 --- /dev/null +++ b/policyengine_uk_data/tests/test_obr_universal_credit_target.py @@ -0,0 +1,246 @@ +"""Tests for the OBR universal credit target. + +OBR EFO table 4.9 splits universal credit between spending inside the welfare +cap and outside it (the Intensive Work Search group, DWP's UC equivalent of +JSA). The split is not the household benefit cap, and policyengine-uk cannot +identify the Intensive Work Search group, so the two rows are one target: +GB universal credit, the sum of both rows. +""" + +from functools import lru_cache +from types import SimpleNamespace + +import numpy as np +import openpyxl +import pandas as pd +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_uk_data.storage import STORAGE_FOLDER +from policyengine_uk_data.targets import build_loss_matrix +from policyengine_uk_data.targets.build_loss_matrix import ( + _compute_column, + restrict_to_countries, +) +from policyengine_uk_data.targets.schema import GREAT_BRITAIN, GeographicLevel +from policyengine_uk_data.targets.sources import obr + +COUNTRIES = ("ENGLAND", "SCOTLAND", "WALES", "NORTHERN_IRELAND") +YEAR_COLUMNS = dict(zip("CDEFGHI", range(2024, 2031))) + + +def _committed_table() -> openpyxl.Workbook: + return openpyxl.load_workbook(STORAGE_FOLDER / "obr_efo" / "efo_expenditure.xlsx") + + +def _uc_targets(wb) -> dict: + return {t.name: t for t in obr._parse_welfare(wb) if "universal_credit" in t.name} + + +@lru_cache(maxsize=1) +def _committed_target(): + return _uc_targets(_committed_table())["obr/universal_credit"] + + +def test_universal_credit_is_one_gb_target(): + targets = _uc_targets(_committed_table()) + assert list(targets) == ["obr/universal_credit"] + target = targets["obr/universal_credit"] + assert target.variable == "universal_credit" + assert target.countries == GREAT_BRITAIN + # March 2026 EFO table 4.9, 2025-26: £66.411bn inside the welfare cap + # (row 18) plus £12.876bn outside it (row 43). + assert target.values[2025] == pytest.approx(79.287e9, abs=1e6) + + +def test_target_is_the_sum_of_both_rows_in_every_year(): + wb = _committed_table() + ws = wb["4.9"] + rows = [ + row + for row in range(1, 56) + if str(ws[f"B{row}"].value or "").startswith("Universal credit") + ] + assert rows == [18, 43] + target = _uc_targets(wb)["obr/universal_credit"] + assert sorted(target.values) == list(YEAR_COLUMNS.values()) + for column, year in YEAR_COLUMNS.items(): + expected = sum(ws[f"{column}{row}"].value for row in rows) * 1e9 + assert target.values[year] == pytest.approx(expected) + + +def _table(rows: list[tuple]) -> openpyxl.Workbook: + """A minimal sheet 4.9: (label, value per year) in column B onwards.""" + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "4.9" + for i, (label, value) in enumerate(rows, start=6): + ws[f"B{i}"] = label + if value is not None: + for column in YEAR_COLUMNS: + ws[f"{column}{i}"] = value + return wb + + +def test_rows_are_found_by_section_not_position(): + wb = _table( + [ + ("Welfare cap", None), + ("Pension credit", 6.0), + ("Universal credit", 60.0), + ("Child benefit", 13.0), + ("Welfare spending outside the welfare cap", None), + ("State pension", 140.0), + ("Universal credit", 10.0), + ] + ) + target = _uc_targets(wb)["obr/universal_credit"] + assert set(target.values.values()) == {70e9} + + +@pytest.mark.parametrize( + "rows", + [ + # No outside-the-cap row: a partial total would understate UC. + [ + ("Universal credit", 60.0), + ("Welfare spending outside the welfare cap", None), + ("State pension", 140.0), + ], + # No section heading: the rows cannot be told apart. + [("Universal credit", 60.0), ("Universal credit", 10.0)], + # Two rows in one section. + [ + ("Universal credit", 60.0), + ("Universal credit", 1.0), + ("Welfare spending outside the welfare cap", None), + ("Universal credit", 10.0), + ], + ], +) +def test_no_target_unless_both_rows_are_unambiguous(rows): + assert _uc_targets(_table(rows)) == {} + + +def test_no_target_when_the_rows_cover_different_years(): + wb = _table( + [ + ("Universal credit", 60.0), + ("Welfare spending outside the welfare cap", None), + ("Universal credit", 10.0), + ] + ) + wb["4.9"]["I8"] = None # 2030-31 missing from the outside-the-cap row only + assert _uc_targets(wb) == {} + + +def test_rows_below_row_55_are_found(): + padding = [(f"Other benefit {i}", 1.0) for i in range(60)] + wb = _table( + [("Universal credit", 60.0)] + + padding + + [("Welfare spending outside the welfare cap", None)] + + padding + + [("Universal credit", 10.0)] + ) + target = _uc_targets(wb)["obr/universal_credit"] + assert set(target.values.values()) == {70e9} + + +def test_target_matrix_counts_gb_households_only(monkeypatch): + """Through create_target_matrix itself: England has two benefit units on + UC, Northern Ireland and Wales one each, Scotland none.""" + import policyengine_uk + + target = _committed_target() + uc = pd.Series([100.0, 50.0, 300.0, 20.0]) + benunit_household = np.array([0, 0, 1, 2]) + country = pd.Series(["ENGLAND", "NORTHERN_IRELAND", "WALES", "SCOTLAND"]) + + class FakeMicrosimulation: + tax_benefit_system = SimpleNamespace( + variables={ + "universal_credit": SimpleNamespace( + entity=SimpleNamespace(key="benunit") + ) + } + ) + + def __init__(self, dataset=None, reform=None): + pass + + def calculate(self, variable, *args, **kwargs): + return {"universal_credit": uc, "country": country}[variable] + + def map_result(self, values, source, target_entity): + assert (source, target_entity) == ("benunit", "household") + return np.bincount( + benunit_household, weights=np.asarray(values), minlength=len(country) + ) + + monkeypatch.setattr(policyengine_uk, "Microsimulation", FakeMicrosimulation) + monkeypatch.setattr( + build_loss_matrix, + "get_all_targets", + lambda geographic_level=None: ( + [target] if geographic_level == GeographicLevel.NATIONAL else [] + ), + ) + + matrix, values = build_loss_matrix.create_target_matrix( + SimpleNamespace(time_period="2025"), time_period="2025" + ) + np.testing.assert_array_equal(matrix["obr/universal_credit"], [150, 0, 20, 0]) + assert values["obr/universal_credit"] == target.values[2025] + + +def _fake_ctx(uc, benunit_household, household_country): + n_households = len(household_country) + variables = { + "universal_credit": SimpleNamespace(entity=SimpleNamespace(key="benunit")) + } + return SimpleNamespace( + sim=SimpleNamespace( + tax_benefit_system=SimpleNamespace(variables=variables), + calculate=lambda variable, *args, **kwargs: uc, + ), + household_from_family=lambda values: np.bincount( + benunit_household, weights=np.asarray(values, float), minlength=n_households + ), + country=household_country, + ) + + +@settings(max_examples=200, deadline=None) +@given( + st.lists(st.sampled_from(COUNTRIES), min_size=1, max_size=20).flatmap( + lambda countries: st.tuples( + st.just(np.array(countries)), + st.lists( + st.tuples( + st.integers(0, len(countries) - 1), + st.floats(0, 1e5, allow_nan=False), + ), + max_size=40, + ), + ) + ) +) +def test_column_counts_gb_universal_credit_once(case): + """Property: the column sums UC over benefit units in GB households, each + once, and is zero for every Northern Ireland household.""" + household_country, benunits = case + benunit_household = np.array([h for h, _ in benunits], dtype=int) + uc = np.array([amount for _, amount in benunits], dtype=float) + ctx = _fake_ctx(uc, benunit_household, household_country) + target = _committed_target() + + column = restrict_to_countries( + _compute_column(target, ctx, 2025), ctx.country, target.countries + ) + + in_gb = household_country[benunit_household] != "NORTHERN_IRELAND" + assert column.sum() == pytest.approx(uc[in_gb].sum()) + assert (column[household_country == "NORTHERN_IRELAND"] == 0).all() + assert (column >= 0).all() diff --git a/policyengine_uk_data/tests/test_pension_credit_reported_capital.py b/policyengine_uk_data/tests/test_pension_credit_reported_capital.py new file mode 100644 index 000000000..d19185811 --- /dev/null +++ b/policyengine_uk_data/tests/test_pension_credit_reported_capital.py @@ -0,0 +1,119 @@ +"""`pension_credit_reported_capital` from the FRS benefit-unit capital +measure (TOTCAPB4, falling back to TOTCAPB3). + +Invariants: +1. A finite, non-negative TOTCAPB4 is carried over unchanged; where TOTCAPB4 + is absent or invalid, a valid TOTCAPB3 is used. +2. A missing, non-numeric or negative value in both gives -1 + (policyengine-uk's "none recorded" sentinel), so the household proxy + applies. +3. Without either column every benefit unit gets -1. +4. The output is always -1 or a non-negative number, one per benefit unit. +""" + +import numpy as np +import pandas as pd + +from policyengine_uk_data.datasets.frs import derive_pension_credit_reported_capital + + +def test_values_carry_over_and_invalid_values_fall_back(): + benunit = pd.DataFrame( + {"totcapb3": [0.0, 300.0, 2_900.0, 1_250_000.0, np.nan, -5.0, "x"]} + ) + result = derive_pension_credit_reported_capital(benunit) + assert result.tolist() == [0.0, 300.0, 2_900.0, 1_250_000.0, -1.0, -1.0, -1.0] + + +def test_totcapb4_takes_precedence_with_totcapb3_fallback(): + benunit = pd.DataFrame( + { + "totcapb3": [100.0, 200.0, 300.0, np.nan, -1.0], + "totcapb4": [150.0, np.nan, -5.0, 400.0, np.nan], + } + ) + result = derive_pension_credit_reported_capital(benunit) + assert result.tolist() == [150.0, 200.0, 300.0, 400.0, -1.0] + + +def test_nullable_inputs_keep_the_totcapb3_fallback(): + """Nullable (pd.NA) columns must not wipe a valid TOTCAPB3 where TOTCAPB4 + is missing.""" + benunit = pd.DataFrame( + { + "totcapb3": pd.array([0.0, 250.0, pd.NA, -3.0], dtype="Float64"), + "totcapb4": pd.array([pd.NA, pd.NA, 400.0, pd.NA], dtype="Float64"), + } + ) + result = derive_pension_credit_reported_capital(benunit) + assert result.tolist() == [0.0, 250.0, 400.0, -1.0] + + +def test_missing_column_gives_sentinel(): + benunit = pd.DataFrame({"benunit_id": [101, 102, 201]}) + assert derive_pension_credit_reported_capital(benunit).tolist() == [-1.0] * 3 + + +def test_output_is_sentinel_or_non_negative_for_random_inputs(): + rng = np.random.default_rng(1_792) + for _ in range(50): + n = int(rng.integers(1, 200)) + values = rng.normal(5_000, 20_000, n) + values[rng.random(n) < 0.1] = np.nan + result = derive_pension_credit_reported_capital( + pd.DataFrame({"totcapb4": values}) + ) + assert len(result) == n + assert np.all((result == -1) | (result >= 0)) + keep = np.isfinite(values) & (values >= 0) + np.testing.assert_array_equal(result[keep], values[keep]) + assert np.all(result[~keep] == -1) + + +def test_uprating_rows_match_the_model(): + """The build uprates the column with these rows (calibration materialises + the calibration year from them), and policyengine-uk projects the saved + dataset with the variable's own uprating index at runtime. The rows must + equal what ``create_policyengine_uprating_factors_table`` derives from the + locked policyengine-uk, or a unit near the 10,000 pound deemed-income + disregard can be assessed differently in calibration and at runtime.""" + from policyengine_uk.system import system + + from policyengine_uk_data.storage import STORAGE_FOLDER + from policyengine_uk_data.utils.uprating import END_YEAR, START_YEAR + + variable = system.variables["pension_credit_reported_capital"] + index = system.parameters.get_child(variable.uprating) + years = range(START_YEAR, END_YEAR + 1) + expected = {y: round(index(y) / index(START_YEAR), 3) for y in years} + factors = pd.read_csv(STORAGE_FOLDER / "uprating_factors.csv").set_index("Variable") + growth = pd.read_csv(STORAGE_FOLDER / "uprating_growth_factors.csv").set_index( + "Variable" + ) + for y in years: + assert factors.loc["pension_credit_reported_capital", str(y)] == expected[y] + expected_growth = ( + 0 if y == START_YEAR else round(expected[y] / expected[y - 1] - 1, 3) + ) + assert growth.loc["pension_credit_reported_capital", str(y)] == expected_growth + + +def test_spi_copy_records_no_capital(): + """SPI-synthetic copies carry SPI-imputed incomes, so the FRS donor's + capital is cleared to -1 (household proxy) on them.""" + from types import SimpleNamespace + + from policyengine_uk_data.datasets.imputations.income import ( + clear_frs_reported_capital, + ) + + copy = SimpleNamespace( + benunit=pd.DataFrame({"pension_credit_reported_capital": [0.0, 300.0, -1.0]}) + ) + assert clear_frs_reported_capital(copy).benunit[ + "pension_credit_reported_capital" + ].tolist() == [-1.0, -1.0, -1.0] + without = SimpleNamespace(benunit=pd.DataFrame({"benunit_id": [1, 2]})) + assert "pension_credit_reported_capital" not in ( + clear_frs_reported_capital(without).benunit.columns + ) diff --git a/policyengine_uk_data/tests/test_pension_credit_takeup.py b/policyengine_uk_data/tests/test_pension_credit_takeup.py new file mode 100644 index 000000000..a7fc3db4b --- /dev/null +++ b/policyengine_uk_data/tests/test_pension_credit_takeup.py @@ -0,0 +1,229 @@ +"""Pension Credit take-up solved over entitled units, and DWP targets.""" + +import numpy as np +import openpyxl +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_uk_data.parameters import load_take_up_rate +from policyengine_uk_data.targets import get_all_targets +from policyengine_uk_data.targets.schema import GREAT_BRITAIN, Unit +from policyengine_uk_data.targets.sources import dwp_pension_credit, obr +from policyengine_uk_data.datasets.pension_credit_takeup import ( + pension_credit_takeup_flags, +) +from policyengine_uk_data.utils.takeup import solve_fill_probability + +_units = st.lists( + st.tuples(st.floats(0, 5_000), st.booleans(), st.booleans()), + min_size=1, + max_size=60, +) + + +@settings(max_examples=300, deadline=None) +@given(_units, st.floats(0, 1)) +def test_fill_probability_meets_the_rate_or_hits_a_bound(units, rate): + weights = np.array([u[0] for u in units]) + eligible = np.array([u[1] for u in units]) + reported = np.array([u[2] for u in units]) + p = solve_fill_probability(rate, weights, eligible, reported) + assert 0 <= p <= 1 + target = rate * weights[eligible].sum() + reporting = weights[eligible & reported].sum() + remaining = weights[eligible & ~reported].sum() + expected = reporting + p * remaining + if remaining <= 0: + assert p == 0 + elif 0 < p < 1: + assert np.isclose(expected, target, rtol=1e-9, atol=1e-6) + elif p == 0: + assert reporting >= target - 1e-6 + else: + assert reporting + remaining <= target + 1e-6 + + +@settings(max_examples=200, deadline=None) +@given( + _units, st.floats(0, 1), st.floats(0, 1), st.floats(0, 1), st.integers(0, 2**32 - 1) +) +def test_reporters_claim_entitled_units_share_the_solved_probability_others_the_new_rate( + units, rate, newly_entitled_rate, other_rate, seed +): + weights = np.array([u[0] for u in units]) + entitled = np.array([u[1] for u in units]) + reported = np.array([u[2] for u in units]) + gb = np.ones(len(units), dtype=bool) + draws = np.random.default_rng(seed).random(len(units)) + result, p = pension_credit_takeup_flags( + draws, rate, weights, entitled, reported, gb, newly_entitled_rate + ) + assert p == solve_fill_probability(rate, weights, entitled, reported) + assert result[reported].all() + entitled_non_reporters = entitled & ~reported + np.testing.assert_array_equal( + result[entitled_non_reporters], (draws < p)[entitled_non_reporters] + ) + others = ~entitled & ~reported + np.testing.assert_array_equal(result[others], (draws < newly_entitled_rate)[others]) + # The newly entitled rate changes neither the probability nor any flag of + # an entitled unit, so the calibration year's take-up is unchanged. + other, p_other = pension_credit_takeup_flags( + draws, rate, weights, entitled, reported, gb, other_rate + ) + assert p_other == p + np.testing.assert_array_equal(other[entitled], result[entitled]) + + +def test_weighted_take_up_among_eligible_matches_rate(): + rng = np.random.default_rng(0) + n = 200_000 + weights = rng.uniform(100, 3_000, n) + entitled = rng.random(n) < 0.2 + reported = entitled & (rng.random(n) < 0.4) + result, _ = pension_credit_takeup_flags( + rng.random(n), 0.62, weights, entitled, reported, np.ones(n, bool), 0.37 + ) + take_up = weights[entitled & result].sum() / weights[entitled].sum() + assert abs(take_up - 0.62) < 0.005 + # Units with no entitlement claim at the newly entitled rate. + assert abs(result[~entitled].mean() - 0.37) < 0.01 + + +def test_rate_is_dwp_fye_2024_caseload_take_up(): + assert load_take_up_rate("pension_credit", 2025) == 0.62 + + +def test_newly_entitled_rate_is_dwp_savings_credit_only_take_up(): + assert load_take_up_rate("pension_credit_newly_entitled", 2025) == 0.37 + assert load_take_up_rate("pension_credit_newly_entitled", 2022) == 0.42 + + +def test_targets_reconcile_with_dwp_components(): + """Guarantee + savings credit spending and the three claim types sum to + the transcribed totals (DWP Pension Credit sheet, separate rows).""" + guarantee = {2022: 4_594.32, 2025: 5_787.44, 2030: 5_543.38} + savings = {2022: 340.99, 2025: 357.03, 2030: 278.38} + claims = {2022: (727, 448, 199), 2025: (807, 399, 176), 2030: (734, 285, 114)} + targets = {t.name: t for t in dwp_pension_credit.get_targets()} + spending = targets["dwp/pension_credit"] + caseload = targets["dwp/pension_credit_claims"] + for year in guarantee: + assert abs(spending.values[year] / 1e6 - guarantee[year] - savings[year]) < 0.02 + assert abs(caseload.values[year] / 1e3 - sum(claims[year])) <= 1 + for target in targets.values(): + assert target.countries == GREAT_BRITAIN + assert target.unit == (Unit.COUNT if target.is_count else Unit.GBP) + + +def test_pension_credit_is_targeted_only_by_dwp(): + names = {t.name for t in get_all_targets() if t.variable == "pension_credit"} + assert names == {"dwp/pension_credit", "dwp/pension_credit_claims"} + + +def test_obr_pension_credit_row_is_not_parsed(): + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "4.9" + ws["B2"] = "Pension credit" + for col in "CDEFGHI": + ws[f"{col}2"] = 6.0 + assert not obr._parse_welfare(wb) + + +def test_built_dataset_survey_reporters_claim_and_caseload_is_near_dwp( + baseline, enhanced_frs +): + """Every survey reporter claims. SPI-synthetic reports are imputed, so + those units are drawn like non-reporters.""" + from policyengine_uk_data.datasets.pension_credit_takeup import ( + spi_synthetic_benunits, + ) + from policyengine_uk_data.targets.build_loss_matrix import ( + _SimContext, + restrict_to_countries, + ) + + year = 2025 + reported = ( + baseline.calculate("pension_credit_reported", year, map_to="benunit").values > 0 + ) & ~spi_synthetic_benunits(enhanced_frs) + would_claim = baseline.calculate("would_claim_pc", year).values.astype(bool) + assert would_claim[reported].all() + + baseline.default_calculation_period = str(year) + ctx = _SimContext(baseline, str(year), None, None) + weight = baseline.calculate("household_weight", year).values + for target in dwp_pension_credit.get_targets(): + column = restrict_to_countries( + target.custom_compute(ctx, target, year), ctx.country, target.countries + ) + ratio = (column * weight).sum() / target.values[year] + assert 0.5 < ratio < 2, (target.name, ratio) + + +def test_spi_synthetic_benefit_units_are_found_through_their_household(): + from types import SimpleNamespace + + import pandas as pd + + from policyengine_uk_data.datasets.pension_credit_takeup import ( + spi_synthetic_benunits, + ) + + dataset = SimpleNamespace( + household=pd.DataFrame( + {"household_id": [1, 2, 3], "household_is_spi_synthetic": [0, 1, 0]} + ), + person=pd.DataFrame( + { + "person_household_id": [1, 2, 2, 3], + "person_benunit_id": [10, 20, 21, 30], + } + ), + benunit=pd.DataFrame({"benunit_id": [30, 21, 20, 10]}), + ) + np.testing.assert_array_equal( + spi_synthetic_benunits(dataset), [False, True, True, False] + ) + dataset.household = dataset.household.drop(columns="household_is_spi_synthetic") + assert not spi_synthetic_benunits(dataset).any() + + +@settings(max_examples=200, deadline=None) +@given(_units, _units, st.floats(0, 1), st.integers(0, 2**32 - 1)) +def test_northern_ireland_cannot_change_the_gb_solution(gb_units, ni_units, rate, seed): + """DWP's take-up rate covers Great Britain, so Northern Ireland's + entitlement, reporting and weights leave the probability and every GB + flag unchanged.""" + + def arrays(units): + return ( + np.array([u[0] for u in units]), + np.array([u[1] for u in units]), + np.array([u[2] for u in units]), + ) + + gw, ge, gr = arrays(gb_units) + nw, ne, nr = arrays(ni_units) + draws = np.random.default_rng(seed).random(len(gw) + len(nw)) + gb_only, p_gb = pension_credit_takeup_flags( + draws[: len(gw)], rate, gw, ge, gr, np.ones(len(gw), dtype=bool), 0.37 + ) + combined, p_uk = pension_credit_takeup_flags( + draws, + rate, + np.concatenate([gw, nw]), + np.concatenate([ge, ne]), + np.concatenate([gr, nr]), + np.concatenate([np.ones(len(gw), bool), np.zeros(len(nw), bool)]), + 0.37, + ) + assert p_uk == p_gb + np.testing.assert_array_equal(combined[: len(gw)], gb_only) + # Entitled Northern Ireland non-reporters are drawn at the GB probability, + # the rest at the newly entitled rate. + ni_draws = draws[len(gw) :] + ni_flags = combined[len(gw) :] + np.testing.assert_array_equal(ni_flags[ne & ~nr], (ni_draws < p_gb)[ne & ~nr]) + np.testing.assert_array_equal(ni_flags[~ne & ~nr], (ni_draws < 0.37)[~ne & ~nr]) diff --git a/policyengine_uk_data/tests/test_policybench_transfer.py b/policyengine_uk_data/tests/test_policybench_transfer.py index 900b73d1f..a8579c0ec 100644 --- a/policyengine_uk_data/tests/test_policybench_transfer.py +++ b/policyengine_uk_data/tests/test_policybench_transfer.py @@ -26,6 +26,10 @@ "pip_dl_category", "pip_m_category", } +# Benefit-unit roles the source records, which policyengine-uk infers by +# formula when a dataset leaves them out (is_claimant_or_partner from +# policyengine-uk#1896). Releases that predate a role ignore its column. +DATASET_SUPPLIED_ROLES = {"is_claimant_or_partner"} def _subset_source(tmp_path: Path, rows: int) -> Path: @@ -81,11 +85,15 @@ def test_policybench_transfer_writes_only_valid_leaf_inputs(tmp_path: Path): invalid_columns = [ column for column in frame.columns - if column not in system.variables - or system.variables[column].entity.key != entity + if (column not in system.variables and column not in DATASET_SUPPLIED_ROLES) or ( - not system.variables[column].is_input_variable() - and column not in ALLOWED_COMPATIBILITY_INPUTS + column in system.variables + and system.variables[column].entity.key != entity + ) + or ( + column in system.variables + and not system.variables[column].is_input_variable() + and column not in ALLOWED_COMPATIBILITY_INPUTS | DATASET_SUPPLIED_ROLES ) ] assert invalid_columns == [] @@ -205,6 +213,9 @@ def test_policybench_transfer_family_structure_matches_person_membership( person_benunit_ids = sim.calculate("person_benunit_id", map_to="person").values is_adult = sim.calculate("is_adult", map_to="person").values is_child = sim.calculate("is_child", map_to="person").values + is_claimant_or_partner = sim.calculate( + "is_claimant_or_partner", map_to="person" + ).values is_married = sim.calculate("is_married", map_to="benunit").values family_type = sim.calculate("family_type", map_to="benunit").values @@ -213,7 +224,12 @@ def test_policybench_transfer_family_structure_matches_person_membership( adults = int(is_adult[member_mask].sum()) children = int(is_child[member_mask].sum()) - assert bool(married) == (adults == 2) + # policyengine-uk (from 2.107, #1896) presumes a couple married when + # the dataset does not say, and a couple is a claimant and partner, + # not any two adults: a member under 20 and 16+ years younger than + # the claimant is presumed to be their child. + claimant_and_partner = int(is_claimant_or_partner[member_mask].sum()) + assert bool(married) == (claimant_and_partner == 2) if adults == 2 and children > 0: expected = "COUPLE_WITH_CHILDREN" @@ -228,6 +244,38 @@ def test_policybench_transfer_family_structure_matches_person_membership( assert observed == expected +def test_policybench_transfer_marks_head_and_joint_spouse_as_claimant_or_partner( + tmp_path: Path, +): + source_file_path = _subset_source(tmp_path, 200) + source = pd.read_csv(source_file_path) + dataset = create_enhanced_cps(source_file_path=source_file_path, calibrate=False) + person = dataset.person + + counts = person.groupby("person_benunit_id").is_claimant_or_partner.sum() + expected = np.where(source.filing_status == "joint", 2, 1) + np.testing.assert_array_equal(counts.sort_index().to_numpy(), expected) + # The head and any spouse are listed before every other member. + first = person.groupby( + "person_benunit_id" + ).cumcount() < person.person_benunit_id.map(counts) + assert (person.is_claimant_or_partner == first).all() + + +def test_checked_in_enhanced_cps_h5_roles_match_the_builder(tmp_path: Path): + checked_in = UKSingleYearDataset(file_path=str(ENHANCED_CPS_FILE)).person + built = create_enhanced_cps( + source_file_path=_subset_source(tmp_path, 500), + calibrate=False, + ).person + head = checked_in.iloc[: len(built)] + + for column in ("person_id", "person_benunit_id", "age", "is_claimant_or_partner"): + np.testing.assert_array_equal(head[column].to_numpy(), built[column].to_numpy()) + counts = checked_in.groupby("person_benunit_id").is_claimant_or_partner.sum() + assert counts.between(1, 2).all() + + def test_assign_council_tax_bands_handles_upper_percentile_edge(): households = pd.DataFrame( { diff --git a/policyengine_uk_data/tests/test_salary_sacrifice_headcount.py b/policyengine_uk_data/tests/test_salary_sacrifice_headcount.py index cbae09725..17a18a42d 100644 --- a/policyengine_uk_data/tests/test_salary_sacrifice_headcount.py +++ b/policyengine_uk_data/tests/test_salary_sacrifice_headcount.py @@ -9,21 +9,24 @@ from policyengine_uk_data.datasets.frs_release import CURRENT_FRS_RELEASE -# The total combines below-cap and above-cap users and moves slightly with -# each generated FRS calibration refresh. Widened from 0.16 after the -# household-weight alignment fix (#436) shifted the calibration starting point -# under the reduced-epoch CI build (TESTING=1). -TOTAL_TOLERANCE = 0.20 -# The below-cap count sits right at the 15% boundary under the -# reduced-epoch CI build (observed 15.09% on one TESTING=1 run and passing -# the next), so the tolerance widens under TESTING like TOTAL_TOLERANCE -# did after #436. Full builds keep the strict 15%. -TOLERANCE = 0.25 if os.environ.get("TESTING") == "1" else 0.15 +REDUCED_BUILD = os.environ.get("TESTING") == "1" +# The 32-epoch reduced build stops well short of the OBR salary-sacrifice +# counts that calibration targets: in the seed-0 full build of #529 the users +# are 4.5m at epoch 0, 5.5m at epoch 30 and 7.9m at epoch 510. Before #529, +# reduced builds met the old bounds (20% total, 25% below cap) only because +# SPI-synthetic children were imputed salary sacrifice (about 1.35m users on +# the seed-0 reduced build of main, which held 5.0m users without them). +# #529 gives those children no pay, so the reduced bounds widen to 40% and +# 45%. They still catch a collapse; full builds keep the strict bounds. +# The total was earlier widened from 0.16 after the household-weight +# alignment fix (#436). +TOTAL_TOLERANCE = 0.40 if REDUCED_BUILD else 0.20 +TOLERANCE = 0.45 if REDUCED_BUILD else 0.15 # Widened under the reduced-epoch CI build after the benunit-table sort fix # (#462) shifted the calibration starting point (observed 20.4% on a # TESTING=1 run), following the precedent of TOTAL_TOLERANCE (#436) and # TOLERANCE above. Full builds keep the strict 20%. -ABOVE_CAP_TOLERANCE = 0.25 if os.environ.get("TESTING") == "1" else 0.20 +ABOVE_CAP_TOLERANCE = 0.25 if REDUCED_BUILD else 0.20 PERIOD = CURRENT_FRS_RELEASE.calibration_year diff --git a/policyengine_uk_data/tests/test_spi_build.py b/policyengine_uk_data/tests/test_spi_build.py index efb37f810..eff98ef7a 100644 --- a/policyengine_uk_data/tests/test_spi_build.py +++ b/policyengine_uk_data/tests/test_spi_build.py @@ -66,6 +66,7 @@ "MCAS", "BPADUE", "MAIND", + "SEINC_NUM", ] @@ -386,6 +387,24 @@ def __init__(self, path): ] +def _write_income_model_cache(path, income_module, metadata): + """A cache in the per-earnings-group format, with stub fitted models.""" + with path.open("wb") as f: + pickle.dump( + { + "models": { + group: SimpleNamespace( + imputed_variables=list(income_module.IMPUTATIONS) + ) + for group in income_module.EARNINGS_GROUPS + }, + "input_columns": income_module.PREDICTORS, + "metadata": metadata, + }, + f, + ) + + def test_income_model_cache_rejects_stale_spi_release(tmp_path, monkeypatch): from policyengine_uk_data.datasets.imputations import income as income_module @@ -395,17 +414,7 @@ def test_income_model_cache_rejects_stale_spi_release(tmp_path, monkeypatch): "spi_release_name": "spi_2020_21", "spi_tab_filename": "put2021uk.tab", } - with cache.open("wb") as f: - pickle.dump( - { - "model": SimpleNamespace( - imputed_variables=list(income_module.IMPUTATIONS) - ), - "input_columns": income_module.PREDICTORS, - "metadata": stale_metadata, - }, - f, - ) + _write_income_model_cache(cache, income_module, stale_metadata) sentinel = object() monkeypatch.setattr(income_module, "INCOME_MODEL_PATH", cache) @@ -423,17 +432,7 @@ def test_income_model_cache_rejects_stale_sample_size(tmp_path, monkeypatch): **income_module.get_income_model_metadata(), "sample_size": income_module.TESTING_INCOME_MODEL_SAMPLE_SIZE, } - with cache.open("wb") as f: - pickle.dump( - { - "model": SimpleNamespace( - imputed_variables=list(income_module.IMPUTATIONS) - ), - "input_columns": income_module.PREDICTORS, - "metadata": stale_metadata, - }, - f, - ) + _write_income_model_cache(cache, income_module, stale_metadata) sentinel = object() monkeypatch.setattr(income_module, "INCOME_MODEL_PATH", cache) @@ -447,17 +446,7 @@ def test_income_model_cache_accepts_current_spi_release(tmp_path, monkeypatch): cache = tmp_path / "income_spi_2022_23.pkl" current_metadata = income_module.get_income_model_metadata() - with cache.open("wb") as f: - pickle.dump( - { - "model": SimpleNamespace( - imputed_variables=list(income_module.IMPUTATIONS) - ), - "input_columns": income_module.PREDICTORS, - "metadata": current_metadata, - }, - f, - ) + _write_income_model_cache(cache, income_module, current_metadata) monkeypatch.setattr(income_module, "INCOME_MODEL_PATH", cache) monkeypatch.setattr( @@ -467,3 +456,15 @@ def test_income_model_cache_accepts_current_spi_release(tmp_path, monkeypatch): ) assert income_module.create_income_model().metadata == current_metadata + + +def test_create_spi_marks_every_taxpayer_as_their_benefit_units_claimant(tmp_path): + from policyengine_uk_data.datasets.spi import create_spi + + tab = tmp_path / "spi.tab" + _write_fake_spi(tab) + + person = create_spi(tab, 2020, seed=0).person + assert person["is_claimant_or_partner"].dtype == bool + assert person["is_claimant_or_partner"].all() + assert person["person_benunit_id"].is_unique diff --git a/policyengine_uk_data/tests/test_spi_donor_benefit_rules.py b/policyengine_uk_data/tests/test_spi_donor_benefit_rules.py new file mode 100644 index 000000000..e162400d7 --- /dev/null +++ b/policyengine_uk_data/tests/test_spi_donor_benefit_rules.py @@ -0,0 +1,458 @@ +"""Benefit reports and the flags built from them on SPI-donor rows. + +Properties of ``apply_spi_donor_benefit_rules`` for any input: + +1. Every column in ``SPI_DONOR_ZEROED_PERSON_VARIABLES`` is zero afterwards, + and every column in ``SPI_DONOR_RESTORED_PERSON_VARIABLES`` equals the + donor's value. +2. Nothing else changes except ``receives_benefits_in_own_right`` and the + redrawn take-up flags, and the input is not mutated. +3. A benefit unit with a member reporting UC or Pension Credit claims it, + unless 6 applies. +4. ``receives_benefits_in_own_right`` holds exactly when the person reports + one of ``BENEFITS_IN_OWN_RIGHT_REPORTED_COLUMNS``. +5. The rules are deterministic and idempotent, and no flag's draws depend on + which other columns are present. +6. With the pension-age exclusion mask supplied, a benefit unit whose + claimant and any partner have all reached State Pension age never gets + ``would_claim_uc``, even if it reports UC, as in ``create_frs``. + +The fixtures are synthetic, not survey records. +""" + +import numpy as np +import pandas as pd +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from policyengine_uk.data import UKSingleYearDataset + +from policyengine_uk_data.datasets.disability_benefits import ( + add_disability_benefit_flags_from_reported_amounts, +) +from policyengine_uk_data.datasets.frs import ( + BENEFITS_IN_OWN_RIGHT_REPORTED_COLUMNS, + REPORTED_TAKEUP_ANCHORS, +) +from policyengine_uk_data.datasets.imputations import frs_only +from policyengine_uk_data.datasets.imputations.frs_only import ( + FRS_ONLY_PERSON_VARIABLES, + SPI_DONOR_REDRAWN_TAKEUP_FLAGS, + SPI_DONOR_RESTORED_PERSON_VARIABLES, + SPI_DONOR_ZEROED_PERSON_VARIABLES, + apply_spi_donor_benefit_rules, +) +from policyengine_uk_data.parameters import load_take_up_rate + +YEAR = 2024 +REPORT_COLUMNS = sorted( + set(FRS_ONLY_PERSON_VARIABLES) | set(SPI_DONOR_ZEROED_PERSON_VARIABLES) +) +CHANGED = {"receives_benefits_in_own_right", *SPI_DONOR_REDRAWN_TAKEUP_FLAGS} +RULED = set(SPI_DONOR_ZEROED_PERSON_VARIABLES) | set( + SPI_DONOR_RESTORED_PERSON_VARIABLES +) +ANCHORING_REPORTS = { + REPORTED_TAKEUP_ANCHORS[flag][1] for flag in SPI_DONOR_REDRAWN_TAKEUP_FLAGS +} + + +def test_rule_sets_are_pinned(): + """Each column's treatment is a decision; changing one must be deliberate.""" + assert set(SPI_DONOR_ZEROED_PERSON_VARIABLES) == { + "universal_credit_reported", + "pension_credit_reported", + "housing_benefit_reported", + "income_support_reported", + "working_tax_credit_reported", + "child_tax_credit_reported", + "jsa_income_reported", + "esa_income_reported", + "ssmg_reported", + "jsa_contrib_reported", + "esa_contrib_reported", + "incapacity_benefit_reported", + "sda_reported", + "child_benefit_reported", + } + assert set(SPI_DONOR_RESTORED_PERSON_VARIABLES) == { + "iidb_reported", + "afcs_reported", + "bsp_reported", + } + # Kept as drawn: every report the QRF draws that no rule above touches. + drawn_reports = {c for c in FRS_ONLY_PERSON_VARIABLES if c.endswith("_reported")} + assert drawn_reports - RULED == { + "state_pension_reported", + "winter_fuel_allowance_reported", + "attendance_allowance_reported", + "dla_sc_reported", + "dla_m_reported", + "pip_m_reported", + "pip_dl_reported", + "carers_allowance_reported", + "maternity_allowance_reported", + "council_tax_benefit_reported", + } + assert set(SPI_DONOR_REDRAWN_TAKEUP_FLAGS) == {"would_claim_uc", "would_claim_pc"} + assert not set(SPI_DONOR_ZEROED_PERSON_VARIABLES) & set( + SPI_DONOR_RESTORED_PERSON_VARIABLES + ) + + +def _dataset(benunit_sizes, reports, flags, id_seed=0) -> UKSingleYearDataset: + """Benefit units of the given sizes, with shuffled, gapped ids.""" + rng = np.random.default_rng(id_seed) + n_benunits, n_people = len(benunit_sizes), sum(benunit_sizes) + benunit_ids = rng.permutation(n_benunits) * 7 + 3 + benunit_of_person = np.repeat(benunit_ids, benunit_sizes) + person = pd.DataFrame( + { + "person_id": rng.permutation(n_people) * 5 + 11, + "person_benunit_id": benunit_of_person, + "person_household_id": benunit_of_person, + "age": np.full(n_people, 40), + "receives_benefits_in_own_right": np.asarray(flags[:n_people]), + } + ) + for i, column in enumerate(REPORT_COLUMNS): + person[column] = np.asarray(reports[i][:n_people], dtype=float) + person = person.sample(frac=1, random_state=id_seed).reset_index(drop=True) + benunit = pd.DataFrame({"benunit_id": benunit_ids}) + for j, flag in enumerate(REPORTED_TAKEUP_ANCHORS): + benunit[flag] = np.roll(np.asarray(flags[:n_benunits]), j) + household = pd.DataFrame({"household_id": benunit_ids, "household_weight": 0.0}) + return UKSingleYearDataset( + person=person, benunit=benunit, household=household, fiscal_year=YEAR + ) + + +@st.composite +def datasets(draw): + sizes = draw(st.lists(st.integers(1, 4), min_size=1, max_size=12)) + n = sum(sizes) + amount = st.one_of(st.just(0.0), st.floats(0.01, 50_000)) + reports = [draw(st.lists(amount, min_size=n, max_size=n)) for _ in REPORT_COLUMNS] + flags = draw(st.lists(st.booleans(), min_size=n, max_size=n)) + return _dataset(sizes, reports, flags, id_seed=draw(st.integers(0, 10**6))) + + +def _donor(dataset, seed=1): + """A donor person table: the same people with different report values.""" + donor = dataset.person.copy() + rng = np.random.default_rng(seed) + for column in SPI_DONOR_RESTORED_PERSON_VARIABLES: + donor[column] = np.where(rng.random(len(donor)) < 0.5, 0.0, 1_234.0) + return donor + + +def _reporting_benunits(person, benunit, column): + reporters = person.loc[person[column] > 0, "person_benunit_id"] + return benunit.benunit_id.isin(set(reporters)).values + + +@settings(max_examples=60, deadline=None) +@given(datasets()) +def test_rules_zero_restore_and_touch_nothing_else(dataset): + before = dataset.copy() + donor = _donor(dataset) + after = apply_spi_donor_benefit_rules(dataset, donor) + + for column in SPI_DONOR_ZEROED_PERSON_VARIABLES: + assert (after.person[column] == 0).all(), column + for column in SPI_DONOR_RESTORED_PERSON_VARIABLES: + np.testing.assert_array_equal(after.person[column], donor[column]) + unchanged = [c for c in after.person.columns if c not in CHANGED | RULED] + pd.testing.assert_frame_equal(after.person[unchanged], before.person[unchanged]) + kept_flags = [c for c in after.benunit.columns if c not in CHANGED] + pd.testing.assert_frame_equal(after.benunit[kept_flags], before.benunit[kept_flags]) + pd.testing.assert_frame_equal(after.household, before.household) + pd.testing.assert_frame_equal(dataset.person, before.person) + pd.testing.assert_frame_equal(dataset.benunit, before.benunit) + + +@settings(max_examples=30, deadline=None) +@given(datasets()) +def test_restored_columns_untouched_without_donor(dataset): + after = apply_spi_donor_benefit_rules(dataset) + restored = list(SPI_DONOR_RESTORED_PERSON_VARIABLES) + pd.testing.assert_frame_equal(after.person[restored], dataset.person[restored]) + + +@settings( + max_examples=60, + deadline=None, + suppress_health_check=[HealthCheck.function_scoped_fixture], +) +@given(dataset=datasets()) +def test_flags_follow_own_reports_whatever_is_zeroed(dataset, monkeypatch): + # With the own-right and anchoring reports left in place, the rebuilt + # flags must follow them, not the donor's flags. + monkeypatch.setattr( + frs_only, + "SPI_DONOR_ZEROED_PERSON_VARIABLES", + [ + c + for c in SPI_DONOR_ZEROED_PERSON_VARIABLES + if c not in ANCHORING_REPORTS | set(BENEFITS_IN_OWN_RIGHT_REPORTED_COLUMNS) + ], + ) + after = apply_spi_donor_benefit_rules(dataset) + own = after.person[list(BENEFITS_IN_OWN_RIGHT_REPORTED_COLUMNS)].sum(axis=1) > 0 + assert (after.person.receives_benefits_in_own_right == own).all() + for flag in SPI_DONOR_REDRAWN_TAKEUP_FLAGS: + column = REPORTED_TAKEUP_ANCHORS[flag][1] + reports = _reporting_benunits(after.person, after.benunit, column) + assert after.benunit[flag].values[reports].all(), flag + + +@settings(max_examples=40, deadline=None) +@given(datasets()) +def test_rules_are_deterministic_and_idempotent(dataset): + donor = _donor(dataset) + once = apply_spi_donor_benefit_rules(dataset, donor) + again = apply_spi_donor_benefit_rules(dataset, donor) + twice = apply_spi_donor_benefit_rules(once, donor) + for frame in ("person", "benunit", "household"): + pd.testing.assert_frame_equal(getattr(once, frame), getattr(again, frame)) + pd.testing.assert_frame_equal(getattr(once, frame), getattr(twice, frame)) + + +@settings(max_examples=30, deadline=None) +@given(datasets()) +def test_draws_do_not_depend_on_other_columns(dataset): + full = apply_spi_donor_benefit_rules(dataset) + dataset.benunit = dataset.benunit.drop(columns=["would_claim_uc"]) + partial = apply_spi_donor_benefit_rules(dataset) + pd.testing.assert_series_equal( + full.benunit.would_claim_pc, partial.benunit.would_claim_pc + ) + + +def test_unreported_units_claim_at_the_take_up_rate(): + n = 40_000 + rng = np.random.default_rng(0) + reports = [rng.gamma(2, 1_000, n) for _ in REPORT_COLUMNS] + dataset = _dataset([1] * n, reports, np.ones(n, dtype=bool)) + after = apply_spi_donor_benefit_rules(dataset).benunit + for flag in SPI_DONOR_REDRAWN_TAKEUP_FLAGS: + rate = load_take_up_rate(REPORTED_TAKEUP_ANCHORS[flag][0], YEAR) + assert after[flag].mean() == pytest.approx(rate, abs=0.01), flag + assert after.would_claim_child_benefit.all() + + +def test_take_up_rates_are_read_for_the_dataset_year(monkeypatch): + years = [] + + def rate(name, year): + years.append(year) + return 0.5 + + monkeypatch.setattr("policyengine_uk_data.datasets.frs.load_take_up_rate", rate) + dataset = _dataset([1, 2], [[1.0] * 3 for _ in REPORT_COLUMNS], [True] * 3) + dataset = UKSingleYearDataset( + person=dataset.person, + benunit=dataset.benunit, + household=dataset.household, + fiscal_year=2031, + ) + apply_spi_donor_benefit_rules(dataset) + assert years == [2031] * len(SPI_DONOR_REDRAWN_TAKEUP_FLAGS) + + +def _people_dataset(people, seed=None) -> UKSingleYearDataset: + """One person per (benefit unit, age, claimant or partner) triple, every + unit reporting UC and entering with the donor's would_claim_uc. With a + ``seed``, both tables are shuffled.""" + units, ages, claimants = (np.asarray(column) for column in zip(*people)) + person = pd.DataFrame( + { + "person_id": np.arange(len(people)) * 5 + 11, + "person_benunit_id": units, + "person_household_id": units, + "age": ages, + "is_claimant_or_partner": claimants, + "universal_credit_reported": 1_000.0, + "pension_credit_reported": 0.0, + } + ) + benunit_ids = np.unique(units) + benunit = pd.DataFrame( + {"benunit_id": benunit_ids, "would_claim_uc": True, "would_claim_pc": False} + ) + if seed is not None: + rng = np.random.default_rng(seed) + person = person.iloc[rng.permutation(len(person))].reset_index(drop=True) + benunit = benunit.iloc[rng.permutation(len(benunit))].reset_index(drop=True) + household = pd.DataFrame({"household_id": benunit_ids, "household_weight": 0.0}) + return UKSingleYearDataset( + person=person, benunit=benunit, household=household, fiscal_year=YEAR + ) + + +def _everyone_takes_up(monkeypatch): + monkeypatch.setattr( + "policyengine_uk_data.datasets.frs.load_take_up_rate", lambda name, year: 1.0 + ) + + +def test_pension_age_units_never_get_would_claim_uc(monkeypatch): + """With every unit drawn to claim, exactly the units whose claimant and any + partner have all reached State Pension age lose would_claim_uc, judged by + the dataset's is_claimant_or_partner. Pension Credit is not affected.""" + _everyone_takes_up(monkeypatch) + # (benefit unit, age, claimant or partner): a pensioner couple, a mixed-age + # couple, a single pensioner, a working-age single, a pensioner living with + # a younger adult who is neither, and a pensioner with a child. + people = [ + (3, 70, True), + (3, 72, True), + (10, 70, True), + (10, 50, True), + (17, 80, True), + (24, 40, True), + (31, 75, True), + (31, 30, False), + (38, 68, True), + (38, 10, False), + ] + dataset = _people_dataset(people) + excluded = frs_only._all_claimants_over_state_pension_age(dataset, YEAR) + after = apply_spi_donor_benefit_rules( + dataset, uc_pension_age_excluded=excluded + ).benunit + assert dict(zip(after.benunit_id, after.would_claim_uc)) == { + 3: False, + 10: True, + 17: False, + 24: True, + 31: False, + 38: False, + } + assert after.would_claim_pc.all() + + +@st.composite +def units_clear_of_pension_age(draw): + """Units of one or two claimants, each clearly under (20 to 59) or over + (67 and up) State Pension age in 2024, plus up to two other members.""" + clear_age = st.one_of(st.integers(20, 59), st.integers(67, 95)) + people = [] + for unit in range(draw(st.integers(1, 8))): + for _ in range(draw(st.integers(1, 2))): + people.append((unit * 7 + 3, draw(clear_age), True)) + for _ in range(draw(st.integers(0, 2))): + people.append((unit * 7 + 3, draw(st.integers(0, 95)), False)) + return people + + +@settings( + max_examples=20, + deadline=None, + suppress_health_check=[HealthCheck.function_scoped_fixture], +) +@given(people=units_clear_of_pension_age(), seed=st.integers(0, 10**6)) +def test_uc_is_excluded_exactly_when_all_claimants_are_over_pension_age( + people, seed, monkeypatch +): + _everyone_takes_up(monkeypatch) + dataset = _people_dataset(people, seed) + excluded = frs_only._all_claimants_over_state_pension_age(dataset, YEAR) + after = apply_spi_donor_benefit_rules( + dataset, uc_pension_age_excluded=excluded + ).benunit + for unit, claims in zip(after.benunit_id, after.would_claim_uc): + ages = [age for u, age, claimant in people if u == unit and claimant] + assert claims == (min(ages) < 67), (unit, ages) + + +def test_stage_two_always_passes_the_pension_age_exclusion(monkeypatch): + """The pipeline computes the mask on its target and passes it to the rules.""" + dataset = _people_dataset([(3, 70, True), (10, 40, True)]) + expected = frs_only._all_claimants_over_state_pension_age(dataset, YEAR) + captured = [] + + def capture_rules(target, donor_person=None, *, uc_pension_age_excluded=None): + captured.append(uc_pension_age_excluded) + return target + + # Keep this a caller wiring check without fitting a QRF. + monkeypatch.setattr( + frs_only, "_impute_outputs", lambda train, target, outputs: target + ) + monkeypatch.setattr(frs_only, "apply_spi_donor_benefit_rules", capture_rules) + frs_only.impute_frs_only_variables(dataset, dataset) + + assert len(captured) == 1 + assert captured[0] is not None + np.testing.assert_array_equal(captured[0], expected) + np.testing.assert_array_equal(captured[0], [True, False]) + + +def test_stage_two_applies_the_rules_and_keeps_drawn_values(monkeypatch): + """Through ``impute_frs_only_variables``: zeroed, restored, kept and flags.""" + from policyengine_uk_data.tests.test_frs_only_imputation import _fake_dataset + + train = _fake_dataset(person_rows=400, seed=0) + rng = np.random.default_rng(1) + for column in ("esa_contrib_reported", "state_pension_reported", "iidb_reported"): + train.person[column] = np.where(rng.random(400) < 0.3, 5_000.0, 0.0) + target = _fake_dataset(person_rows=80, seed=1) + target.person["ssmg_reported"] = 600.0 + target.person["iidb_reported"] = np.where(rng.random(80) < 0.5, 0.0, 777.0) + target.person["receives_benefits_in_own_right"] = True + target.benunit["would_claim_uc"] = True + target.benunit["would_claim_child_benefit"] = False + + ruled = frs_only.impute_frs_only_variables(train, target) + monkeypatch.setattr(frs_only, "SPI_DONOR_ZEROED_PERSON_VARIABLES", []) + monkeypatch.setattr(frs_only, "SPI_DONOR_RESTORED_PERSON_VARIABLES", []) + unruled = frs_only.impute_frs_only_variables(train, target) + + for column in SPI_DONOR_ZEROED_PERSON_VARIABLES: + assert (ruled.person[column] == 0).all(), column + np.testing.assert_array_equal( + ruled.person.iidb_reported, target.person.iidb_reported + ) + assert not np.array_equal(unruled.person.iidb_reported, target.person.iidb_reported) + kept = [c for c in FRS_ONLY_PERSON_VARIABLES if c not in RULED] + pd.testing.assert_frame_equal(ruled.person[kept], unruled.person[kept]) + assert not ruled.person.receives_benefits_in_own_right.any() + assert not ruled.benunit.would_claim_child_benefit.any() + # Every unit entered claiming UC as the donor; with the reports zeroed, + # the redraw at the 55% rate leaves some 80 units out. + assert not ruled.benunit.would_claim_uc.all() + + # Disability flags come from the final reports: ESA (contributory) was + # drawn for some people but no longer marks them disabled. + assert (unruled.person.esa_contrib_reported > 0).any() + flags = ["is_disabled_for_benefits", "is_severely_disabled_for_benefits"] + recomputed = add_disability_benefit_flags_from_reported_amounts( + ruled.person.drop(columns=flags), int(str(ruled.time_period)[:4]) + ) + pd.testing.assert_frame_equal(ruled.person[flags], recomputed[flags]) + + +def test_council_tax_reduction_keeps_the_stage_two_draw(): + """SPI rows carry the QRF's CTR draw through unchanged, as before the + rules: not zeroed, not the donor's. Zeroing waits for #499.""" + from policyengine_uk_data.tests.test_frs_only_imputation import _fake_dataset + + train = _fake_dataset(person_rows=400, seed=0) + rng = np.random.default_rng(2) + train.person["council_tax_benefit_reported"] = np.where( + rng.random(400) < 0.3, 1_200.0, 0.0 + ) + target = _fake_dataset(person_rows=80, seed=1) + target.person["council_tax_benefit_reported"] = 999.0 + outputs = [c for c in FRS_ONLY_PERSON_VARIABLES if c in target.person.columns] + + # The draw alone, which is what stage two returned before the rules. + drawn = frs_only._impute_outputs(train, target.copy(), outputs).person + ruled = frs_only.impute_frs_only_variables(train, target).person + + np.testing.assert_array_equal( + ruled.council_tax_benefit_reported, drawn.council_tax_benefit_reported + ) + assert (drawn.council_tax_benefit_reported > 0).any() + assert (drawn.council_tax_benefit_reported != 999.0).any() diff --git a/policyengine_uk_data/tests/test_spi_income_earnings_groups.py b/policyengine_uk_data/tests/test_spi_income_earnings_groups.py new file mode 100644 index 000000000..44cf55de0 --- /dev/null +++ b/policyengine_uk_data/tests/test_spi_income_earnings_groups.py @@ -0,0 +1,522 @@ +"""SPI income draws are conditioned on earnings group. + +Invariants (Hypothesis properties unless noted): + +1. FRS rows: children (FRS child table or under 16) are ``NOT_IMPUTED``. + Everyone else is in exactly one earnings group, which has pay if and only + if the main job is as an employee or the FRS records pay, and a trade if + and only if the main job is self-employment or the FRS records a profit. +2. SPI records: the group has pay if and only if PAY + EPB + TAXTERM > 0, and + a trade if and only if SEINC_NUM = 1 or PROFITS > 0. +3. Both mappings are monotone (more of one income adds that source and leaves + the other alone) and give the same answer elementwise as row by row. +4. Training sample: every group with weight gets at least + ``MIN_GROUP_SAMPLE_SHARE`` of the nominal sample size, groups without + weight get none, and groups above the floor get their weighted share. + Floors are not offset elsewhere, so the total lies between the nominal size + (less rounding) and the nominal size plus one floor per group. +5. ``generate_spi_table`` resamples each group only from its own records. +6. Model draws: pay is positive exactly in the groups with pay, profit is zero + in the groups without a trade, and ``NOT_IMPUTED`` rows get no draw. +7. ``apply_income_draws`` overwrites drawn rows (missing draws become zero), + leaves ``NOT_IMPUTED`` rows and other columns alone, and does not mutate + its input. +8. A cached model in another format or for another grouping is retrained. +9. On a built enhanced FRS, SPI-synthetic rows' incomes agree with their + employment status (skipped when no build is present). +""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk_data.datasets.imputations import income as income_module +from policyengine_uk_data.datasets.imputations.income import ( + CHILD_STATUS, + EARNINGS_GROUPS, + EMPLOYEE, + EMPLOYEE_AND_SELF_EMPLOYED, + EMPLOYEE_STATUSES, + IMPUTATIONS, + MIN_GROUP_SAMPLE_SHARE, + NO_EARNINGS, + NOT_IMPUTED, + PREDICTORS, + SELF_EMPLOYED, + SELF_EMPLOYED_STATUSES, + EarningsGroupIncomeModel, + apply_income_draws, + earnings_group_sample_sizes, + frs_earnings_group, + generate_spi_table, + spi_earnings_group, +) + +PAY_GROUPS = {EMPLOYEE, EMPLOYEE_AND_SELF_EMPLOYED} +TRADE_GROUPS = {SELF_EMPLOYED, EMPLOYEE_AND_SELF_EMPLOYED} +NON_WORKING_STATUSES = ( + "UNEMPLOYED", + "RETIRED", + "STUDENT", + "CARER", + "LONG_TERM_DISABLED", + "SHORT_TERM_DISABLED", + "OTHER_INACTIVE", +) +STATUSES = ( + (CHILD_STATUS,) + EMPLOYEE_STATUSES + SELF_EMPLOYED_STATUSES + NON_WORKING_STATUSES +) +REGIONS = ("LONDON", "WALES", "SCOTLAND", "NORTH_EAST") + +# Generation and QRF draws are slow on a loaded runner; that is not a failure. +RELAXED = settings(deadline=None, suppress_health_check=[HealthCheck.too_slow]) + +amounts = st.one_of( + st.just(0.0), st.floats(0.01, 5e6, allow_nan=False, allow_infinity=False) +) + + +def test_statuses_are_policyengine_uk_enum_names(): + from policyengine_uk.variables.household.income.employment_status import ( + EmploymentStatus, + ) + + names = {status.name for status in EmploymentStatus} + # Every status the FRS build writes is covered: an employee, self-employed, + # child or out-of-work group. + assert set(STATUSES) == names + + +@st.composite +def frs_people(draw): + n = draw(st.integers(1, 40)) + return pd.DataFrame( + { + "employment_status": draw( + st.lists(st.sampled_from(STATUSES), min_size=n, max_size=n) + ), + "age": draw(st.lists(st.integers(0, 95), min_size=n, max_size=n)), + "employment_income": draw(st.lists(amounts, min_size=n, max_size=n)), + "self_employment_income": draw(st.lists(amounts, min_size=n, max_size=n)), + } + ) + + +def _frs_groups(people: pd.DataFrame) -> np.ndarray: + return frs_earnings_group( + people.employment_status, + people.age, + people.employment_income, + people.self_employment_income, + ) + + +@RELAXED +@given(frs_people()) +def test_frs_group_follows_status_and_recorded_earnings(people): + groups = _frs_groups(people) + status = people.employment_status.to_numpy() + child = (status == CHILD_STATUS) | (people.age.to_numpy() < 16) + + assert (groups[child] == NOT_IMPUTED).all() + adult = groups[~child] + assert np.isin(adult, EARNINGS_GROUPS).all() + + has_pay = np.isin(status, EMPLOYEE_STATUSES) | ( + people.employment_income.to_numpy() > 0 + ) + has_trade = np.isin(status, SELF_EMPLOYED_STATUSES) | ( + people.self_employment_income.to_numpy() > 0 + ) + np.testing.assert_array_equal(np.isin(adult, list(PAY_GROUPS)), has_pay[~child]) + np.testing.assert_array_equal(np.isin(adult, list(TRADE_GROUPS)), has_trade[~child]) + + +@RELAXED +@given(frs_people(), st.floats(0.01, 1e6), st.sampled_from(["pay", "profit"])) +def test_frs_group_monotone_in_income(people, extra, source): + before = _frs_groups(people) + column = "employment_income" if source == "pay" else "self_employment_income" + after = _frs_groups(people.assign(**{column: people[column] + extra})) + imputed = before != NOT_IMPUTED + np.testing.assert_array_equal(after == NOT_IMPUTED, ~imputed) + gained, other = ( + (PAY_GROUPS, TRADE_GROUPS) if source == "pay" else (TRADE_GROUPS, PAY_GROUPS) + ) + # The source that grew is now present; the other source is unchanged. + assert np.isin(after[imputed], list(gained)).all() + np.testing.assert_array_equal( + np.isin(after[imputed], list(other)), np.isin(before[imputed], list(other)) + ) + + +@RELAXED +@given(frs_people()) +def test_frs_group_elementwise_equals_rowwise(people): + vectorised = _frs_groups(people) + rowwise = [ + frs_earnings_group( + [row.employment_status], + [row.age], + [row.employment_income], + [row.self_employment_income], + )[0] + for row in people.itertuples() + ] + as_lists = frs_earnings_group( + people.employment_status.tolist(), + people.age.tolist(), + people.employment_income.tolist(), + people.self_employment_income.tolist(), + ) + assert list(vectorised) == rowwise == list(as_lists) + + +@RELAXED +@given( + st.lists( + st.tuples(amounts, amounts, st.sampled_from([-1, 0, 1])), + min_size=1, + max_size=40, + ) +) +def test_spi_group_follows_pay_and_self_employment_pages(records): + pay, profit, indicator = (np.array(column) for column in zip(*records)) + groups = spi_earnings_group(pay, profit, indicator) + assert np.isin(groups, EARNINGS_GROUPS).all() + np.testing.assert_array_equal(np.isin(groups, list(PAY_GROUPS)), pay > 0) + np.testing.assert_array_equal( + np.isin(groups, list(TRADE_GROUPS)), (indicator == 1) | (profit > 0) + ) + rowwise = [spi_earnings_group([p], [q], [i])[0] for p, q, i in records] + assert list(groups) == rowwise + + +@RELAXED +@given( + st.lists( + st.tuples(amounts, amounts, st.sampled_from([-1, 0, 1])), + min_size=1, + max_size=40, + ), + st.floats(0.01, 1e6), + st.sampled_from(["pay", "profit"]), +) +def test_spi_group_monotone_in_income(records, extra, source): + pay, profit, indicator = (np.array(column, dtype=float) for column in zip(*records)) + before = spi_earnings_group(pay, profit, indicator) + if source == "pay": + after = spi_earnings_group(pay + extra, profit, indicator) + gained, other = PAY_GROUPS, TRADE_GROUPS + else: + after = spi_earnings_group(pay, profit + extra, indicator) + gained, other = TRADE_GROUPS, PAY_GROUPS + assert np.isin(after, list(gained)).all() + np.testing.assert_array_equal( + np.isin(after, list(other)), np.isin(before, list(other)) + ) + + +@RELAXED +@given( + st.dictionaries( + st.sampled_from(EARNINGS_GROUPS), + st.one_of(st.just(0.0), st.floats(1e-3, 1e8)), + min_size=1, + ), + st.integers(1, 200_000), +) +def test_group_sample_sizes(weights, sample_size): + sizes = earnings_group_sample_sizes(weights, sample_size) + positive = {group for group, w in weights.items() if w > 0} + assert set(sizes) == positive + if not positive: + return + floor = int(np.ceil(MIN_GROUP_SAMPLE_SHARE * sample_size)) + total = sum(weights[group] for group in positive) + for group, size in sizes.items(): + share = sample_size * weights[group] / total + assert size >= floor + if share > floor + 1: + assert abs(size - share) <= 0.5 + 1e-9 + groups = len(positive) + assert sample_size - 0.5 * groups <= sum(sizes.values()) + assert sum(sizes.values()) <= sample_size + groups * (floor + 0.5) + assert sizes == earnings_group_sample_sizes(weights, sample_size) + + +def _raw_spi(rng: np.random.Generator, n: int) -> pd.DataFrame: + """A raw SPI-shaped frame with all four earnings groups.""" + group = rng.choice(EARNINGS_GROUPS, n) + has_pay = np.isin(group, list(PAY_GROUPS)) + has_trade = np.isin(group, list(TRADE_GROUPS)) + raw = { + column: np.zeros(n) + for column in set(income_module.SPI_RENAMES.values()) + | {"PAY", "EPB", "TAXTERM", "SEINC_NUM", "GIFTINV"} + } + raw["PAY"] = np.where(has_pay, rng.lognormal(10, 1, n), 0.0) + raw["EPB"] = np.where(has_pay & (rng.random(n) < 0.1), 500.0, 0.0) + # Three in ten traders make no assessable profit. + raw["PROFITS"] = np.where( + has_trade & (rng.random(n) < 0.7), rng.lognormal(9, 1, n), 0.0 + ) + raw["SEINC_NUM"] = has_trade.astype(int) + for column in ("INCBBS", "DIVIDENDS", "PENSION", "INCPROP", "GIFTAID", "GIFTINV"): + raw[column] = rng.exponential(1_000, n) * (rng.random(n) < 0.3) + raw["FACT"] = rng.uniform(1, 500, n) + raw["SEX"] = rng.choice([1, 2], n) + raw["GORCODE"] = rng.choice([1, 7, 10, 11], n) + raw["AGERANGE"] = rng.choice([1, 2, 3, 4, 5, 6, 7], n) + return pd.DataFrame(raw) + + +@settings(deadline=None, max_examples=25, suppress_health_check=[HealthCheck.too_slow]) +@given(st.integers(0, 2**31 - 1), st.integers(50, 2_000)) +def test_generate_spi_table_resamples_within_groups(seed, sample_size): + raw = _raw_spi(np.random.default_rng(seed), 600) + table = generate_spi_table(raw.copy(), seed=seed % 1000, sample_size=sample_size) + np.testing.assert_array_equal( + table.earnings_group.to_numpy(), + spi_earnings_group( + table.employment_income, table.self_employment_income, table.SEINC_NUM + ), + ) + raw_groups = spi_earnings_group( + raw.PAY + raw.EPB + raw.TAXTERM, raw.PROFITS, raw.SEINC_NUM + ) + weights = raw.FACT.groupby(raw_groups).sum().to_dict() + assert table.earnings_group.value_counts().to_dict() == ( + earnings_group_sample_sizes(weights, sample_size) + ) + + +@pytest.fixture(scope="module") +def fitted_model(): + """A real per-group QRF fitted through ``generate_spi_table``.""" + from policyengine_uk_data.utils.qrf import QRF + + table = generate_spi_table( + _raw_spi(np.random.default_rng(1), 4_000), seed=0, sample_size=2_000 + ) + models = {} + for group in EARNINGS_GROUPS: + training = table[table.earnings_group == group] + model = QRF() + model.fit(training[PREDICTORS], training[IMPUTATIONS]) + models[group] = model + return EarningsGroupIncomeModel(models, income_module.get_income_model_metadata()) + + +@st.composite +def model_inputs(draw): + n = draw(st.integers(1, 30)) + return pd.DataFrame( + { + "age": draw(st.lists(st.floats(16, 90), min_size=n, max_size=n)), + "gender": draw( + st.lists(st.sampled_from(["MALE", "FEMALE"]), min_size=n, max_size=n) + ), + "region": draw( + st.lists( + st.sampled_from(["NORTH_EAST", "LONDON", "WALES", "SCOTLAND"]), + min_size=n, + max_size=n, + ) + ), + "earnings_group": draw( + st.lists( + st.sampled_from(EARNINGS_GROUPS + (NOT_IMPUTED,)), + min_size=n, + max_size=n, + ) + ), + }, + index=draw( + st.lists(st.integers(0, 10**6), min_size=n, max_size=n, unique=True) + ), + ) + + +@settings( + deadline=None, + max_examples=40, + suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow], +) +@given(model_inputs()) +def test_model_draws_agree_with_group(fitted_model, inputs): + draws = fitted_model.predict(inputs) + assert list(draws.columns) == IMPUTATIONS + assert draws.index.equals(inputs.index) + groups = inputs.earnings_group.to_numpy() + drawn = groups != NOT_IMPUTED + assert draws[~drawn].isna().all().all() + assert draws[drawn].notna().all().all() + pay = draws.employment_income.to_numpy()[drawn] + profit = draws.self_employment_income.to_numpy()[drawn] + np.testing.assert_array_equal(pay > 0, np.isin(groups[drawn], list(PAY_GROUPS))) + assert (profit[~np.isin(groups[drawn], list(TRADE_GROUPS))] == 0).all() + assert (draws[drawn].to_numpy() >= 0).all() + + +@pytest.mark.parametrize("group", [SELF_EMPLOYED, EMPLOYEE_AND_SELF_EMPLOYED]) +def test_model_draws_some_profit_for_traders(fitted_model, group): + """Both trade groups draw zero and positive profits, as the SPI has both.""" + n = 400 + inputs = pd.DataFrame( + { + "age": np.linspace(20, 80, n), + "gender": ["MALE", "FEMALE"] * (n // 2), + "region": ["LONDON"] * n, + "earnings_group": [group] * n, + } + ) + profit = fitted_model.predict(inputs).self_employment_income + assert 0.3 < (profit > 0).mean() < 1 + + +@st.composite +def person_and_draws(draw): + n = draw(st.integers(1, 30)) + columns = ["employment_income", "dividend_income", "rent"] + person = pd.DataFrame( + {c: draw(st.lists(amounts, min_size=n, max_size=n)) for c in columns} + ) + draws = pd.DataFrame( + { + c: draw( + st.lists(st.one_of(amounts, st.just(np.nan)), min_size=n, max_size=n) + ) + for c in IMPUTATIONS + } + ) + groups = draw( + st.lists( + st.sampled_from(EARNINGS_GROUPS + (NOT_IMPUTED,)), min_size=n, max_size=n + ) + ) + outputs = draw( + st.lists(st.sampled_from(IMPUTATIONS), min_size=1, max_size=4, unique=True) + ) + return person, draws, np.array(groups, dtype=object), outputs + + +@RELAXED +@given(person_and_draws()) +def test_apply_income_draws(case): + person, draws, groups, outputs = case + before = person.copy() + result = apply_income_draws(person, draws, groups, outputs) + pd.testing.assert_frame_equal(person, before) + + kept = groups == NOT_IMPUTED + for column in outputs: + own = ( + before[column].to_numpy() + if column in before.columns + else np.zeros(len(before)) + ) + np.testing.assert_array_equal(result[column].to_numpy()[kept], own[kept]) + np.testing.assert_array_equal( + result[column].to_numpy()[~kept], + np.nan_to_num(draws[column].to_numpy(dtype=float)[~kept], nan=0.0), + ) + for column in set(before.columns) - set(outputs): + pd.testing.assert_series_equal(result[column], before[column]) + + +def test_old_single_model_cache_is_retrained(tmp_path, monkeypatch): + import pickle + from types import SimpleNamespace + + cache = tmp_path / "income_spi_2022_23.pkl" + old_metadata = { + key: value + for key, value in income_module.get_income_model_metadata().items() + if key not in ("earnings_groups", "min_group_sample_share") + } + with cache.open("wb") as f: + pickle.dump( + { + "model": SimpleNamespace(imputed_variables=list(IMPUTATIONS)), + "input_columns": PREDICTORS, + "metadata": old_metadata, + }, + f, + ) + sentinel = object() + monkeypatch.setattr(income_module, "INCOME_MODEL_PATH", cache) + monkeypatch.setattr(income_module, "save_imputation_models", lambda: sentinel) + assert income_module.create_income_model() is sentinel + + +def test_cache_missing_a_group_is_retrained(tmp_path, monkeypatch, fitted_model): + cache = tmp_path / "income_spi_2022_23.pkl" + partial = EarningsGroupIncomeModel( + {g: m for g, m in fitted_model.models.items() if g != SELF_EMPLOYED}, + income_module.get_income_model_metadata(), + ) + partial.save(cache) + sentinel = object() + monkeypatch.setattr(income_module, "INCOME_MODEL_PATH", cache) + monkeypatch.setattr(income_module, "save_imputation_models", lambda: sentinel) + assert income_module.create_income_model() is sentinel + + +def test_current_cache_round_trips(tmp_path, monkeypatch, fitted_model): + cache = tmp_path / "income_spi_2022_23.pkl" + fitted_model.save(cache) + monkeypatch.setattr(income_module, "INCOME_MODEL_PATH", cache) + monkeypatch.setattr( + income_module, + "save_imputation_models", + lambda: pytest.fail("a current cache should be reused"), + ) + loaded = income_module.create_income_model() + inputs = pd.DataFrame( + { + "age": [30.0, 50.0, 70.0, 10.0], + "gender": ["MALE", "FEMALE", "MALE", "FEMALE"], + "region": ["LONDON", "WALES", "SCOTLAND", "LONDON"], + "earnings_group": [EMPLOYEE, SELF_EMPLOYED, NO_EARNINGS, NOT_IMPUTED], + } + ) + pd.testing.assert_frame_equal(loaded.predict(inputs), fitted_model.predict(inputs)) + + +def test_built_enhanced_frs_spi_rows_agree_with_status(enhanced_frs): + person = enhanced_frs.person + household = enhanced_frs.household.set_index("household_id") + spi = person.person_household_id.map(household.household_is_spi_synthetic) + if not spi.any(): + pytest.skip("No SPI-synthetic rows in this build") + status = person.employment_status.astype(str) + rows = person[spi.to_numpy(dtype=bool)] + rows_status = status[spi.to_numpy(dtype=bool)] + + employees = rows_status.isin(EMPLOYEE_STATUSES) + assert (rows.employment_income[employees] > 0).all() + + children = rows_status.eq(CHILD_STATUS) + assert (rows.employment_income[children] == 0).all() + assert (rows.self_employment_income[children] == 0).all() + + # The SPI self-employed group draws a profit for about nine in ten + # (zero for traders who break even or make a loss); before the groups + # it was under one in ten. + self_employed = rows_status.isin(SELF_EMPLOYED_STATUSES) + if self_employed.sum() >= 50: + assert (rows.self_employment_income[self_employed] > 0).mean() > 0.6 + + # Out of work: earnings only where the FRS donor recorded some, which + # the FRS does for almost no one. + out_of_work = rows_status.isin(NON_WORKING_STATUSES) + if out_of_work.sum() >= 50: + earning = (rows.employment_income[out_of_work] > 0) | ( + rows.self_employment_income[out_of_work] > 0 + ) + assert earning.mean() < 0.02 diff --git a/policyengine_uk_data/tests/test_target_registry.py b/policyengine_uk_data/tests/test_target_registry.py index 22cf7b2d0..c2a1c1c37 100644 --- a/policyengine_uk_data/tests/test_target_registry.py +++ b/policyengine_uk_data/tests/test_target_registry.py @@ -81,6 +81,26 @@ def pe_count(variable): assert _compute_column(target, DummyCtx(), 2025) == [1, 0, 1] +def test_hmrc_salary_sacrifice_relief_targets(): + """HMRC Table 6.1/6.2 relief targets reach the registry (uk-data: 410 Gone + on the old CSV dropped them from every build without failing it).""" + targets = {t.name: t for t in get_all_targets(year=2025)} + for name in ( + "hmrc/salary_sacrifice_it_relief_basic_rate", + "hmrc/salary_sacrifice_it_relief_higher_rate", + "hmrc/salary_sacrifice_it_relief_additional_rate", + "hmrc/salary_sacrifice_employee_nics_relief", + "hmrc/salary_sacrifice_employer_nics_relief", + ): + assert name in targets, f"{name} missing from the target registry" + # The bands, not HMRC's separately rounded total, are the targets. + assert "hmrc/salary_sacrifice_it_relief_total" not in targets + # One NICs relief target per class: the OBR-labelled copies of the + # 2023-24 figures are gone. + assert "obr/salary_sacrifice_employee_ni_relief" not in targets + assert "obr/salary_sacrifice_employer_ni_relief" not in targets + + def test_voa_council_tax_targets(): """VOA council tax band targets should exist.""" targets = get_all_targets(year=2025) diff --git a/policyengine_uk_data/tests/test_uc_gainful_self_employment.py b/policyengine_uk_data/tests/test_uc_gainful_self_employment.py new file mode 100644 index 000000000..48766685e --- /dev/null +++ b/policyengine_uk_data/tests/test_uc_gainful_self_employment.py @@ -0,0 +1,233 @@ +import numpy as np +import pandas as pd +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st +from policyengine_uk.variables.household.income.employment_status import ( + EmploymentStatus, +) + +from policyengine_uk_data.datasets.frs import ( + SELF_EMPLOYED_STATUSES, + derive_uc_is_in_gainful_self_employment, +) +from policyengine_uk_data.tests.test_imputation_source_flags import ( + _FakeDataset, + _stack_without_remapping, +) + +STATUSES = [status.name for status in EmploymentStatus] +GAINFUL = "uc_is_in_gainful_self_employment" +incomes = st.floats(0, 1e6, allow_nan=False) + + +def derive(status, profit, pay): + return derive_uc_is_in_gainful_self_employment([status], [profit], [pay])[0] + + +def oracle(status, profit, pay): + # Written out per person, independently of the vectorised helper. + if status == "FT_SELF_EMPLOYED" or status == "PT_SELF_EMPLOYED": + return True + return profit > 0 and profit > pay + + +def test_self_employed_statuses_are_model_enum_members(): + assert set(SELF_EMPLOYED_STATUSES) <= set(STATUSES) + + +@pytest.mark.parametrize("status", STATUSES) +@pytest.mark.parametrize("profit", [0.0, 5_000.0]) +def test_every_status_without_a_side_trade_that_out_earns_pay(status, profit): + # Pay at or above profit: only the main-job status decides. + expected = status in ("FT_SELF_EMPLOYED", "PT_SELF_EMPLOYED") + assert derive(status, profit, 10_000.0) == expected + + +@pytest.mark.parametrize("profit", [0.0, 1.0, 20_000.0]) +def test_self_employed_main_job_is_gainful_whatever_the_profit(profit): + # A loss reaches this build floored at zero, so zero covers losses too. + for status in SELF_EMPLOYED_STATUSES: + assert derive(status, profit, 0.0) + assert derive(status, profit, 50_000.0) + + +def test_side_trade_counts_only_when_it_out_earns_pay(): + # ADM H4034 example 1 (Jos): more employed hours, more self-employed pay. + assert derive("PT_EMPLOYED", 140 * 52, 80 * 52) + # ADM H4035 example 2 (Ann): the job earns more. + assert not derive("PT_EMPLOYED", 40 * 52, 49.6 * 52) + assert not derive("FT_EMPLOYED", 10_000.0, 10_000.0) + assert not derive("FT_EMPLOYED", 0.0, 0.0) + assert derive("UNEMPLOYED", 1.0, 0.0) + + +@pytest.mark.parametrize("status", STATUSES) +@pytest.mark.parametrize( + "profit, pay", + [(0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (1.0, 1.0), (2.0, 1.0), (1.0, 2.0)], +) +def test_truth_table_against_oracle(status, profit, pay): + assert derive(status, profit, pay) == oracle(status, profit, pay) + + +@settings(max_examples=500, deadline=None) +@given(st.sampled_from(STATUSES), incomes, incomes) +def test_invariants(status, profit, pay): + gainful = derive(status, profit, pay) + # Every self-employed main job is gainful. + if status in SELF_EMPLOYED_STATUSES: + assert gainful + # Nobody else is gainful without a profit above their pay. + if gainful: + assert status in SELF_EMPLOYED_STATUSES or profit > max(pay, 0) + # More profit never removes the flag; more pay never adds it. + assert derive(status, profit * 2 + 1, pay) >= gainful + assert derive(status, profit, pay * 2 + 1) <= gainful + + +@settings(max_examples=100, deadline=None) +@given(st.lists(st.tuples(st.sampled_from(STATUSES), incomes, incomes), max_size=40)) +def test_vectorised_matches_elementwise(rows): + statuses = [r[0] for r in rows] + profits = [r[1] for r in rows] + pays = [r[2] for r in rows] + result = derive_uc_is_in_gainful_self_employment(statuses, profits, pays) + assert result.dtype == bool + assert result.tolist() == [oracle(*r) for r in rows] + np.testing.assert_array_equal( + derive_uc_is_in_gainful_self_employment( + pd.Series(statuses, dtype="category"), + pd.Series(profits, dtype=float), + pd.Series(pays, dtype=float), + ), + result, + ) + + +def _frs_like_dataset(statuses, profits, pays): + n = len(statuses) + person = pd.DataFrame( + { + "person_id": np.arange(1, n + 1), + "person_household_id": np.arange(1, n + 1), + "person_benunit_id": np.arange(1, n + 1), + "employment_status": statuses, + "employment_income": pays, + "self_employment_income": profits, + "savings_interest_income": 0.0, + "dividend_income": 0.0, + "private_pension_income": 0.0, + "property_income": 0.0, + } + ) + person[GAINFUL] = derive_uc_is_in_gainful_self_employment( + person.employment_status, + person.self_employment_income, + person.employment_income, + ) + household = pd.DataFrame( + { + "household_id": np.arange(1, n + 1), + "household_weight": 1.0, + "region": "LONDON", + } + ) + return _FakeDataset(person=person, household=household) + + +@settings(max_examples=50, deadline=None) +@given( + st.lists( + st.tuples(st.sampled_from(STATUSES), incomes, incomes, incomes, incomes), + min_size=1, + max_size=10, + ) +) +def test_spi_copy_flag_follows_its_own_imputed_incomes(rows): + from policyengine_uk_data.datasets import disability_benefits + from policyengine_uk_data.datasets.imputations import frs_only + from policyengine_uk_data.datasets.imputations import income as income_module + + imputed_profit = [r[3] for r in rows] + imputed_pay = [r[4] for r in rows] + + def impute_over_incomes(dataset, _model, output_variables): + dataset = dataset.copy() + if "self_employment_income" in output_variables: + dataset.person["self_employment_income"] = imputed_profit + dataset.person["employment_income"] = imputed_pay + return dataset + + with pytest.MonkeyPatch.context() as m: + m.setattr(income_module, "create_income_model", lambda: object()) + m.setattr(income_module, "subsample_dataset", lambda d, _n: d.copy()) + m.setattr(income_module, "impute_over_incomes", impute_over_incomes) + m.setattr( + frs_only, + "impute_frs_only_variables", + lambda train_dataset, target_dataset: target_dataset, + ) + m.setattr( + disability_benefits, + "strip_internal_disability_reported_amounts", + lambda dataset: dataset, + ) + m.setattr(income_module, "stack_datasets", _stack_without_remapping) + result = income_module.impute_income( + _frs_like_dataset( + [r[0] for r in rows], [r[1] for r in rows], [r[2] for r in rows] + ) + ) + + person = result.person + n = len(rows) + assert len(person) == 2 * n + # The SPI copy kept the donors' statuses and took the imputed incomes. + assert person.self_employment_income.iloc[n:].tolist() == imputed_profit + assert person[GAINFUL].tolist() == [ + oracle(*values) + for values in zip( + person.employment_status, + person.self_employment_income, + person.employment_income, + ) + ] + + +def test_frs_only_stage_leaves_the_rule_inputs_alone(): + # The second-stage QRF rewrites these columns on the SPI copy after the + # flag's inputs are set; none of them may be an input to the flag. + from policyengine_uk_data.datasets.imputations.frs_only import ( + FRS_ONLY_PERSON_VARIABLES, + ) + + rule_columns = { + GAINFUL, + "employment_status", + "employment_income", + "self_employment_income", + } + assert rule_columns.isdisjoint(FRS_ONLY_PERSON_VARIABLES) + + +@pytest.mark.parametrize("fixture", ["frs", "enhanced_frs"]) +def test_built_dataset_flag(fixture, request): + # Skips only when no built dataset exists; a build without the column fails. + dataset = request.getfixturevalue(fixture) + person = dataset.person + assert GAINFUL in person.columns, f"{fixture} lacks {GAINFUL}: rebuild it" + gainful = person[GAINFUL].to_numpy(dtype=bool) + self_employed = np.isin(person.employment_status, SELF_EMPLOYED_STATUSES) + assert gainful[self_employed].all() + profit = person.self_employment_income.to_numpy() + assert (profit[gainful & ~self_employed] > 0).all() + if fixture == "frs": + # Later enhanced-FRS stages reprice incomes, so the exact rule holds on + # the base build only. + np.testing.assert_array_equal( + gainful, + derive_uc_is_in_gainful_self_employment( + person.employment_status, profit, person.employment_income + ), + ) diff --git a/policyengine_uk_data/tests/test_uc_payment_distribution_targets.py b/policyengine_uk_data/tests/test_uc_payment_distribution_targets.py new file mode 100644 index 000000000..6b0ff8888 --- /dev/null +++ b/policyengine_uk_data/tests/test_uc_payment_distribution_targets.py @@ -0,0 +1,189 @@ +"""Tests for the DWP UC payment distribution targets. + +The Stat-Xplore extract (storage/uc_national_payment_dist.xlsx, May 2025) +counts households on UC by monthly award band and family type. Its top band, +'£2500.01 or over', is open-ended; it used to parse to NaN bounds, so its four +targets (1.4k to 83k households) had a column of zeros and could never be met. +""" + +from types import SimpleNamespace + +import numpy as np +import pandas as pd +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_uk_data.storage import STORAGE_FOLDER +from policyengine_uk_data.targets.compute.benefits import compute_uc_payment_dist +from policyengine_uk_data.targets.sources.dwp import _uc_payment_distribution_targets +from policyengine_uk_data.utils.uc_data import ( + _check_bands_disjoint, + parse_monthly_award_band, + uc_national_payment_dist, +) + +FAMILY_TYPES = { + "Single, no children": "SINGLE", + "Single, with children": "LONE_PARENT", + "Couple, no children": "COUPLE_NO_CHILDREN", + "Couple, with children": "COUPLE_WITH_CHILDREN", +} + + +def _raw_extract() -> pd.DataFrame: + """Household counts indexed by award band, one column per family type.""" + raw = pd.read_excel(STORAGE_FOLDER / "uc_national_payment_dist.xlsx", header=None) + counts = raw.iloc[9:, 3:7] + counts.index = raw.iloc[9:, 1] + counts.columns = raw.iloc[7, 3:7] + return counts + + +def test_parse_monthly_award_band(): + assert parse_monthly_award_band("£0.01 to £100.00") == (0, 1_200) + assert parse_monthly_award_band("£100.01 to £200.00") == (1_200, 2_400) + assert parse_monthly_award_band("£1000.01 to £1100.00") == (12_000, 13_200) + assert parse_monthly_award_band("£2,500.01 or over") == (30_000, np.inf) + with pytest.raises(ValueError): + parse_monthly_award_band("No payment") + + +@pytest.mark.parametrize( + "band", + ["£nan or over", "£inf or over", "£100.01 to £99.00", "£1.00 to £nan"], +) +def test_invalid_bands_are_rejected(band): + with pytest.raises(ValueError): + parse_monthly_award_band(band) + + +def test_top_band_targets_are_reachable(): + targets = {t.name: t for t in _uc_payment_distribution_targets()} + top = _raw_extract().loc["£2500.01 or over"] + for label, family_type in FAMILY_TYPES.items(): + target = targets[ + f"dwp/uc_payment_dist/{family_type}_annual_payment_30_000_to_inf" + ] + assert target.lower_bound == 30_000 + assert target.upper_bound == np.inf + assert target.values[2025] == top[label] + for target in targets.values(): + assert not np.isnan(target.lower_bound) + assert not np.isnan(target.upper_bound) + assert "nan" not in target.name + + +def test_band_counts_sum_to_households_with_a_payment(): + """Conservation: the bands hold every household with a payment, once. + + Stat-Xplore perturbs each cell for disclosure control, so a total can + differ from the sum of its cells by a few households. + """ + raw = _raw_extract() + for label, family_type in FAMILY_TYPES.items(): + with_payment = raw.loc["Total", label] - raw.loc["No payment", label] + parsed = uc_national_payment_dist[ + uc_national_payment_dist.family_type == family_type + ] + assert abs(parsed.household_count.sum() - with_payment) <= 10, label + + +def test_bands_tile_the_positive_awards(): + for family_type, bands in uc_national_payment_dist.groupby("family_type"): + bands = bands.sort_values("uc_annual_payment_min") + lower = bands.uc_annual_payment_min.to_numpy() + upper = bands.uc_annual_payment_max.to_numpy() + assert lower[0] == 0, family_type + assert np.array_equal(lower[1:], upper[:-1]), family_type + assert upper[-1] == np.inf, family_type + + +def test_overlapping_summary_band_is_rejected(): + bands = pd.DataFrame( + { + "family_type": ["SINGLE"] * 3, + "uc_annual_payment_min": [18_000.0, 19_200.0, 18_000.0], + "uc_annual_payment_max": [19_200.0, 20_400.0, np.inf], + } + ) + with pytest.raises(ValueError): + _check_bands_disjoint(bands) + + +def _fake_ctx(uc, family_type, household): + def calculate(variable, map_to=None): + values = {"universal_credit": uc, "family_type": family_type}[variable] + return SimpleNamespace(values=values) + + n_households = household.max() + 1 + return SimpleNamespace( + sim=SimpleNamespace(calculate=calculate), + household_from_family=lambda values: np.bincount( + household, weights=np.asarray(values, float), minlength=n_households + ), + ) + + +_TARGETS = _uc_payment_distribution_targets() +_EDGES = sorted({t.upper_bound for t in _TARGETS if np.isfinite(t.upper_bound)}) +_AWARDS = st.one_of( + st.just(0.0), # no payment + st.floats(0, 1e6, allow_nan=False), # anywhere, including the open top band + st.sampled_from(_EDGES), # exactly on a band edge + st.sampled_from(_EDGES).map(lambda edge: np.nextafter(edge, np.inf)), + st.sampled_from(_EDGES).map(lambda edge: np.nextafter(edge, -np.inf)), + st.sampled_from(_EDGES).map(lambda e: np.nextafter(np.float32(e), np.inf)), + st.sampled_from(_EDGES).map(lambda e: np.nextafter(np.float32(e), -np.inf)), +) +_BENEFIT_UNITS = st.lists( + st.tuples(_AWARDS, st.sampled_from(list(FAMILY_TYPES.values())), st.integers(0, 9)), + min_size=1, + max_size=60, +) + + +def _oracle_band(uc, family_type): + """Target name of the band holding each award (None for no payment), found + by searching the family type's sorted upper bounds rather than by the + compute function's comparisons.""" + names = np.full(len(uc), None, dtype=object) + for ft in np.unique(family_type): + bands = sorted( + (t.upper_bound, t.lower_bound, t.name) + for t in _TARGETS + if t.name.removeprefix("dwp/uc_payment_dist/").startswith(ft + "_annual") + ) + upper = np.array([b[0] for b in bands]) + m = family_type == ft + i = np.searchsorted(upper, uc[m], side="left") + names[m] = [ + bands[k][2] if k < len(bands) and award > bands[k][1] else None + for k, award in zip(i, uc[m]) + ] + return names + + +@settings(max_examples=200, deadline=None) +@given(_BENEFIT_UNITS, st.booleans()) +def test_each_award_lands_in_its_own_band(benefit_units, as_float32): + """Property: every target's column counts exactly the household's benefit + units whose award an interval search places in that target's band (so no + award is counted twice, moved to a neighbouring band, or lost), for float64 + and float32 awards, on band edges and one ulp either side.""" + uc, family_type, household = (np.array(column) for column in zip(*benefit_units)) + uc = uc.astype(np.float32 if as_float32 else float) + ctx = _fake_ctx(uc, family_type, household) + band = _oracle_band(uc, family_type) + + for target in _TARGETS: + expected = np.bincount( + household, + weights=(band == target.name).astype(float), + minlength=household.max() + 1, + ) + np.testing.assert_array_equal( + compute_uc_payment_dist(target, ctx), expected, err_msg=target.name + ) + # Every positive award is in some band; no payment is in none. + assert all((b is not None) == (award > 0) for b, award in zip(band, uc)) diff --git a/policyengine_uk_data/tests/test_would_claim_uc_pension_age.py b/policyengine_uk_data/tests/test_would_claim_uc_pension_age.py new file mode 100644 index 000000000..b6a4f634d --- /dev/null +++ b/policyengine_uk_data/tests/test_would_claim_uc_pension_age.py @@ -0,0 +1,66 @@ +"""Wholly pension-age benefit units never get would_claim_uc.""" + +import numpy as np +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_uk_data.datasets.frs import ( + derive_all_claimants_over_state_pension_age, +) +from policyengine_uk_data.utils.benefit_units import claimant_or_partner_variable + + +def test_examples(): + # Units: pensioner couple, mixed-age couple, working-age single, a + # pensioner and a younger person flagged as claimant or partner, a unit + # with no claimant, and a unit with no members at all. + result = derive_all_claimants_over_state_pension_age( + person_benunit_ids=[10, 10, 20, 20, 30, 40, 40, 50], + is_claimant_or_partner=[1, 1, 1, 1, 1, 1, 1, 0], + is_over_state_pension_age=[1, 1, 1, 0, 0, 1, 0, 0], + benunit_ids=[10, 20, 30, 40, 50, 60], + ) + np.testing.assert_array_equal(result, [True, False, False, False, False, False]) + + +@settings(max_examples=300, deadline=None) +@given( + st.lists( + st.tuples(st.integers(0, 6), st.booleans(), st.booleans()), + max_size=30, + ), + st.permutations(list(range(8))), +) +def test_matches_a_direct_definition(people, benunit_order): + """Differential check against a per-unit loop, for any unit order.""" + benunit_ids = np.array(benunit_order) + result = derive_all_claimants_over_state_pension_age( + person_benunit_ids=[p[0] for p in people], + is_claimant_or_partner=[p[1] for p in people], + is_over_state_pension_age=[p[2] for p in people], + benunit_ids=benunit_ids, + ) + for i, unit in enumerate(benunit_ids): + adults = [over for b, adult, over in people if b == unit and adult] + assert result[i] == (len(adults) > 0 and all(adults)) + + +def test_claimant_or_partner_variable_prefers_the_legal_flag(): + assert claimant_or_partner_variable({"is_adult": 0}) == "is_adult" + assert ( + claimant_or_partner_variable({"is_adult": 0, "is_claimant_or_partner": 0}) + == "is_claimant_or_partner" + ) + + +def test_built_dataset_has_no_pension_age_uc_claimants(baseline): + year = 2025 + claimant = claimant_or_partner_variable(baseline.tax_benefit_system.variables) + adult = baseline.calculate(claimant, year).values.astype(bool) + over = adult & baseline.calculate("is_SP_age", year).values.astype(bool) + adults = baseline.map_result(adult.astype(float), "person", "benunit") + adults_over = baseline.map_result(over.astype(float), "person", "benunit") + wholly_pension_age = (adults > 0) & (adults_over == adults) + would_claim_uc = baseline.calculate("would_claim_uc", year).values.astype(bool) + assert wholly_pension_age.any() + assert not (would_claim_uc & wholly_pension_age).any() diff --git a/policyengine_uk_data/utils/benefit_units.py b/policyengine_uk_data/utils/benefit_units.py new file mode 100644 index 000000000..fd098770b --- /dev/null +++ b/policyengine_uk_data/utils/benefit_units.py @@ -0,0 +1,15 @@ +"""Benefit-unit membership helpers shared by the dataset build and targets.""" + + +def claimant_or_partner_variable(variables) -> str: + """Name of the policyengine-uk variable marking a benefit unit's claimant + and any partner. + + Releases that define ``is_claimant_or_partner`` use it for Housing + Benefit's pension-age route. Earlier releases, such as 2.102.5, use + ``is_adult`` (age 18 or over) there, which also counts an 18 or 19 year + old dependant. + """ + if "is_claimant_or_partner" in variables: + return "is_claimant_or_partner" + return "is_adult" diff --git a/policyengine_uk_data/utils/calibrate.py b/policyengine_uk_data/utils/calibrate.py index a31dcc1fb..f97c4e1ef 100644 --- a/policyengine_uk_data/utils/calibrate.py +++ b/policyengine_uk_data/utils/calibrate.py @@ -349,8 +349,11 @@ def loss(w, validation: bool = False): mask = validation_targets_national else: mask = ~validation_targets_national - pred_national = pred_national[mask] - mse_national = torch.mean(sre(pred_national, y_national[mask])) + if mask.any(): + mse_national = torch.mean(sre(pred_national[mask], y_national[mask])) + else: + # Validation targets can all be local; an empty mean is NaN. + mse_national = pred_national.sum() * 0 else: mse_national = torch.mean(sre(pred_national, y_national)) diff --git a/policyengine_uk_data/utils/employment_status.py b/policyengine_uk_data/utils/employment_status.py new file mode 100644 index 000000000..950cbc49f --- /dev/null +++ b/policyengine_uk_data/utils/employment_status.py @@ -0,0 +1,7 @@ +"""Groups of policyengine-uk ``EmploymentStatus`` names (the FRS ILO main-job status).""" + +EMPLOYEE_STATUSES = ("FT_EMPLOYED", "PT_EMPLOYED") +SELF_EMPLOYED_STATUSES = ("FT_SELF_EMPLOYED", "PT_SELF_EMPLOYED") +# The FRS child table: dependent children, including 16-19-year-olds in +# non-advanced education. +CHILD_STATUS = "CHILD" diff --git a/policyengine_uk_data/utils/stack.py b/policyengine_uk_data/utils/stack.py index 2fe9df828..d722d347e 100644 --- a/policyengine_uk_data/utils/stack.py +++ b/policyengine_uk_data/utils/stack.py @@ -1,10 +1,17 @@ from policyengine_uk.data import UKSingleYearDataset import pandas as pd +# Benefit-unit roles taken from the survey, which policyengine-uk otherwise +# infers from ages. A stacked table without them would leave missing roles. +SURVEY_ROLE_COLUMNS = ("is_claimant_or_partner",) + def stack_datasets( data_1: UKSingleYearDataset, data_2: UKSingleYearDataset ) -> UKSingleYearDataset: + for column in SURVEY_ROLE_COLUMNS: + if (column in data_1.person.columns) != (column in data_2.person.columns): + raise ValueError(f"Only one of the stacked datasets has {column}.") person_id_offset = data_1.person.person_id.max() + 1 benunit_id_offset = data_1.benunit.benunit_id.max() + 1 household_id_offset = data_1.household.household_id.max() + 1 diff --git a/policyengine_uk_data/utils/takeup.py b/policyengine_uk_data/utils/takeup.py index ab5f6241c..965c11aab 100644 --- a/policyengine_uk_data/utils/takeup.py +++ b/policyengine_uk_data/utils/takeup.py @@ -57,3 +57,30 @@ def assign_takeup_with_reported_anchors( adjusted_rate = remaining_needed / int(non_reporters.sum()) result |= non_reporters & (draws < adjusted_rate) return result + + +def solve_fill_probability( + rate: float, + weights: np.ndarray, + eligible: np.ndarray, + reported: np.ndarray, +) -> float: + """Claim probability for eligible non-reporters that makes weighted + take-up among eligible entities equal ``rate`` in expectation. + + Reporters always claim. If they alone exceed ``rate`` the probability is + 0; if every eligible non-reporter claiming still falls short it is 1. + """ + weights = np.asarray(weights, dtype=np.float64) + eligible = np.asarray(eligible, dtype=bool) + reported = np.asarray(reported, dtype=bool) + eligible_weight = weights[eligible].sum() + reporting_weight = weights[eligible & reported].sum() + remaining_weight = weights[eligible & ~reported].sum() + if remaining_weight <= 0: + return 0.0 + needed = float(rate) * eligible_weight - reporting_weight + # A vanishing remaining weight sends the ratio to infinity; clip handles it. + with np.errstate(over="ignore", divide="ignore"): + probability = needed / remaining_weight + return float(np.clip(probability, 0.0, 1.0)) diff --git a/policyengine_uk_data/utils/uc_data.py b/policyengine_uk_data/utils/uc_data.py index 7bacebc57..ffe20896e 100644 --- a/policyengine_uk_data/utils/uc_data.py +++ b/policyengine_uk_data/utils/uc_data.py @@ -1,7 +1,49 @@ +import numpy as np import pandas as pd from pathlib import Path +def parse_monthly_award_band(band: str) -> tuple[float, float]: + """Annual (lower, upper] payment bounds of a Stat-Xplore monthly award band. + + Awards are whole pence, so the band '£100.01 to £200.00' holds monthly + awards over £100.00 and up to £200.00: annual bounds (1,200, 2,400]. The + lower bound is the previous band's top, so consecutive bands meet with no + gap. The open top band '£2500.01 or over' is (30,000, inf). + """ + text = band.replace("£", "").replace(",", "").strip() + if text.endswith(" or over"): + lower, upper = float(text.removesuffix(" or over")), np.inf + else: + parts = text.split(" to ") + if len(parts) != 2: + raise ValueError(f"Unrecognised UC monthly award band: {band!r}") + lower, upper = float(parts[0]), float(parts[1]) * 12 + lower = round((lower - 0.01) * 12, 2) + if not (np.isfinite(lower) and lower >= 0 and upper > lower): + raise ValueError(f"Invalid UC monthly award band: {band!r}") + return lower, upper + + +def _check_bands_disjoint(bands: pd.DataFrame) -> None: + """Fail if any family type's payment bands overlap. + + Stat-Xplore's '£1500.01 or over' band is its top band for months up to + August 2022 and spans the finer bands added from September 2022. It is + suppressed ('..') in the committed extract; if a new extract filled it + in, counting it as well would double count those households. + """ + for family_type, group in bands.groupby("family_type"): + group = group.sort_values("uc_annual_payment_min") + lower = group.uc_annual_payment_min.to_numpy() + upper = group.uc_annual_payment_max.to_numpy() + if (lower[1:] < upper[:-1]).any(): + raise ValueError( + f"UC payment bands for {family_type} overlap: check for " + "summary bands spanning finer ones" + ) + + def _parse_uc_national_payment_dist(): """Parse UC national payment distribution into long format.""" storage_path = Path(__file__).parent.parent / "storage" @@ -44,17 +86,10 @@ def _parse_uc_national_payment_dist(): result_df = pd.DataFrame(data_rows) - # Parse monthly band into min and max, then convert to annual - def parse_band(band): - """Parse band like '£100.01 to £200.00' into (min, max).""" - parts = band.replace("£", "").replace(",", "").split(" to ") - if len(parts) == 2: - return float(parts[0]) * 12, float(parts[1]) * 12 - return None, None - - result_df[["uc_annual_payment_min", "uc_annual_payment_max"]] = result_df[ - "monthly_award_band" - ].apply(lambda x: pd.Series(parse_band(x))) + result_df[["uc_annual_payment_min", "uc_annual_payment_max"]] = [ + parse_monthly_award_band(band) for band in result_df["monthly_award_band"] + ] + _check_bands_disjoint(result_df) # Map family types to constant names family_type_mapping = { diff --git a/pyproject.toml b/pyproject.toml index beff7f1a4..453fbacf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ authors = [ license = {file = "LICENSE"} requires-python = ">=3.13" dependencies = [ - "policyengine-core>=3.19.4", + "policyengine-core>=3.32.13", "requests", "tqdm", "tabulate", @@ -21,7 +21,7 @@ dependencies = [ "policyengine", "google-cloud-storage", "google-auth", - "policyengine-uk>=2.93.0", + "policyengine-uk>=2.122.0", "microcalibrate>=0.18.0", "microimpute>=1.0.1", "ruff>=0.9.0", @@ -45,8 +45,9 @@ dev = [ "yaml-changelog>=0.1.7", "itables", "quantile-forest", - "build", "towncrier>=24.8.0", - + "build", + "towncrier>=24.8.0", + "hypothesis>=6.168.3", ] [tool.setuptools] diff --git a/tools/brma_households/README.md b/tools/brma_households/README.md new file mode 100644 index 000000000..08206015a --- /dev/null +++ b/tools/brma_households/README.md @@ -0,0 +1,60 @@ +# BRMA private-rented households + +`build.py` writes `policyengine_uk_data/storage/brma_private_rented_households.csv` (936 rows: `region,brma,bedrooms,households`). The table counts private-rented households (private landlord or letting agency, plus other private rented) by Broad Rental Market Area (BRMA), keeping each area's census region. Bedrooms are `1`, `2`, `3` or `4+`; Northern Ireland uses `all` because its census did not ask about bedrooms. Sources, method and validation are summarised in `policyengine_uk_data/storage/BRMA_DATA_SOURCES.md`. + +## Method + +- **England, Census 2021.** + - Each LSOA's TS054 private-rented total is split by that LSOA's bedroom mix for ONS custom-API tenure category 3, "private rented or lives rent free". + - The LSOA's ONS population-weighted centroid places it in a VOA May 2020 BRMA polygon. + - Regions come from the ONS OA-to-LSOA and OA-to-region lookups. + - The GML is parsed directly so that nine MultiPolygons declared as Polygons keep all their parts. `make_valid` repairs seven self-intersections. +- **Wales, Census 2021.** + - Same method as England, using the archived Rent Officers Wales polygons (May 2012 geometry, September 2014 release). + - Eleven Welsh LSOAs fall in West Cheshire and keep region `WALES`. +- **Scotland, Census 2022.** + - NRS ward tenure × bedrooms tables are split across BRMAs by output-area household counts. Each output area is placed by its population-weighted centroid in the Scottish Government BRMA polygons. + - Official postcode candidates assign nine output areas whose centroids fall outside every polygon. + - Rent Service Scotland's postcode lookup moves 61 Balloch output areas to West Dunbartonshire: 59 on unique postcode matches and 2 via the council's own lookup. + - Four and five-or-more bedrooms are combined into `4+`. +- **Northern Ireland, Census 2021.** + - NISRA households by postcode district are summed into NIHE's postcode-district BRMAs. + - The totals are scaled by Northern Ireland's private-rented share from NISRA tenure by Data Zone. + - A private-rented split by area would need ONS Postcode Directory Northern Ireland records, which are under the LPS end user licence, so it is not attempted. +- **Overrides.** Eight area assignments come from official lookups rather than polygons: five English border LSOAs, Cardiff Bay and two Scottish output areas. Each carries a dated evidence comment in `build.py`. The build asserts the exact set of areas needing an override, so a boundary change fails loudly. +- **Output.** Cells are rounded to whole households, and empty cells are dropped. There is no national balancing adjustment. + +## Inputs + +`sources.yaml` lists the 87 inputs with their URLs, landing pages, licences and cache file names. +- The 72 ONS custom-API batches are pinned on the sha256 of the decoded JSON (`content_sha256`), because their gzip bytes vary between downloads. +- Every other input is pinned on the sha256 of its original bytes. + +## Rebuild + +Run from the repository root. Scotland's ward table is the one manual download: follow its `manual: true` instructions in `sources.yaml` and place the file in the cache. The scripts never log in or accept terms. Both scripts check every input's hash, and `build.py` reads only cached originals and checks the output's digest. + +```sh +nice -n 10 uvx --with pandas --with geopandas --with shapely --with pyogrio --with openpyxl --with pyyaml python tools/brma_households/fetch.py .brma-cache +nice -n 10 uvx --with pandas --with geopandas --with shapely --with pyogrio --with openpyxl --with pyyaml python tools/brma_households/build.py .brma-cache +``` + +- `fetch.py --only SOURCE_ID ...` fetches or checks only the listed sources. +- `build.py --output PATH` writes the table elsewhere. + +## Licences and limits + +- **Licences.** Census and geography inputs are under the Open Government Licence. NIHE's archived BRMA page has no stated reuse licence; the build uses only its postcode-district lists. +- **Locations are approximate.** Centroids and ward shares approximate where households live. +- **Rent-free households.** England and Wales bedroom mixes include them. +- **Disclosure control.** Census cells are perturbed. +- **Geography vintages differ** between nations. +- **Bedrooms describe the home,** not the household's LHA entitlement. +- **Northern Ireland has one private-rented share for every BRMA.** + +## Verification + +On 2 October 2026: +- All inputs passed `fetch.py` against a cache filled from retained originals. +- Fresh downloads of eight originals, covering all seven publishers and including an English and a Welsh ONS batch, matched their pins. +- The rebuild has 936 rows, sha256 `fd40dae019e5eefb8c976873f69c66f9b74a0747505326e8098b0ad99cc2ae1f`. diff --git a/tools/brma_households/build.py b/tools/brma_households/build.py new file mode 100644 index 000000000..115c8b317 --- /dev/null +++ b/tools/brma_households/build.py @@ -0,0 +1,477 @@ +#!/usr/bin/env python3 +"""Rebuild the BRMA private-rented household table from verified cached originals.""" + +import argparse +import csv +import hashlib +import html +import io +import json +from pathlib import Path +import re +import xml.etree.ElementTree as ET +import zipfile + +import geopandas as gpd +import pandas as pd +from shapely import make_valid +from shapely.geometry import MultiPolygon, Polygon + +from fetch import decoded_content, sources, verify_cache + + +# Overrides established on 2026-10-02; live reply pages are evidence, not inputs. +LSOA_OVERRIDES = { + "E01014018": "BRECON_AND_RADNOR", # Official VOA postcode lookup HR3 5TA, 2026-10-02. + "E01022272": "MONMOUTHSHIRE", # Official VOA postcode lookup NP16 7AG, 2026-10-02. + "E01022273": "MONMOUTHSHIRE", # Official VOA postcode lookup NP16 7RB, 2026-10-02. + "E01022274": "MONMOUTHSHIRE", # Official VOA postcode lookup NP16 7QB, 2026-10-02. + "E01022275": "MONMOUTHSHIRE", # Official VOA postcode lookup NP16 7EN, 2026-10-02. + "W01002024": "CARDIFF", # Official VOA lookup CF11 0AS (and 12 other postcodes), 2026-10-02. +} +SCOTTISH_OVERRIDES = { + "S00179382": "WEST_DUNBARTONSHIRE", # G83 8NQ; official VOA West Dunbartonshire council lookup, 2026-10-02. + "S00179383": "WEST_DUNBARTONSHIRE", # G83 8SD; official VOA West Dunbartonshire council lookup, 2026-10-02. +} + + +def read_json(path): + return json.loads(decoded_content(path)) + + +def brma_key(name): + return re.sub(r"\W+", "_", name.replace("&", "").upper()).strip("_") + + +def voa_polygons(path): + """Parse actual GML types: its XSD wrongly declares nine MultiPolygons.""" + ns = {"gml": "http://www.opengis.net/gml", "ogr": "http://ogr.maptools.org/"} + + def ring(node): + return [tuple(map(float, xy.split(","))) for xy in node.text.split()] + + def polygon(node): + shell = ring( + node.find("gml:outerBoundaryIs/gml:LinearRing/gml:coordinates", ns) + ) + holes = [ + ring(n) + for n in node.findall( + "gml:innerBoundaryIs/gml:LinearRing/gml:coordinates", ns + ) + ] + return Polygon(shell, holes) + + def parts(geom): + if geom.geom_type == "Polygon": + return [geom] + if geom.geom_type in {"MultiPolygon", "GeometryCollection"}: + return [p for child in geom.geoms for p in parts(child)] + return [] + + with zipfile.ZipFile(path) as z: + root = ET.fromstring(z.read("English BRMA(LHA) Layer.gml")) + records = [] + for feature in root.findall("gml:featureMember/ogr:English_BRMA_LHA__Layer", ns): + node = feature.find("ogr:geometryProperty", ns)[0] + assert {n.attrib["srsName"] for n in node.iter() if "srsName" in n.attrib} == { + "EPSG:27700" + } + if node.tag.endswith("}Polygon"): + geom = polygon(node) + else: + assert node.tag.endswith("}MultiPolygon") + geom = MultiPolygon( + [polygon(n) for n in node.findall("gml:polygonMember/gml:Polygon", ns)] + ) + repaired = parts(make_valid(geom)) + assert repaired + geom = repaired[0] if len(repaired) == 1 else MultiPolygon(repaired) + assert geom.is_valid and not geom.is_empty + records.append( + { + "brma": brma_key(feature.findtext("ogr:Name", namespaces=ns)), + "geometry": geom, + } + ) + assert len(records) == len({r["brma"] for r in records}) == 152 + return gpd.GeoDataFrame(records, crs=27700) + + +def lsoa_geography(cache): + regions = dict( + zip( + [f"E1200000{i}" for i in range(1, 10)], + [ + "NORTH_EAST", + "NORTH_WEST", + "YORKSHIRE", + "EAST_MIDLANDS", + "WEST_MIDLANDS", + "EAST_OF_ENGLAND", + "LONDON", + "SOUTH_EAST", + "SOUTH_WEST", + ], + ) + ) + hierarchy = pd.read_csv( + cache / "ons_geo_oa21_lsoa21.csv", usecols=["OA21CD", "LSOA21CD"], dtype=str + ) + hierarchy = hierarchy[hierarchy.LSOA21CD.str.startswith("E")] + region = pd.read_csv( + cache / "ons_geo_oa21_region.csv", usecols=["oa21cd", "rgn22cd"], dtype=str + ) + hierarchy = hierarchy.merge( + region, left_on="OA21CD", right_on="oa21cd", how="left", validate="one_to_one" + ) + assert ( + hierarchy.rgn22cd.notna().all() + and hierarchy.groupby("LSOA21CD").rgn22cd.nunique().eq(1).all() + ) + points = gpd.read_file(f"zip://{cache / 'ons_geo_lsoa21_pwc.zip'}") + points = points[points.LSOA21CD.str.startswith("E")].to_crs(27700) + assert points.LSOA21CD.is_unique and len(points) == 33755 + eng = gpd.sjoin( + points, + voa_polygons(cache / "brma_england_voa_may2020.zip"), + how="left", + predicate="intersects", + ) + assert eng.LSOA21CD.is_unique + outside = eng[eng.brma.isna()].copy() + assert set(outside.LSOA21CD) == {c for c in LSOA_OVERRIDES if c.startswith("E")}, ( + "English override set changed" + ) + outside["brma"] = outside.LSOA21CD.map(LSOA_OVERRIDES) + wal_points = gpd.read_file(cache / "geo_lsoa21_wales_pwc.geojson").to_crs(27700) + assert wal_points.LSOA21CD.is_unique and len(wal_points) == 1917 + wal_polygons = gpd.read_file( + f"zip://{cache / 'geo_brma_wales_2014.zip'}!Broad Rental Market Areas/wales_brma.shp" + ).to_crs(27700) + wal_polygons.geometry = wal_polygons.geometry.make_valid() + wal = gpd.sjoin( + wal_points, + wal_polygons[["brma_name", "geometry"]], + how="left", + predicate="intersects", + ) + assert wal.LSOA21CD.is_unique + assert set(wal.loc[wal.brma_name.isna(), "LSOA21CD"]) == { + c for c in LSOA_OVERRIDES if c.startswith("W") + }, "Welsh override set changed" + wal["brma"] = wal.brma_name.map(lambda n: brma_key(n) if pd.notna(n) else None) + wal["brma"] = wal.brma.fillna(wal.LSOA21CD.map(LSOA_OVERRIDES)) + xw = pd.concat( + [ + eng[eng.brma.notna()].sort_values("LSOA21CD"), + wal.sort_values("LSOA21CD"), + outside.sort_values("LSOA21CD"), + ], + ignore_index=True, + )[["LSOA21CD", "brma"]] + regional = ( + hierarchy[["LSOA21CD", "rgn22cd"]] + .drop_duplicates() + .set_index("LSOA21CD") + .rgn22cd.map(regions) + ) + xw["region"] = xw.LSOA21CD.map(regional) + xw.loc[xw.LSOA21CD.str.startswith("W"), "region"] = "WALES" + assert xw.region.notna().all() and xw.LSOA21CD.is_unique + return xw.rename(columns={"LSOA21CD": "area_code"}) + + +def england_and_wales(cache, entries): + with zipfile.ZipFile(cache / "census_nomis_ts054.zip") as z: + ts = pd.read_csv(io.BytesIO(z.read("census2021-ts054-lsoa.csv"))) + col = "Tenure of household: Private rented" + assert ( + ts[col] + .eq( + ts[col + ": Private landlord or letting agency"] + + ts[col + ": Other private rented"] + ) + .all() + ) + prs = ts.set_index("geography code")[col].astype(float) + rows = [] + paths = sorted( + cache / e["filename"] + for e in entries + if e["filename"].startswith( + ("census_lsoa_tenure5a_bedrooms_", "census_wales_lsoa_tenure5a_bedrooms_") + ) + ) + for path in paths: + data = read_json(path) + assert not data.get("blocked_areas"), path + for observation in data["observations"]: + area, tenure, beds = (d["option_id"] for d in observation["dimensions"]) + if tenure == "3" and beds in {"1", "2", "3", "4"}: + rows.append( + (area, "4+" if beds == "4" else beds, observation["observation"]) + ) + mix = pd.DataFrame(rows, columns=["lsoa", "bedrooms", "n"]).pivot_table( + index="lsoa", columns="bedrooms", values="n", aggfunc="sum" + ) + mix = mix.div(mix.sum(axis=1), axis=0) + xw = lsoa_geography(cache) + assert set(xw.area_code) == set(prs.index) == set(mix.index) + assert mix.notna().all().all() or prs[mix.isna().any(axis=1)].eq(0).all() + cells = mix.fillna(0).mul(prs, axis=0).stack().rename("households").reset_index() + cells.columns = ["area_code", "bedrooms", "households"] + out = xw.merge(cells, on="area_code") + return out.groupby(["region", "brma", "bedrooms"], as_index=False).households.sum() + + +def scotland(cache): + """Allocate ward tenure × bedrooms using OA household-weighted BRMA shares.""" + + def name(value): + value = re.sub(r"\s*/\s*", " AND ", value.strip()).upper().replace(" ", "_") + return "AYRSHIRES" if value == "AYRSHIRE" else value + + def postcode(value): + key = re.sub(r"\s+", "", str(value).upper()) + match = re.fullmatch(r"([A-Z]{1,2}[0-9][0-9A-Z]?[0-9][A-Z]{2})[A-Z]?", key) + if not match: + raise ValueError(f"Invalid Scottish postcode: {value!r}") + return match.group(1) + + brmas = gpd.read_file(f"zip://{cache / 'scot_brma_boundaries_2009.zip'}") + assert brmas.geometry.is_valid.all() and len(brmas) == 18 + brmas["brma"] = brmas.BRMAName.map(name) + oas = gpd.read_file( + f"zip://{cache / 'scot_oa2022_population_weighted_centroids.zip'}!OutputArea2022_PWC/OutputArea2022_PWC.shp" + ) + oas = oas.rename(columns={"code": "area_code", "HHcount": "households"}).to_crs( + brmas.crs + ) + assert len(oas) == 46363 and not oas.area_code.duplicated().any() + matches = gpd.sjoin( + oas, brmas[["brma", "geometry"]], how="left", predicate="within" + ) + missing = matches.loc[matches.brma.isna(), "area_code"] + boundary = gpd.sjoin( + oas[oas.area_code.isin(missing)], + brmas[["brma", "geometry"]], + how="left", + predicate="intersects", + ) + matches = pd.concat( + [matches[~matches.area_code.isin(missing)], boundary], ignore_index=True + ) + single = matches[ + matches.area_code.map(matches.groupby("area_code").brma.count()).eq(1) + ] + oas = oas.drop(columns="geometry").merge( + single[["area_code", "brma"]], on="area_code", how="left", validate="one_to_one" + ) + original_brma = oas.brma.copy() + official = pd.read_excel( + cache / "scot_foi_postcodes_brma_2023.xlsx", usecols=[0, 1], dtype=str + ) + official.columns = ["brma", "postcode"] + official["postcode"] = official.postcode.map(postcode) + official["brma"] = official.brma.map(name) + candidates = official.groupby("postcode").brma.agg(lambda x: set(x)) + oas["candidates"] = oas.masterpc.map(postcode).map(candidates) + unique = oas.candidates.map( + lambda x: next(iter(x)) if isinstance(x, set) and len(x) == 1 else None + ) + oas["brma"] = oas.brma.fillna(unique) + assert oas.brma.notna().all(), "OA outside polygons without unique postcode BRMA" + with zipfile.ZipFile(cache / "scot_census2022_geography_index.zip") as z: + higher = pd.read_csv( + z.open("Census_2022_Index/OA_TO_HIGHER_AREAS.csv"), + encoding="utf-8-sig", + usecols=["OA2022", "EW2022", "CA2019"], + ) + oas = oas.merge( + higher, left_on="area_code", right_on="OA2022", validate="one_to_one" + ) + conflict = pd.Series( + [ + isinstance(cs, set) and pd.notna(b) and b not in cs + for b, cs in zip(original_brma, oas.candidates) + ], + index=oas.index, + ) + oas.loc[conflict & unique.notna(), "brma"] = unique + unresolved = conflict & unique.isna() + assert set(oas.loc[unresolved, "area_code"]) == set(SCOTTISH_OVERRIDES), ( + "Scottish override set changed" + ) + assert oas.loc[unresolved, "CA2019"].eq("S12000039").all() + # Clear unresolved polygon assignments before applying the documented exceptions. + oas.loc[unresolved, "brma"] = None + assert set(oas.loc[oas.brma.isna(), "area_code"]) == set(SCOTTISH_OVERRIDES) + oas["brma"] = oas.brma.fillna(oas.area_code.map(SCOTTISH_OVERRIDES)) + assert all( + not isinstance(cs, set) or b in cs for b, cs in zip(oas.brma, oas.candidates) + ) + shares = oas.groupby(["EW2022", "brma"], as_index=False).households.sum() + shares["share"] = shares.households / shares.EW2022.map( + oas.groupby("EW2022").households.sum() + ) + shares = shares.rename(columns={"EW2022": "area_code"}) + assert shares.area_code.nunique() == 355 + assert shares.groupby("area_code").share.sum().sub(1).abs().max() < 1e-12 + + with (cache / "census_2022_ward2022_tenure_bedrooms.csv").open( + encoding="utf-8-sig", newline="" + ) as f: + rows = list(csv.reader(f)) + assert ["Counting: Households"] in rows and not any( + r and r[0].strip() == "ERROR" for r in rows + ) + tenures = { + "Private rented: Private landlord or letting agency": "private_rented_landlord_or_agent", + "Private rented: Other": "private_rented_other", + } + bands = { + "One bedroom": "1", + "Two bedrooms": "2", + "Three bedrooms": "3", + "Four bedrooms": "4+", + "Five or more bedrooms": "4+", + } + records, tenure, headers = [], None, None + for row in rows: + if not row: + continue + label = row[0].strip() + if len(row) == 1: + tenure = tenures.get(label) + elif label == "Number of bedrooms": + headers = row[1:] + assert {h for h in headers if h} == set(bands) | {"Total"}, headers + elif tenure and re.fullmatch(r"S13\d{6}", label): + for bedroom, value in zip(headers, row[1:], strict=True): + if bedroom in bands: + records.append((label, bands[bedroom], tenure, int(value))) + census = pd.DataFrame( + records, columns=["area_code", "bedrooms", "tenure", "households"] + ) + census = census.groupby( + ["area_code", "bedrooms", "tenure"], as_index=False + ).households.sum() + assert len(census) == 355 * 4 * 2 and set(census.area_code) == set(shares.area_code) + joined = census.merge(shares[["area_code", "brma", "share"]], on="area_code") + joined["households"] *= joined.share + output = joined.groupby( + ["brma", "bedrooms", "tenure"], as_index=False + ).households.sum() + output = output.groupby(["brma", "bedrooms"], as_index=False).households.sum() + output["region"] = "SCOTLAND" + return output[["region", "brma", "bedrooms", "households"]] + + +def northern_ireland(cache): + """Private-rented households by NIHE BRMA, from Open Government Licence data. + + NIHE defines each BRMA as a set of postcode districts, and NISRA publishes + Census 2021 households for every postcode district (no suppression at that + level). Tenure is published only for census areas, and linking those to + postcode districts needs ONS Postcode Directory records whose Northern + Ireland licence (LPS end user licence) does not clearly allow publishing + derived figures. So each BRMA's private-rented households are its census + households times Northern Ireland's private-rented share (NISRA tenure by + Data Zone, summed). + """ + names = { + "Belfast": "BELFAST", + "Lough Neagh Lower": "LOUGH_NEAGH_LOWER", + "Lough Neagh Upper": "LOUGH_NEAGH_UPPER", + "North": "NORTH_NI", + "North West": "NORTH_WEST_NI", + "South East": "SOUTH_EAST_NI", + "South": "SOUTH_NI", + "South West": "SOUTH_WEST_NI", + } + text = html.unescape( + re.sub( + r"<[^>]+>", " ", (cache / "nihe_current_lha_archive_2021.html").read_text() + ) + ) + choices = "|".join(sorted(names, key=len, reverse=True)) + records = re.findall(rf"({choices}) BRMA:\s*((?:BT|[\d,\s\-])+)", text) + assert len(records) == 8 + memberships = {} + for name, districts in records: + for first, last in re.findall(r"BT(\d+)(?:-BT(\d+))?", districts): + for number in range(int(first), int(last or first) + 1): + district = f"BT{number}" + assert district not in memberships + memberships[district] = names[name] + assert len(memberships) == 80 + districts = pd.read_excel( + cache / "nisra_census2021_postcode_households.xlsx", + sheet_name="Postcode district", + header=5, + ) + districts["district"] = districts["Postcode district"].astype(str).str.strip() + districts = districts[districts.district.str.fullmatch(r"BT\d+")] + assert set(districts.district) == set(memberships) + households = districts.groupby(districts.district.map(memberships)).Households.sum() + tenure = pd.read_csv(cache / "tenure_7_data_zones.csv") + tenure.columns = [ + "area_code", + "area_name", + "tenure_code", + "tenure_label", + "households", + ] + assert len(tenure) == 3780 * 7 and tenure.area_code.nunique() == 3780 + private_share = ( + tenure.loc[tenure.tenure_code.isin([5, 6]), "households"].sum() + / tenure.households.sum() + ) + out = ( + (households * private_share) + .rename("households") + .rename_axis("brma") + .reset_index() + ) + out["region"], out["bedrooms"] = "NORTHERN_IRELAND", "all" + return out[["region", "brma", "bedrooms", "households"]] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("cache", type=Path) + parser.add_argument( + "--output", + type=Path, + default=Path("policyengine_uk_data/storage/brma_private_rented_households.csv"), + ) + args = parser.parse_args() + entries = sources() + verify_cache(args.cache, entries) + table = pd.concat( + [ + england_and_wales(args.cache, entries), + scotland(args.cache), + northern_ireland(args.cache), + ], + ignore_index=True, + ) + table = table[table.households > 0].copy() + table["households"] = table.households.round().astype(int) + table = table[table.households > 0].sort_values(["region", "brma", "bedrooms"]) + data = ( + table[["region", "brma", "bedrooms", "households"]] + .to_csv(index=False, lineterminator="\n") + .encode("utf-8") + ) + digest = hashlib.sha256(data).hexdigest() + expected = "fd40dae019e5eefb8c976873f69c66f9b74a0747505326e8098b0ad99cc2ae1f" + if digest != expected: + raise ValueError(f"Rebuilt table differs: expected {expected}, got {digest}") + args.output.write_bytes(data) + print(f"Wrote {len(table)} rows to {args.output}; SHA256 {digest}") + + +if __name__ == "__main__": + main() diff --git a/tools/brma_households/fetch.py b/tools/brma_households/fetch.py new file mode 100644 index 000000000..be7fc7bfa --- /dev/null +++ b/tools/brma_households/fetch.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Fetch pinned originals without a login, browser session or accepting terms.""" + +import argparse +import gzip +import hashlib +from pathlib import Path +import shutil +from urllib.request import Request, urlopen + +import yaml + + +def sources(): + entries = yaml.safe_load(Path(__file__).with_name("sources.yaml").read_text()) + required = { + "id", + "publisher", + "title", + "url", + "landing_page", + "licence", + "notes", + "filename", + } + ids, filenames = set(), set() + for entry in entries: + if not required <= entry.keys() or not ( + {"sha256", "content_sha256"} & entry.keys() + ): + raise ValueError(f"Incomplete source: {entry.get('id')}") + name = entry["filename"] + if entry["id"] in ids or name in filenames or Path(name).name != name: + raise ValueError(f"Duplicate source or unsafe cache filename: {name}") + ids.add(entry["id"]) + filenames.add(name) + return entries + + +def decoded_content(path): + data = path.read_bytes() + return gzip.decompress(data) if data.startswith(b"\x1f\x8b") else data + + +def verify(path, entry): + if not path.is_file(): + instructions = f"\n{entry['notes']}" if entry.get("manual") else "" + raise FileNotFoundError(f"Missing source {entry['id']}: {path}{instructions}") + if "content_sha256" in entry: + actual = hashlib.sha256(decoded_content(path)).hexdigest() + expected = entry["content_sha256"] + else: + with path.open("rb") as stream: + actual = hashlib.file_digest(stream, "sha256").hexdigest() + expected = entry["sha256"] + if actual != expected: + raise ValueError( + f"SHA256 mismatch for {entry['id']}: expected {expected}, got {actual}" + ) + + +def verify_cache(cache, entries): + """Check every original before the builder interprets any source.""" + for entry in entries: + verify(cache / entry["filename"], entry) + + +def fetch(cache, entries): + cache.mkdir(parents=True, exist_ok=True) + missing = [] + for entry in entries: + path = cache / entry["filename"] + if not path.exists() and entry.get("manual"): + print(f"Manual download required: {path}\n{entry['notes']}", flush=True) + missing.append(entry["id"]) + continue + if not path.exists(): + print(f"Downloading {entry['id']}", flush=True) + request = Request( + entry["url"], + headers={ + "User-Agent": "BRMA-household-pipeline/1.0", + "Accept-Encoding": "gzip" if path.suffix == ".gz" else "identity", + }, + ) + temporary = path.with_name(path.name + ".part") + try: + # Keep entity bytes; custom-API pins verify decoded content instead. + with ( + urlopen(request, timeout=180) as response, + temporary.open("wb") as stream, + ): + shutil.copyfileobj(response, stream) + verify(temporary, entry) + temporary.replace(path) + finally: + temporary.unlink(missing_ok=True) + verify(path, entry) + print(f"Verified {entry['id']}", flush=True) + if missing: + raise FileNotFoundError( + "Supply the manual downloads above: " + ", ".join(missing) + ) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("cache", type=Path) + parser.add_argument( + "--only", + nargs="+", + metavar="SOURCE_ID", + help="Fetch/verify selected sources (default: all)", + ) + args = parser.parse_args() + entries = sources() + if args.only: + unknown = set(args.only) - {entry["id"] for entry in entries} + if unknown: + parser.error("Unknown source IDs: " + ", ".join(sorted(unknown))) + entries = [entry for entry in entries if entry["id"] in args.only] + fetch(args.cache, entries) + + +if __name__ == "__main__": + main() diff --git a/tools/brma_households/sources.yaml b/tools/brma_households/sources.yaml new file mode 100644 index 000000000..262d8a919 --- /dev/null +++ b/tools/brma_households/sources.yaml @@ -0,0 +1,1037 @@ +- id: census_lsoa_tenure5a_bedrooms_001 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 001 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01000001,E01000002,E01000003,E01000005,E01000006,E01000007,E01000008,E01000009,E01000011,E01000012,E01000013,E01000014,E01000015,E01000016,E01000017,E01000018,E01000019,E01000020,E01000021,E01000022,E01000024,E01000025,E01000027,E01000028,E01000029,E01000030,E01000031,E01000032,E01000033,E01000034,E01000035,E01000036,E01000037,E01000038,E01000039,E01000040,E01000041,E01000042,E01000043,E01000044,E01000045,E01000046,E01000049,E01000050,E01000051,E01000052,E01000053,E01000054,E01000055,E01000056,E01000057,E01000058,E01000059,E01000060,E01000061,E01000062,E01000063,E01000064,E01000065,E01000066,E01000067,E01000068,E01000069,E01000070,E01000071,E01000072,E01000073,E01000074,E01000075,E01000076,E01000077,E01000078,E01000079,E01000080,E01000081,E01000082,E01000083,E01000084,E01000085,E01000086,E01000087,E01000088,E01000089,E01000090,E01000091,E01000093,E01000094,E01000095,E01000096,E01000097,E01000098,E01000099,E01000100,E01000101,E01000102,E01000103,E01000104,E01000105,E01000106,E01000107,E01000108,E01000110,E01000111,E01000112,E01000113,E01000114,E01000115,E01000116,E01000117,E01000118,E01000119,E01000120,E01000121,E01000122,E01000123,E01000124,E01000126,E01000127,E01000128,E01000129,E01000130,E01000131,E01000132,E01000133,E01000134,E01000135,E01000136,E01000137,E01000138,E01000139,E01000140,E01000141,E01000142,E01000143,E01000144,E01000145,E01000146,E01000147,E01000150,E01000151,E01000152,E01000153,E01000154,E01000156,E01000157,E01000158,E01000159,E01000160,E01000161,E01000162,E01000163,E01000164,E01000165,E01000166,E01000167,E01000168,E01000169,E01000170,E01000171,E01000172,E01000173,E01000174,E01000175,E01000176,E01000177,E01000178,E01000179,E01000180,E01000181,E01000182,E01000183,E01000184,E01000185,E01000186,E01000187,E01000188,E01000189,E01000190,E01000191,E01000192,E01000193,E01000194,E01000195,E01000196,E01000197,E01000198,E01000199,E01000200,E01000201,E01000202,E01000203,E01000204,E01000205,E01000206,E01000207,E01000208,E01000209,E01000210,E01000211,E01000212,E01000213,E01000214,E01000215,E01000216,E01000217,E01000218,E01000219,E01000220,E01000221,E01000222,E01000223,E01000224,E01000225,E01000226,E01000227,E01000228,E01000229,E01000230,E01000231,E01000232,E01000233,E01000234,E01000235,E01000236,E01000237,E01000238,E01000239,E01000240,E01000241,E01000242,E01000243,E01000244,E01000245,E01000246,E01000247,E01000248,E01000249,E01000250,E01000251,E01000252,E01000253,E01000254,E01000255,E01000256,E01000257,E01000258,E01000259,E01000260,E01000261,E01000263,E01000264,E01000265,E01000266,E01000267,E01000268,E01000269,E01000270,E01000271,E01000272,E01000273,E01000274,E01000275,E01000276,E01000277,E01000278,E01000279,E01000280,E01000281,E01000282,E01000283,E01000284,E01000285,E01000286,E01000287,E01000288,E01000289,E01000290,E01000291,E01000292,E01000293,E01000294,E01000295,E01000296,E01000297,E01000298,E01000299,E01000300,E01000301,E01000302,E01000303,E01000304,E01000305,E01000306,E01000307,E01000308,E01000309,E01000310,E01000311,E01000312,E01000313,E01000314,E01000315,E01000316,E01000317,E01000318,E01000319,E01000320,E01000321,E01000322,E01000323,E01000324,E01000325,E01000326,E01000327,E01000328,E01000329,E01000330,E01000331,E01000332,E01000333,E01000334,E01000335,E01000336,E01000337,E01000338,E01000339,E01000340,E01000341,E01000342,E01000343,E01000344,E01000345,E01000346,E01000347,E01000348,E01000349,E01000350,E01000351,E01000352,E01000353,E01000354,E01000355,E01000356,E01000357,E01000358,E01000359,E01000360,E01000361,E01000362,E01000363,E01000364,E01000365,E01000366,E01000367,E01000368,E01000369,E01000370,E01000371,E01000372,E01000373,E01000374,E01000375,E01000376,E01000377,E01000379,E01000380,E01000381,E01000382,E01000383,E01000384,E01000385,E01000386,E01000387,E01000388,E01000389,E01000390,E01000391,E01000392,E01000393,E01000394,E01000395,E01000396,E01000398,E01000399,E01000400,E01000401,E01000402,E01000403,E01000404,E01000405,E01000406,E01000407,E01000408,E01000409,E01000410,E01000411,E01000412,E01000413,E01000414,E01000415,E01000416,E01000417,E01000418,E01000419,E01000420,E01000421,E01000422,E01000423,E01000424,E01000425,E01000426,E01000427,E01000428,E01000429,E01000430,E01000431,E01000433,E01000434,E01000435,E01000436,E01000437,E01000438,E01000439,E01000440,E01000441,E01000442,E01000443,E01000444,E01000445,E01000446,E01000447,E01000448,E01000449,E01000450,E01000451,E01000452,E01000453,E01000454,E01000455,E01000456,E01000457,E01000458,E01000459,E01000460,E01000461,E01000462,E01000463,E01000464,E01000465,E01000466,E01000467,E01000468,E01000469,E01000470,E01000471,E01000472,E01000473,E01000474,E01000475,E01000476,E01000477,E01000478,E01000479,E01000480,E01000481,E01000483,E01000484,E01000485,E01000486,E01000487,E01000488,E01000489,E01000490,E01000491,E01000492,E01000493,E01000494,E01000495,E01000497,E01000498,E01000499,E01000501,E01000502,E01000503,E01000504,E01000505,E01000506,E01000507,E01000508,E01000509,E01000510,E01000511,E01000512,E01000513,E01000514,E01000515,E01000516,E01000517,E01000518,E01000519&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_001.json.gz + content_sha256: 2c0395055bd054fda769f99aaa1acb0f23317afa5408fc9a98112a9c6bb2f255 +- id: census_lsoa_tenure5a_bedrooms_002 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 002 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01000520,E01000521,E01000522,E01000523,E01000524,E01000525,E01000526,E01000527,E01000528,E01000530,E01000531,E01000532,E01000533,E01000534,E01000535,E01000536,E01000538,E01000539,E01000540,E01000541,E01000543,E01000544,E01000545,E01000546,E01000547,E01000548,E01000549,E01000550,E01000551,E01000552,E01000553,E01000554,E01000555,E01000556,E01000557,E01000558,E01000559,E01000560,E01000561,E01000562,E01000563,E01000564,E01000565,E01000566,E01000567,E01000568,E01000569,E01000570,E01000571,E01000572,E01000573,E01000574,E01000575,E01000576,E01000577,E01000578,E01000579,E01000580,E01000581,E01000582,E01000583,E01000584,E01000585,E01000586,E01000587,E01000589,E01000590,E01000591,E01000592,E01000593,E01000594,E01000595,E01000596,E01000599,E01000601,E01000602,E01000603,E01000604,E01000605,E01000606,E01000607,E01000608,E01000609,E01000610,E01000611,E01000612,E01000613,E01000615,E01000616,E01000617,E01000618,E01000619,E01000620,E01000621,E01000622,E01000623,E01000624,E01000625,E01000626,E01000627,E01000628,E01000629,E01000630,E01000631,E01000632,E01000633,E01000634,E01000636,E01000637,E01000638,E01000639,E01000640,E01000641,E01000642,E01000643,E01000644,E01000645,E01000646,E01000647,E01000648,E01000649,E01000650,E01000651,E01000652,E01000653,E01000654,E01000655,E01000656,E01000657,E01000658,E01000659,E01000661,E01000662,E01000663,E01000664,E01000665,E01000666,E01000667,E01000668,E01000669,E01000670,E01000671,E01000672,E01000673,E01000674,E01000676,E01000677,E01000678,E01000679,E01000680,E01000681,E01000682,E01000683,E01000684,E01000685,E01000686,E01000687,E01000688,E01000689,E01000690,E01000691,E01000692,E01000693,E01000694,E01000695,E01000696,E01000697,E01000698,E01000699,E01000700,E01000701,E01000702,E01000703,E01000704,E01000705,E01000706,E01000707,E01000708,E01000709,E01000710,E01000711,E01000712,E01000713,E01000714,E01000715,E01000716,E01000717,E01000718,E01000719,E01000720,E01000721,E01000722,E01000723,E01000724,E01000725,E01000726,E01000727,E01000728,E01000729,E01000730,E01000731,E01000732,E01000733,E01000734,E01000735,E01000736,E01000737,E01000738,E01000739,E01000741,E01000742,E01000743,E01000744,E01000745,E01000746,E01000747,E01000748,E01000749,E01000750,E01000751,E01000752,E01000753,E01000754,E01000755,E01000756,E01000757,E01000758,E01000759,E01000760,E01000761,E01000762,E01000763,E01000764,E01000765,E01000766,E01000767,E01000768,E01000769,E01000770,E01000771,E01000772,E01000773,E01000774,E01000775,E01000777,E01000778,E01000779,E01000780,E01000781,E01000782,E01000783,E01000784,E01000785,E01000786,E01000787,E01000788,E01000789,E01000790,E01000791,E01000792,E01000793,E01000794,E01000795,E01000796,E01000797,E01000798,E01000799,E01000800,E01000801,E01000802,E01000803,E01000804,E01000805,E01000806,E01000807,E01000808,E01000809,E01000810,E01000811,E01000812,E01000813,E01000814,E01000815,E01000816,E01000818,E01000819,E01000820,E01000822,E01000823,E01000824,E01000825,E01000826,E01000827,E01000828,E01000829,E01000830,E01000831,E01000832,E01000833,E01000834,E01000835,E01000836,E01000837,E01000838,E01000839,E01000840,E01000841,E01000842,E01000843,E01000844,E01000845,E01000846,E01000847,E01000848,E01000849,E01000850,E01000851,E01000853,E01000855,E01000856,E01000857,E01000858,E01000859,E01000860,E01000861,E01000862,E01000863,E01000866,E01000867,E01000868,E01000869,E01000870,E01000871,E01000872,E01000873,E01000874,E01000875,E01000876,E01000877,E01000878,E01000879,E01000880,E01000881,E01000882,E01000883,E01000884,E01000885,E01000886,E01000887,E01000888,E01000889,E01000890,E01000891,E01000892,E01000893,E01000894,E01000895,E01000896,E01000897,E01000898,E01000899,E01000900,E01000901,E01000902,E01000903,E01000904,E01000905,E01000906,E01000907,E01000908,E01000909,E01000910,E01000911,E01000912,E01000913,E01000914,E01000915,E01000916,E01000917,E01000918,E01000919,E01000920,E01000921,E01000922,E01000923,E01000924,E01000925,E01000926,E01000927,E01000928,E01000929,E01000930,E01000931,E01000932,E01000933,E01000934,E01000935,E01000937,E01000938,E01000939,E01000941,E01000942,E01000943,E01000944,E01000946,E01000947,E01000948,E01000949,E01000951,E01000952,E01000954,E01000955,E01000956,E01000957,E01000958,E01000959,E01000960,E01000961,E01000962,E01000963,E01000964,E01000965,E01000966,E01000967,E01000968,E01000969,E01000970,E01000971,E01000972,E01000973,E01000974,E01000975,E01000976,E01000977,E01000978,E01000979,E01000980,E01000981,E01000982,E01000983,E01000984,E01000985,E01000986,E01000987,E01000989,E01000990,E01000991,E01000992,E01000993,E01000994,E01000995,E01000996,E01000997,E01000998,E01000999,E01001000,E01001001,E01001002,E01001003,E01001004,E01001005,E01001006,E01001007,E01001008,E01001009,E01001010,E01001011,E01001012,E01001013,E01001015,E01001016,E01001017,E01001018,E01001019,E01001020,E01001021,E01001022,E01001023,E01001024,E01001025,E01001026,E01001027,E01001028,E01001029,E01001030,E01001031,E01001032,E01001033,E01001034,E01001035,E01001036,E01001037,E01001038,E01001039,E01001040,E01001041,E01001042,E01001044,E01001046,E01001047&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_002.json.gz + content_sha256: beb8c647f39d8e239002c273d03b1dcfb33cffe927892747c7f2cb4a8171d90a +- id: census_lsoa_tenure5a_bedrooms_003 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 003 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01001049,E01001050,E01001051,E01001052,E01001053,E01001054,E01001055,E01001056,E01001057,E01001058,E01001059,E01001060,E01001061,E01001062,E01001063,E01001064,E01001065,E01001066,E01001067,E01001068,E01001069,E01001070,E01001071,E01001072,E01001073,E01001074,E01001075,E01001076,E01001077,E01001078,E01001079,E01001080,E01001081,E01001082,E01001083,E01001084,E01001085,E01001086,E01001087,E01001088,E01001089,E01001090,E01001091,E01001092,E01001093,E01001094,E01001095,E01001096,E01001097,E01001098,E01001099,E01001100,E01001101,E01001102,E01001103,E01001104,E01001105,E01001106,E01001107,E01001108,E01001109,E01001110,E01001111,E01001112,E01001113,E01001114,E01001115,E01001116,E01001118,E01001119,E01001120,E01001121,E01001122,E01001123,E01001124,E01001125,E01001126,E01001127,E01001128,E01001129,E01001130,E01001131,E01001132,E01001133,E01001134,E01001135,E01001136,E01001137,E01001138,E01001139,E01001140,E01001141,E01001142,E01001143,E01001144,E01001145,E01001146,E01001147,E01001148,E01001149,E01001150,E01001151,E01001152,E01001153,E01001154,E01001155,E01001156,E01001157,E01001158,E01001159,E01001160,E01001161,E01001162,E01001163,E01001164,E01001165,E01001166,E01001167,E01001168,E01001169,E01001170,E01001171,E01001172,E01001173,E01001174,E01001175,E01001176,E01001177,E01001178,E01001179,E01001180,E01001181,E01001182,E01001183,E01001184,E01001185,E01001186,E01001187,E01001188,E01001189,E01001190,E01001191,E01001192,E01001193,E01001194,E01001195,E01001196,E01001197,E01001198,E01001199,E01001200,E01001201,E01001202,E01001203,E01001204,E01001205,E01001206,E01001207,E01001208,E01001209,E01001210,E01001211,E01001212,E01001213,E01001214,E01001215,E01001216,E01001217,E01001218,E01001219,E01001220,E01001223,E01001224,E01001225,E01001226,E01001227,E01001228,E01001229,E01001230,E01001231,E01001232,E01001233,E01001234,E01001235,E01001236,E01001237,E01001239,E01001240,E01001241,E01001242,E01001243,E01001244,E01001245,E01001246,E01001247,E01001248,E01001249,E01001250,E01001251,E01001252,E01001253,E01001254,E01001255,E01001257,E01001258,E01001259,E01001260,E01001261,E01001262,E01001263,E01001264,E01001265,E01001266,E01001267,E01001268,E01001269,E01001270,E01001271,E01001272,E01001273,E01001274,E01001275,E01001276,E01001277,E01001278,E01001279,E01001280,E01001281,E01001282,E01001283,E01001284,E01001285,E01001286,E01001287,E01001288,E01001289,E01001290,E01001291,E01001292,E01001293,E01001294,E01001295,E01001296,E01001297,E01001298,E01001299,E01001300,E01001301,E01001302,E01001303,E01001304,E01001305,E01001306,E01001307,E01001308,E01001309,E01001310,E01001311,E01001312,E01001313,E01001314,E01001315,E01001316,E01001317,E01001318,E01001319,E01001320,E01001321,E01001322,E01001323,E01001324,E01001325,E01001326,E01001327,E01001328,E01001329,E01001330,E01001331,E01001332,E01001333,E01001334,E01001335,E01001336,E01001337,E01001338,E01001339,E01001340,E01001341,E01001342,E01001343,E01001344,E01001345,E01001346,E01001347,E01001348,E01001349,E01001350,E01001351,E01001352,E01001353,E01001354,E01001355,E01001356,E01001357,E01001358,E01001359,E01001360,E01001361,E01001362,E01001363,E01001364,E01001365,E01001366,E01001367,E01001368,E01001369,E01001370,E01001371,E01001372,E01001373,E01001374,E01001375,E01001376,E01001377,E01001379,E01001380,E01001381,E01001382,E01001383,E01001384,E01001385,E01001386,E01001387,E01001388,E01001389,E01001390,E01001391,E01001392,E01001393,E01001394,E01001395,E01001396,E01001397,E01001398,E01001399,E01001400,E01001401,E01001402,E01001403,E01001404,E01001405,E01001406,E01001407,E01001408,E01001409,E01001410,E01001411,E01001412,E01001413,E01001414,E01001415,E01001416,E01001417,E01001418,E01001419,E01001420,E01001421,E01001422,E01001423,E01001424,E01001425,E01001426,E01001427,E01001428,E01001429,E01001430,E01001431,E01001432,E01001433,E01001434,E01001435,E01001436,E01001437,E01001438,E01001439,E01001440,E01001441,E01001442,E01001444,E01001445,E01001446,E01001447,E01001448,E01001449,E01001450,E01001451,E01001452,E01001453,E01001454,E01001455,E01001456,E01001457,E01001458,E01001459,E01001460,E01001461,E01001462,E01001463,E01001464,E01001465,E01001466,E01001467,E01001468,E01001469,E01001470,E01001471,E01001472,E01001473,E01001474,E01001475,E01001476,E01001477,E01001478,E01001479,E01001480,E01001481,E01001482,E01001483,E01001484,E01001485,E01001486,E01001487,E01001488,E01001489,E01001491,E01001492,E01001493,E01001494,E01001495,E01001496,E01001497,E01001498,E01001499,E01001500,E01001501,E01001502,E01001503,E01001504,E01001505,E01001507,E01001508,E01001509,E01001511,E01001512,E01001513,E01001514,E01001515,E01001516,E01001517,E01001518,E01001520,E01001521,E01001522,E01001523,E01001524,E01001525,E01001526,E01001527,E01001528,E01001529,E01001530,E01001531,E01001532,E01001533,E01001534,E01001535,E01001536,E01001537,E01001538,E01001539,E01001540,E01001541,E01001542,E01001543,E01001544,E01001545,E01001546,E01001547,E01001548,E01001549,E01001550,E01001551,E01001552,E01001553,E01001554,E01001555,E01001556,E01001557,E01001558,E01001559&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_003.json.gz + content_sha256: b79c9c84f0e058c95fd6ef1639579eba119195328859ed83d699fc292c2d78ad +- id: census_lsoa_tenure5a_bedrooms_004 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 004 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01001560,E01001562,E01001563,E01001564,E01001565,E01001566,E01001567,E01001568,E01001569,E01001570,E01001571,E01001572,E01001573,E01001574,E01001575,E01001576,E01001577,E01001578,E01001579,E01001580,E01001581,E01001582,E01001583,E01001584,E01001585,E01001586,E01001587,E01001588,E01001589,E01001590,E01001591,E01001592,E01001593,E01001594,E01001595,E01001596,E01001597,E01001598,E01001599,E01001600,E01001601,E01001602,E01001603,E01001604,E01001605,E01001606,E01001607,E01001608,E01001609,E01001610,E01001611,E01001613,E01001614,E01001615,E01001617,E01001618,E01001619,E01001620,E01001621,E01001623,E01001624,E01001625,E01001629,E01001631,E01001632,E01001633,E01001634,E01001635,E01001636,E01001637,E01001638,E01001641,E01001642,E01001644,E01001645,E01001646,E01001647,E01001648,E01001649,E01001650,E01001651,E01001652,E01001653,E01001654,E01001655,E01001656,E01001657,E01001658,E01001659,E01001660,E01001661,E01001664,E01001665,E01001666,E01001669,E01001671,E01001672,E01001673,E01001674,E01001675,E01001676,E01001677,E01001678,E01001679,E01001680,E01001681,E01001682,E01001683,E01001684,E01001685,E01001686,E01001687,E01001688,E01001690,E01001692,E01001693,E01001694,E01001695,E01001696,E01001697,E01001698,E01001699,E01001700,E01001701,E01001702,E01001703,E01001704,E01001705,E01001708,E01001710,E01001712,E01001714,E01001715,E01001716,E01001717,E01001718,E01001719,E01001720,E01001722,E01001723,E01001724,E01001725,E01001726,E01001727,E01001728,E01001729,E01001730,E01001731,E01001732,E01001733,E01001734,E01001735,E01001736,E01001737,E01001738,E01001739,E01001740,E01001741,E01001742,E01001744,E01001745,E01001746,E01001747,E01001749,E01001750,E01001751,E01001753,E01001755,E01001756,E01001757,E01001758,E01001759,E01001760,E01001761,E01001762,E01001763,E01001764,E01001765,E01001767,E01001768,E01001769,E01001770,E01001772,E01001773,E01001774,E01001775,E01001776,E01001777,E01001780,E01001781,E01001783,E01001784,E01001785,E01001786,E01001787,E01001788,E01001789,E01001790,E01001791,E01001792,E01001793,E01001794,E01001795,E01001796,E01001798,E01001799,E01001800,E01001801,E01001802,E01001803,E01001804,E01001805,E01001806,E01001807,E01001808,E01001809,E01001811,E01001812,E01001813,E01001814,E01001815,E01001816,E01001819,E01001820,E01001821,E01001822,E01001823,E01001824,E01001825,E01001826,E01001827,E01001828,E01001829,E01001830,E01001831,E01001832,E01001833,E01001834,E01001835,E01001836,E01001837,E01001838,E01001839,E01001840,E01001841,E01001842,E01001843,E01001844,E01001845,E01001846,E01001847,E01001848,E01001849,E01001850,E01001851,E01001852,E01001853,E01001854,E01001855,E01001856,E01001857,E01001858,E01001859,E01001860,E01001861,E01001862,E01001863,E01001864,E01001865,E01001866,E01001867,E01001868,E01001869,E01001870,E01001871,E01001872,E01001873,E01001875,E01001876,E01001877,E01001878,E01001879,E01001880,E01001881,E01001882,E01001883,E01001884,E01001885,E01001886,E01001887,E01001888,E01001889,E01001890,E01001891,E01001892,E01001893,E01001894,E01001895,E01001896,E01001897,E01001898,E01001899,E01001900,E01001901,E01001902,E01001903,E01001904,E01001905,E01001906,E01001907,E01001908,E01001909,E01001910,E01001911,E01001912,E01001913,E01001914,E01001915,E01001916,E01001917,E01001918,E01001919,E01001920,E01001921,E01001922,E01001923,E01001924,E01001925,E01001926,E01001927,E01001928,E01001929,E01001930,E01001931,E01001932,E01001933,E01001934,E01001935,E01001937,E01001938,E01001940,E01001941,E01001942,E01001943,E01001944,E01001945,E01001946,E01001947,E01001948,E01001949,E01001950,E01001951,E01001952,E01001953,E01001954,E01001955,E01001956,E01001957,E01001958,E01001959,E01001960,E01001961,E01001962,E01001963,E01001964,E01001965,E01001966,E01001967,E01001968,E01001969,E01001970,E01001971,E01001972,E01001973,E01001974,E01001975,E01001976,E01001977,E01001978,E01001979,E01001980,E01001981,E01001982,E01001983,E01001984,E01001985,E01001986,E01001987,E01001988,E01001989,E01001990,E01001991,E01001992,E01001993,E01001994,E01001995,E01001996,E01001997,E01001998,E01001999,E01002000,E01002001,E01002002,E01002003,E01002004,E01002005,E01002006,E01002007,E01002008,E01002009,E01002010,E01002011,E01002013,E01002014,E01002015,E01002016,E01002017,E01002018,E01002019,E01002020,E01002021,E01002022,E01002023,E01002024,E01002025,E01002026,E01002027,E01002028,E01002029,E01002030,E01002031,E01002032,E01002033,E01002034,E01002036,E01002037,E01002038,E01002039,E01002040,E01002041,E01002042,E01002043,E01002044,E01002045,E01002046,E01002047,E01002048,E01002049,E01002050,E01002051,E01002052,E01002053,E01002054,E01002055,E01002056,E01002057,E01002058,E01002059,E01002060,E01002061,E01002062,E01002063,E01002064,E01002065,E01002066,E01002067,E01002068,E01002069,E01002070,E01002071,E01002072,E01002073,E01002074,E01002075,E01002076,E01002077,E01002078,E01002079,E01002081,E01002082,E01002083,E01002084,E01002085,E01002086,E01002087,E01002088,E01002089,E01002090,E01002091,E01002092,E01002093,E01002094,E01002095,E01002096,E01002097,E01002098,E01002099,E01002100,E01002101,E01002102&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_004.json.gz + content_sha256: 0e425fc25ba556b447c0d91ea280d55ae563ab5ed21b8710e7466a01d2885b14 +- id: census_lsoa_tenure5a_bedrooms_005 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 005 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01002103,E01002104,E01002105,E01002106,E01002107,E01002108,E01002109,E01002110,E01002111,E01002112,E01002113,E01002114,E01002115,E01002116,E01002118,E01002119,E01002120,E01002121,E01002122,E01002123,E01002124,E01002125,E01002127,E01002128,E01002129,E01002130,E01002131,E01002132,E01002133,E01002134,E01002135,E01002136,E01002137,E01002138,E01002139,E01002140,E01002141,E01002142,E01002143,E01002144,E01002145,E01002146,E01002147,E01002148,E01002149,E01002150,E01002151,E01002152,E01002153,E01002155,E01002156,E01002157,E01002158,E01002159,E01002160,E01002161,E01002162,E01002163,E01002164,E01002165,E01002166,E01002167,E01002168,E01002169,E01002170,E01002171,E01002172,E01002173,E01002174,E01002175,E01002176,E01002177,E01002178,E01002179,E01002181,E01002182,E01002183,E01002184,E01002185,E01002186,E01002187,E01002188,E01002189,E01002190,E01002191,E01002192,E01002193,E01002194,E01002195,E01002196,E01002197,E01002198,E01002199,E01002200,E01002201,E01002202,E01002203,E01002204,E01002206,E01002207,E01002208,E01002209,E01002210,E01002211,E01002212,E01002214,E01002215,E01002216,E01002217,E01002218,E01002219,E01002220,E01002221,E01002222,E01002223,E01002224,E01002225,E01002226,E01002227,E01002228,E01002229,E01002230,E01002231,E01002232,E01002233,E01002234,E01002235,E01002236,E01002237,E01002238,E01002239,E01002240,E01002241,E01002242,E01002243,E01002244,E01002245,E01002246,E01002247,E01002248,E01002250,E01002251,E01002252,E01002253,E01002254,E01002255,E01002256,E01002257,E01002258,E01002259,E01002260,E01002261,E01002262,E01002263,E01002264,E01002265,E01002266,E01002267,E01002268,E01002269,E01002270,E01002271,E01002272,E01002273,E01002274,E01002275,E01002276,E01002277,E01002278,E01002279,E01002280,E01002281,E01002282,E01002283,E01002284,E01002285,E01002286,E01002287,E01002288,E01002289,E01002290,E01002291,E01002292,E01002294,E01002295,E01002296,E01002297,E01002298,E01002299,E01002300,E01002301,E01002302,E01002303,E01002304,E01002305,E01002306,E01002307,E01002308,E01002309,E01002310,E01002311,E01002312,E01002313,E01002314,E01002315,E01002316,E01002317,E01002318,E01002319,E01002320,E01002321,E01002322,E01002323,E01002324,E01002325,E01002326,E01002327,E01002328,E01002329,E01002330,E01002331,E01002332,E01002333,E01002334,E01002335,E01002336,E01002337,E01002338,E01002339,E01002340,E01002341,E01002342,E01002343,E01002344,E01002345,E01002346,E01002347,E01002348,E01002349,E01002350,E01002351,E01002352,E01002353,E01002354,E01002355,E01002356,E01002358,E01002359,E01002360,E01002361,E01002362,E01002363,E01002364,E01002365,E01002366,E01002367,E01002368,E01002369,E01002370,E01002371,E01002372,E01002373,E01002374,E01002375,E01002376,E01002377,E01002378,E01002379,E01002380,E01002381,E01002382,E01002383,E01002384,E01002385,E01002386,E01002387,E01002388,E01002389,E01002390,E01002391,E01002392,E01002393,E01002394,E01002395,E01002396,E01002397,E01002398,E01002399,E01002400,E01002401,E01002402,E01002403,E01002404,E01002405,E01002408,E01002409,E01002410,E01002411,E01002412,E01002413,E01002414,E01002415,E01002416,E01002417,E01002418,E01002419,E01002420,E01002421,E01002422,E01002423,E01002424,E01002425,E01002426,E01002427,E01002428,E01002429,E01002430,E01002431,E01002432,E01002433,E01002434,E01002435,E01002436,E01002437,E01002438,E01002439,E01002440,E01002441,E01002442,E01002443,E01002444,E01002445,E01002446,E01002447,E01002448,E01002449,E01002450,E01002451,E01002452,E01002453,E01002454,E01002455,E01002456,E01002457,E01002458,E01002459,E01002460,E01002461,E01002462,E01002463,E01002464,E01002465,E01002466,E01002467,E01002468,E01002469,E01002470,E01002471,E01002472,E01002473,E01002474,E01002475,E01002476,E01002477,E01002478,E01002479,E01002480,E01002481,E01002482,E01002483,E01002484,E01002485,E01002486,E01002487,E01002488,E01002489,E01002490,E01002491,E01002492,E01002493,E01002494,E01002495,E01002496,E01002497,E01002498,E01002499,E01002501,E01002502,E01002503,E01002505,E01002506,E01002507,E01002508,E01002509,E01002511,E01002512,E01002513,E01002514,E01002516,E01002518,E01002519,E01002520,E01002521,E01002523,E01002525,E01002526,E01002527,E01002528,E01002529,E01002530,E01002531,E01002533,E01002534,E01002535,E01002536,E01002537,E01002538,E01002539,E01002540,E01002541,E01002542,E01002543,E01002544,E01002545,E01002546,E01002547,E01002548,E01002549,E01002550,E01002551,E01002552,E01002553,E01002555,E01002556,E01002557,E01002558,E01002559,E01002560,E01002561,E01002562,E01002563,E01002564,E01002565,E01002567,E01002569,E01002570,E01002571,E01002572,E01002573,E01002574,E01002575,E01002576,E01002577,E01002578,E01002579,E01002580,E01002581,E01002582,E01002583,E01002584,E01002585,E01002586,E01002587,E01002588,E01002589,E01002590,E01002591,E01002592,E01002593,E01002594,E01002595,E01002596,E01002598,E01002599,E01002600,E01002601,E01002602,E01002603,E01002604,E01002605,E01002606,E01002607,E01002608,E01002609,E01002610,E01002611,E01002612,E01002613,E01002614,E01002615,E01002616,E01002617,E01002618,E01002619,E01002620,E01002621,E01002622,E01002623,E01002624,E01002625&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_005.json.gz + content_sha256: 58f73b7f289066ee70cc2b92d1f84df3195210d45d98e5661fed265683405862 +- id: census_lsoa_tenure5a_bedrooms_006 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 006 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01002626,E01002627,E01002628,E01002629,E01002630,E01002631,E01002632,E01002633,E01002634,E01002635,E01002636,E01002637,E01002638,E01002639,E01002640,E01002641,E01002643,E01002645,E01002646,E01002647,E01002649,E01002650,E01002651,E01002652,E01002653,E01002654,E01002655,E01002656,E01002657,E01002658,E01002659,E01002660,E01002661,E01002662,E01002663,E01002665,E01002666,E01002667,E01002668,E01002669,E01002670,E01002671,E01002672,E01002673,E01002674,E01002675,E01002676,E01002677,E01002678,E01002679,E01002680,E01002681,E01002682,E01002686,E01002687,E01002689,E01002690,E01002691,E01002692,E01002693,E01002694,E01002695,E01002696,E01002697,E01002698,E01002699,E01002700,E01002701,E01002702,E01002703,E01002704,E01002706,E01002707,E01002708,E01002709,E01002710,E01002711,E01002712,E01002713,E01002714,E01002715,E01002716,E01002717,E01002718,E01002719,E01002720,E01002721,E01002722,E01002723,E01002724,E01002725,E01002726,E01002727,E01002728,E01002729,E01002730,E01002732,E01002733,E01002734,E01002735,E01002736,E01002737,E01002738,E01002739,E01002740,E01002741,E01002742,E01002743,E01002744,E01002745,E01002747,E01002748,E01002749,E01002751,E01002752,E01002753,E01002754,E01002755,E01002756,E01002757,E01002758,E01002759,E01002760,E01002762,E01002763,E01002764,E01002765,E01002766,E01002767,E01002768,E01002769,E01002770,E01002771,E01002772,E01002773,E01002774,E01002775,E01002776,E01002777,E01002778,E01002779,E01002780,E01002781,E01002782,E01002783,E01002784,E01002785,E01002786,E01002787,E01002788,E01002789,E01002790,E01002791,E01002792,E01002793,E01002794,E01002795,E01002796,E01002797,E01002798,E01002799,E01002800,E01002801,E01002802,E01002804,E01002805,E01002806,E01002807,E01002808,E01002809,E01002810,E01002811,E01002812,E01002813,E01002814,E01002815,E01002816,E01002817,E01002818,E01002819,E01002821,E01002823,E01002824,E01002825,E01002826,E01002827,E01002828,E01002829,E01002830,E01002831,E01002832,E01002833,E01002834,E01002835,E01002836,E01002837,E01002839,E01002840,E01002841,E01002842,E01002843,E01002844,E01002845,E01002846,E01002847,E01002848,E01002849,E01002850,E01002851,E01002852,E01002853,E01002854,E01002855,E01002856,E01002857,E01002858,E01002859,E01002860,E01002861,E01002862,E01002863,E01002864,E01002865,E01002866,E01002867,E01002868,E01002869,E01002870,E01002871,E01002872,E01002873,E01002874,E01002875,E01002876,E01002877,E01002878,E01002879,E01002880,E01002881,E01002882,E01002883,E01002884,E01002885,E01002886,E01002887,E01002888,E01002889,E01002890,E01002891,E01002892,E01002893,E01002894,E01002896,E01002897,E01002898,E01002899,E01002900,E01002901,E01002902,E01002903,E01002904,E01002905,E01002906,E01002907,E01002908,E01002909,E01002910,E01002911,E01002912,E01002913,E01002914,E01002915,E01002916,E01002917,E01002918,E01002919,E01002920,E01002921,E01002922,E01002923,E01002924,E01002925,E01002926,E01002927,E01002928,E01002929,E01002930,E01002931,E01002932,E01002933,E01002934,E01002935,E01002937,E01002938,E01002939,E01002940,E01002941,E01002942,E01002943,E01002944,E01002945,E01002946,E01002947,E01002948,E01002949,E01002950,E01002951,E01002952,E01002953,E01002954,E01002955,E01002956,E01002957,E01002958,E01002959,E01002960,E01002961,E01002962,E01002963,E01002964,E01002965,E01002966,E01002967,E01002968,E01002969,E01002970,E01002971,E01002972,E01002973,E01002974,E01002975,E01002976,E01002977,E01002978,E01002979,E01002980,E01002981,E01002982,E01002983,E01002984,E01002985,E01002986,E01002987,E01002988,E01002989,E01002990,E01002991,E01002992,E01002993,E01002994,E01002995,E01002996,E01002997,E01002998,E01002999,E01003000,E01003001,E01003002,E01003003,E01003004,E01003005,E01003006,E01003007,E01003008,E01003009,E01003010,E01003011,E01003013,E01003014,E01003015,E01003016,E01003017,E01003018,E01003019,E01003020,E01003021,E01003022,E01003023,E01003024,E01003025,E01003026,E01003027,E01003028,E01003029,E01003030,E01003031,E01003032,E01003033,E01003034,E01003035,E01003036,E01003037,E01003038,E01003039,E01003040,E01003041,E01003042,E01003043,E01003044,E01003045,E01003046,E01003047,E01003048,E01003049,E01003050,E01003051,E01003052,E01003053,E01003054,E01003055,E01003056,E01003057,E01003058,E01003059,E01003060,E01003061,E01003062,E01003063,E01003064,E01003065,E01003066,E01003067,E01003068,E01003069,E01003070,E01003071,E01003072,E01003073,E01003074,E01003075,E01003076,E01003077,E01003078,E01003079,E01003080,E01003081,E01003082,E01003083,E01003084,E01003085,E01003086,E01003087,E01003088,E01003089,E01003090,E01003091,E01003092,E01003093,E01003094,E01003095,E01003096,E01003097,E01003098,E01003099,E01003100,E01003101,E01003102,E01003104,E01003105,E01003106,E01003107,E01003108,E01003110,E01003111,E01003112,E01003113,E01003114,E01003115,E01003116,E01003117,E01003118,E01003119,E01003120,E01003121,E01003122,E01003123,E01003124,E01003125,E01003126,E01003127,E01003128,E01003129,E01003130,E01003131,E01003132,E01003133,E01003134,E01003135,E01003136,E01003137,E01003138,E01003139,E01003140,E01003141,E01003142,E01003143,E01003144,E01003145,E01003146,E01003147&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_006.json.gz + content_sha256: d359a4cc9fd12dc44ffb14c375438e6e950315c64950b40bb9ff2671f4c2f609 +- id: census_lsoa_tenure5a_bedrooms_007 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 007 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01003148,E01003149,E01003150,E01003151,E01003152,E01003153,E01003154,E01003155,E01003156,E01003157,E01003158,E01003159,E01003160,E01003161,E01003162,E01003163,E01003164,E01003165,E01003166,E01003167,E01003168,E01003169,E01003170,E01003171,E01003172,E01003173,E01003174,E01003175,E01003176,E01003177,E01003178,E01003179,E01003180,E01003181,E01003182,E01003183,E01003184,E01003185,E01003186,E01003187,E01003188,E01003189,E01003190,E01003191,E01003192,E01003193,E01003194,E01003195,E01003196,E01003197,E01003198,E01003199,E01003200,E01003201,E01003202,E01003203,E01003204,E01003205,E01003206,E01003207,E01003209,E01003210,E01003211,E01003212,E01003213,E01003214,E01003215,E01003216,E01003217,E01003218,E01003219,E01003220,E01003221,E01003222,E01003223,E01003224,E01003225,E01003226,E01003227,E01003228,E01003229,E01003230,E01003231,E01003232,E01003233,E01003235,E01003236,E01003237,E01003238,E01003239,E01003240,E01003241,E01003243,E01003244,E01003245,E01003246,E01003247,E01003248,E01003249,E01003251,E01003252,E01003253,E01003254,E01003255,E01003256,E01003257,E01003258,E01003259,E01003260,E01003261,E01003262,E01003263,E01003264,E01003265,E01003266,E01003267,E01003268,E01003269,E01003270,E01003271,E01003272,E01003273,E01003274,E01003275,E01003276,E01003277,E01003278,E01003279,E01003280,E01003281,E01003282,E01003283,E01003284,E01003285,E01003286,E01003287,E01003288,E01003290,E01003291,E01003292,E01003293,E01003295,E01003297,E01003298,E01003299,E01003300,E01003301,E01003302,E01003303,E01003304,E01003305,E01003307,E01003308,E01003309,E01003310,E01003311,E01003312,E01003313,E01003314,E01003315,E01003316,E01003318,E01003319,E01003320,E01003321,E01003322,E01003323,E01003324,E01003325,E01003326,E01003327,E01003328,E01003329,E01003330,E01003331,E01003332,E01003333,E01003334,E01003335,E01003336,E01003337,E01003338,E01003339,E01003340,E01003341,E01003342,E01003343,E01003344,E01003345,E01003346,E01003347,E01003348,E01003349,E01003350,E01003351,E01003352,E01003353,E01003354,E01003355,E01003356,E01003357,E01003358,E01003359,E01003360,E01003361,E01003362,E01003363,E01003364,E01003365,E01003366,E01003368,E01003369,E01003370,E01003371,E01003372,E01003373,E01003374,E01003375,E01003376,E01003377,E01003378,E01003379,E01003380,E01003381,E01003382,E01003383,E01003384,E01003385,E01003386,E01003387,E01003388,E01003389,E01003390,E01003391,E01003392,E01003393,E01003394,E01003395,E01003396,E01003397,E01003398,E01003399,E01003400,E01003401,E01003402,E01003403,E01003404,E01003405,E01003406,E01003407,E01003408,E01003409,E01003410,E01003411,E01003412,E01003413,E01003414,E01003415,E01003416,E01003417,E01003418,E01003419,E01003420,E01003421,E01003422,E01003423,E01003424,E01003425,E01003426,E01003427,E01003428,E01003429,E01003430,E01003431,E01003432,E01003433,E01003434,E01003435,E01003436,E01003437,E01003438,E01003439,E01003440,E01003441,E01003442,E01003443,E01003444,E01003445,E01003446,E01003447,E01003448,E01003449,E01003450,E01003451,E01003452,E01003453,E01003454,E01003455,E01003456,E01003457,E01003458,E01003459,E01003460,E01003461,E01003462,E01003463,E01003464,E01003465,E01003466,E01003467,E01003468,E01003469,E01003470,E01003471,E01003472,E01003473,E01003474,E01003476,E01003477,E01003478,E01003479,E01003480,E01003481,E01003483,E01003484,E01003485,E01003486,E01003487,E01003488,E01003489,E01003490,E01003491,E01003492,E01003494,E01003495,E01003496,E01003497,E01003498,E01003499,E01003500,E01003501,E01003502,E01003503,E01003506,E01003507,E01003508,E01003509,E01003511,E01003512,E01003513,E01003514,E01003515,E01003516,E01003517,E01003518,E01003519,E01003520,E01003521,E01003522,E01003523,E01003524,E01003525,E01003526,E01003527,E01003528,E01003529,E01003530,E01003531,E01003532,E01003533,E01003534,E01003535,E01003536,E01003537,E01003538,E01003539,E01003540,E01003541,E01003542,E01003543,E01003544,E01003545,E01003546,E01003547,E01003548,E01003549,E01003550,E01003551,E01003552,E01003553,E01003554,E01003555,E01003556,E01003557,E01003558,E01003559,E01003560,E01003561,E01003562,E01003563,E01003564,E01003565,E01003566,E01003567,E01003568,E01003569,E01003570,E01003571,E01003572,E01003573,E01003574,E01003575,E01003576,E01003577,E01003578,E01003579,E01003580,E01003581,E01003582,E01003583,E01003584,E01003585,E01003586,E01003587,E01003588,E01003589,E01003590,E01003591,E01003592,E01003593,E01003594,E01003595,E01003596,E01003597,E01003598,E01003599,E01003600,E01003601,E01003602,E01003603,E01003604,E01003605,E01003607,E01003608,E01003611,E01003615,E01003616,E01003618,E01003619,E01003621,E01003622,E01003623,E01003624,E01003625,E01003626,E01003627,E01003628,E01003629,E01003630,E01003631,E01003632,E01003633,E01003634,E01003635,E01003636,E01003637,E01003638,E01003639,E01003640,E01003641,E01003642,E01003643,E01003644,E01003645,E01003646,E01003647,E01003648,E01003649,E01003650,E01003651,E01003652,E01003653,E01003654,E01003655,E01003656,E01003657,E01003658,E01003659,E01003660,E01003661,E01003662,E01003663,E01003664,E01003665,E01003666,E01003668,E01003669,E01003670,E01003671,E01003672&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_007.json.gz + content_sha256: 225ceac0a244949186db90b49a1f0706c2190bef9497b1527b67f35db2440f9b +- id: census_lsoa_tenure5a_bedrooms_008 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 008 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01003673,E01003674,E01003675,E01003676,E01003677,E01003678,E01003679,E01003680,E01003681,E01003682,E01003683,E01003684,E01003685,E01003686,E01003688,E01003689,E01003690,E01003691,E01003692,E01003693,E01003694,E01003695,E01003696,E01003697,E01003698,E01003699,E01003700,E01003701,E01003702,E01003703,E01003704,E01003705,E01003706,E01003707,E01003708,E01003709,E01003710,E01003711,E01003712,E01003713,E01003714,E01003715,E01003716,E01003718,E01003719,E01003720,E01003721,E01003722,E01003723,E01003724,E01003725,E01003726,E01003727,E01003729,E01003730,E01003731,E01003732,E01003733,E01003734,E01003735,E01003736,E01003737,E01003738,E01003739,E01003740,E01003741,E01003742,E01003743,E01003744,E01003745,E01003746,E01003747,E01003748,E01003749,E01003750,E01003751,E01003752,E01003753,E01003754,E01003755,E01003756,E01003757,E01003759,E01003760,E01003761,E01003762,E01003763,E01003764,E01003765,E01003766,E01003767,E01003768,E01003769,E01003770,E01003771,E01003772,E01003773,E01003774,E01003775,E01003776,E01003777,E01003778,E01003779,E01003780,E01003781,E01003782,E01003783,E01003784,E01003785,E01003786,E01003787,E01003788,E01003789,E01003790,E01003791,E01003792,E01003793,E01003794,E01003795,E01003796,E01003797,E01003798,E01003799,E01003800,E01003801,E01003802,E01003803,E01003804,E01003805,E01003806,E01003807,E01003808,E01003809,E01003810,E01003811,E01003812,E01003813,E01003814,E01003815,E01003816,E01003817,E01003818,E01003819,E01003820,E01003821,E01003822,E01003823,E01003824,E01003825,E01003826,E01003827,E01003828,E01003829,E01003830,E01003831,E01003832,E01003833,E01003834,E01003835,E01003836,E01003837,E01003838,E01003839,E01003840,E01003841,E01003842,E01003843,E01003844,E01003845,E01003846,E01003847,E01003848,E01003849,E01003851,E01003852,E01003853,E01003854,E01003855,E01003856,E01003857,E01003858,E01003859,E01003860,E01003861,E01003862,E01003863,E01003864,E01003865,E01003866,E01003867,E01003868,E01003869,E01003870,E01003871,E01003872,E01003873,E01003874,E01003875,E01003876,E01003877,E01003878,E01003879,E01003880,E01003881,E01003882,E01003883,E01003884,E01003885,E01003886,E01003887,E01003888,E01003889,E01003890,E01003891,E01003892,E01003893,E01003894,E01003895,E01003896,E01003897,E01003898,E01003899,E01003900,E01003901,E01003902,E01003903,E01003904,E01003905,E01003906,E01003907,E01003908,E01003909,E01003910,E01003911,E01003912,E01003913,E01003914,E01003915,E01003916,E01003917,E01003918,E01003919,E01003920,E01003921,E01003922,E01003923,E01003924,E01003925,E01003926,E01003929,E01003930,E01003932,E01003933,E01003934,E01003935,E01003936,E01003937,E01003938,E01003939,E01003940,E01003941,E01003942,E01003943,E01003945,E01003946,E01003947,E01003948,E01003949,E01003950,E01003951,E01003952,E01003953,E01003954,E01003955,E01003956,E01003957,E01003958,E01003960,E01003961,E01003963,E01003966,E01003969,E01003971,E01003972,E01003973,E01003975,E01003976,E01003977,E01003979,E01003980,E01003981,E01003983,E01003984,E01003986,E01003987,E01003988,E01003989,E01003990,E01003991,E01003992,E01003993,E01003994,E01003995,E01003997,E01003998,E01003999,E01004000,E01004001,E01004002,E01004003,E01004004,E01004005,E01004006,E01004007,E01004008,E01004010,E01004011,E01004012,E01004013,E01004014,E01004015,E01004016,E01004017,E01004018,E01004019,E01004020,E01004021,E01004023,E01004024,E01004028,E01004029,E01004030,E01004031,E01004032,E01004033,E01004034,E01004035,E01004036,E01004037,E01004038,E01004039,E01004040,E01004041,E01004042,E01004043,E01004044,E01004045,E01004046,E01004047,E01004048,E01004049,E01004050,E01004051,E01004052,E01004053,E01004054,E01004055,E01004056,E01004057,E01004058,E01004059,E01004060,E01004061,E01004062,E01004063,E01004064,E01004065,E01004066,E01004067,E01004068,E01004069,E01004070,E01004071,E01004072,E01004073,E01004074,E01004075,E01004076,E01004077,E01004078,E01004079,E01004080,E01004081,E01004082,E01004083,E01004084,E01004085,E01004086,E01004087,E01004088,E01004089,E01004090,E01004091,E01004092,E01004093,E01004094,E01004095,E01004096,E01004097,E01004098,E01004099,E01004100,E01004101,E01004102,E01004103,E01004104,E01004105,E01004106,E01004107,E01004108,E01004109,E01004110,E01004111,E01004112,E01004114,E01004115,E01004116,E01004117,E01004118,E01004119,E01004120,E01004121,E01004122,E01004123,E01004124,E01004125,E01004126,E01004127,E01004128,E01004129,E01004130,E01004131,E01004132,E01004133,E01004134,E01004135,E01004136,E01004137,E01004138,E01004139,E01004140,E01004141,E01004143,E01004144,E01004145,E01004146,E01004147,E01004148,E01004149,E01004150,E01004151,E01004152,E01004153,E01004154,E01004155,E01004156,E01004157,E01004158,E01004159,E01004160,E01004161,E01004162,E01004163,E01004164,E01004165,E01004166,E01004167,E01004168,E01004169,E01004170,E01004171,E01004172,E01004173,E01004174,E01004175,E01004176,E01004177,E01004178,E01004179,E01004180,E01004181,E01004182,E01004183,E01004184,E01004185,E01004186,E01004187,E01004189,E01004190,E01004191,E01004192,E01004193,E01004194,E01004195,E01004196,E01004198,E01004199,E01004200,E01004201,E01004202&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_008.json.gz + content_sha256: a88693ef4a27708857fad2ed9812ef3495d2d4f10c46baa569aee1cb753743e9 +- id: census_lsoa_tenure5a_bedrooms_009 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 009 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01004203,E01004204,E01004205,E01004206,E01004207,E01004208,E01004209,E01004211,E01004212,E01004214,E01004216,E01004217,E01004218,E01004219,E01004221,E01004222,E01004223,E01004226,E01004228,E01004229,E01004230,E01004231,E01004232,E01004233,E01004234,E01004235,E01004236,E01004237,E01004239,E01004240,E01004241,E01004243,E01004244,E01004245,E01004247,E01004248,E01004249,E01004251,E01004255,E01004256,E01004257,E01004258,E01004259,E01004260,E01004263,E01004264,E01004266,E01004267,E01004268,E01004270,E01004271,E01004272,E01004274,E01004275,E01004276,E01004277,E01004280,E01004281,E01004283,E01004286,E01004287,E01004288,E01004289,E01004290,E01004291,E01004292,E01004293,E01004295,E01004296,E01004297,E01004298,E01004300,E01004301,E01004302,E01004303,E01004304,E01004305,E01004306,E01004307,E01004308,E01004309,E01004310,E01004311,E01004312,E01004313,E01004315,E01004316,E01004319,E01004321,E01004322,E01004323,E01004324,E01004326,E01004327,E01004328,E01004329,E01004330,E01004331,E01004332,E01004333,E01004334,E01004335,E01004336,E01004337,E01004338,E01004339,E01004340,E01004341,E01004342,E01004343,E01004344,E01004345,E01004346,E01004347,E01004348,E01004349,E01004350,E01004351,E01004352,E01004353,E01004354,E01004355,E01004356,E01004357,E01004358,E01004359,E01004360,E01004361,E01004362,E01004363,E01004364,E01004365,E01004366,E01004367,E01004368,E01004369,E01004370,E01004371,E01004372,E01004373,E01004374,E01004375,E01004376,E01004377,E01004378,E01004379,E01004380,E01004381,E01004382,E01004383,E01004384,E01004385,E01004386,E01004387,E01004388,E01004389,E01004390,E01004391,E01004392,E01004393,E01004394,E01004395,E01004396,E01004397,E01004398,E01004399,E01004400,E01004401,E01004402,E01004403,E01004404,E01004405,E01004406,E01004407,E01004408,E01004409,E01004410,E01004411,E01004412,E01004413,E01004414,E01004415,E01004416,E01004417,E01004418,E01004419,E01004420,E01004421,E01004422,E01004423,E01004424,E01004425,E01004426,E01004427,E01004428,E01004429,E01004430,E01004432,E01004434,E01004435,E01004436,E01004437,E01004438,E01004439,E01004440,E01004441,E01004442,E01004443,E01004444,E01004445,E01004446,E01004447,E01004448,E01004449,E01004450,E01004451,E01004452,E01004453,E01004454,E01004455,E01004456,E01004457,E01004458,E01004459,E01004460,E01004462,E01004463,E01004464,E01004465,E01004466,E01004467,E01004468,E01004469,E01004470,E01004471,E01004472,E01004473,E01004474,E01004475,E01004476,E01004477,E01004478,E01004479,E01004480,E01004481,E01004482,E01004483,E01004484,E01004485,E01004486,E01004487,E01004488,E01004489,E01004490,E01004491,E01004492,E01004493,E01004494,E01004495,E01004496,E01004497,E01004498,E01004499,E01004500,E01004501,E01004502,E01004503,E01004504,E01004505,E01004506,E01004507,E01004508,E01004510,E01004511,E01004512,E01004513,E01004514,E01004515,E01004516,E01004517,E01004518,E01004519,E01004520,E01004521,E01004522,E01004523,E01004524,E01004525,E01004526,E01004527,E01004528,E01004529,E01004530,E01004531,E01004532,E01004533,E01004534,E01004535,E01004536,E01004537,E01004538,E01004539,E01004540,E01004541,E01004542,E01004543,E01004544,E01004545,E01004546,E01004547,E01004548,E01004549,E01004550,E01004551,E01004552,E01004553,E01004554,E01004555,E01004556,E01004557,E01004558,E01004559,E01004560,E01004563,E01004564,E01004565,E01004566,E01004567,E01004568,E01004569,E01004570,E01004571,E01004572,E01004573,E01004574,E01004575,E01004576,E01004577,E01004579,E01004580,E01004581,E01004582,E01004583,E01004584,E01004585,E01004586,E01004587,E01004588,E01004589,E01004590,E01004591,E01004592,E01004593,E01004594,E01004595,E01004596,E01004597,E01004598,E01004599,E01004601,E01004602,E01004604,E01004605,E01004606,E01004607,E01004608,E01004609,E01004610,E01004611,E01004612,E01004613,E01004614,E01004615,E01004616,E01004617,E01004618,E01004619,E01004620,E01004621,E01004622,E01004623,E01004624,E01004625,E01004626,E01004627,E01004628,E01004629,E01004630,E01004631,E01004632,E01004633,E01004634,E01004635,E01004636,E01004637,E01004638,E01004639,E01004640,E01004641,E01004642,E01004643,E01004644,E01004645,E01004646,E01004647,E01004648,E01004649,E01004650,E01004651,E01004652,E01004653,E01004654,E01004656,E01004657,E01004658,E01004659,E01004660,E01004661,E01004662,E01004663,E01004665,E01004668,E01004669,E01004670,E01004674,E01004675,E01004676,E01004677,E01004678,E01004679,E01004680,E01004681,E01004682,E01004683,E01004684,E01004686,E01004687,E01004690,E01004691,E01004692,E01004693,E01004694,E01004695,E01004696,E01004697,E01004698,E01004699,E01004700,E01004701,E01004702,E01004703,E01004705,E01004706,E01004707,E01004708,E01004709,E01004710,E01004711,E01004712,E01004713,E01004714,E01004715,E01004716,E01004717,E01004718,E01004719,E01004720,E01004721,E01004722,E01004723,E01004724,E01004725,E01004726,E01004727,E01004728,E01004729,E01004730,E01004731,E01004732,E01004733,E01004734,E01004735,E01004736,E01004737,E01004740,E01004741,E01004742,E01004743,E01004744,E01004746,E01004747,E01004748,E01004749,E01004750,E01004751,E01004752,E01004753,E01004754,E01004755,E01004756&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_009.json.gz + content_sha256: 1c39708ece58bc09201f490d8bf2c4a8e697ea7a675026cc168e38da5cb8c484 +- id: census_lsoa_tenure5a_bedrooms_010 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 010 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01004757,E01004760,E01004761,E01004762,E01004763,E01004765,E01004766,E01004767,E01004768,E01004769,E01004770,E01004771,E01004772,E01004773,E01004774,E01004775,E01004776,E01004777,E01004778,E01004779,E01004780,E01004781,E01004782,E01004783,E01004784,E01004785,E01004786,E01004787,E01004788,E01004789,E01004790,E01004791,E01004792,E01004793,E01004794,E01004795,E01004796,E01004797,E01004798,E01004799,E01004800,E01004801,E01004802,E01004803,E01004804,E01004805,E01004806,E01004807,E01004808,E01004809,E01004810,E01004811,E01004812,E01004813,E01004814,E01004815,E01004816,E01004817,E01004818,E01004819,E01004820,E01004822,E01004823,E01004824,E01004825,E01004826,E01004827,E01004828,E01004829,E01004830,E01004831,E01004832,E01004833,E01004834,E01004835,E01004836,E01004837,E01004838,E01004839,E01004840,E01004841,E01004842,E01004843,E01004844,E01004845,E01004846,E01004847,E01004848,E01004849,E01004850,E01004851,E01004852,E01004853,E01004854,E01004855,E01004856,E01004857,E01004858,E01004859,E01004860,E01004861,E01004862,E01004863,E01004864,E01004865,E01004866,E01004867,E01004868,E01004869,E01004870,E01004871,E01004872,E01004873,E01004874,E01004875,E01004876,E01004877,E01004878,E01004879,E01004880,E01004881,E01004882,E01004883,E01004884,E01004885,E01004886,E01004887,E01004888,E01004889,E01004890,E01004891,E01004892,E01004893,E01004894,E01004895,E01004896,E01004897,E01004899,E01004900,E01004901,E01004902,E01004903,E01004904,E01004905,E01004906,E01004907,E01004908,E01004909,E01004910,E01004911,E01004912,E01004913,E01004914,E01004915,E01004916,E01004917,E01004918,E01004919,E01004920,E01004921,E01004922,E01004923,E01004924,E01004925,E01004926,E01004927,E01004928,E01004929,E01004930,E01004931,E01004932,E01004933,E01004934,E01004935,E01004936,E01004937,E01004938,E01004939,E01004940,E01004941,E01004942,E01004943,E01004944,E01004945,E01004946,E01004947,E01004948,E01004949,E01004950,E01004951,E01004952,E01004953,E01004954,E01004955,E01004956,E01004957,E01004958,E01004959,E01004960,E01004961,E01004962,E01004963,E01004964,E01004965,E01004966,E01004967,E01004968,E01004969,E01004970,E01004971,E01004972,E01004973,E01004974,E01004975,E01004976,E01004977,E01004978,E01004979,E01004980,E01004981,E01004982,E01004983,E01004984,E01004985,E01004986,E01004987,E01004988,E01004989,E01004990,E01004991,E01004992,E01004993,E01004994,E01004995,E01004996,E01004997,E01004998,E01004999,E01005000,E01005001,E01005002,E01005003,E01005004,E01005005,E01005006,E01005007,E01005008,E01005009,E01005010,E01005011,E01005012,E01005013,E01005014,E01005015,E01005016,E01005017,E01005018,E01005019,E01005020,E01005021,E01005022,E01005023,E01005024,E01005025,E01005026,E01005027,E01005028,E01005029,E01005030,E01005031,E01005032,E01005033,E01005034,E01005035,E01005036,E01005037,E01005038,E01005039,E01005040,E01005041,E01005042,E01005043,E01005044,E01005045,E01005046,E01005047,E01005048,E01005049,E01005050,E01005051,E01005052,E01005053,E01005054,E01005055,E01005056,E01005057,E01005058,E01005059,E01005060,E01005061,E01005063,E01005065,E01005066,E01005067,E01005068,E01005069,E01005070,E01005071,E01005072,E01005073,E01005074,E01005075,E01005076,E01005077,E01005078,E01005079,E01005081,E01005082,E01005083,E01005084,E01005085,E01005086,E01005087,E01005088,E01005089,E01005090,E01005091,E01005092,E01005093,E01005094,E01005097,E01005098,E01005099,E01005100,E01005101,E01005102,E01005103,E01005104,E01005105,E01005106,E01005107,E01005111,E01005112,E01005113,E01005114,E01005115,E01005116,E01005117,E01005118,E01005119,E01005120,E01005121,E01005122,E01005123,E01005124,E01005125,E01005126,E01005128,E01005129,E01005130,E01005132,E01005135,E01005136,E01005138,E01005139,E01005141,E01005142,E01005143,E01005145,E01005147,E01005149,E01005150,E01005151,E01005152,E01005153,E01005154,E01005155,E01005156,E01005157,E01005158,E01005159,E01005160,E01005161,E01005162,E01005163,E01005164,E01005165,E01005166,E01005167,E01005168,E01005169,E01005170,E01005171,E01005172,E01005173,E01005174,E01005175,E01005176,E01005177,E01005178,E01005179,E01005180,E01005181,E01005182,E01005183,E01005184,E01005185,E01005186,E01005187,E01005188,E01005189,E01005190,E01005191,E01005192,E01005193,E01005195,E01005197,E01005198,E01005199,E01005200,E01005201,E01005202,E01005203,E01005205,E01005206,E01005207,E01005208,E01005210,E01005212,E01005213,E01005214,E01005215,E01005216,E01005217,E01005218,E01005219,E01005220,E01005221,E01005222,E01005223,E01005224,E01005225,E01005226,E01005227,E01005228,E01005229,E01005230,E01005231,E01005232,E01005233,E01005234,E01005235,E01005236,E01005237,E01005238,E01005239,E01005243,E01005244,E01005245,E01005246,E01005247,E01005248,E01005249,E01005250,E01005251,E01005252,E01005253,E01005254,E01005255,E01005256,E01005257,E01005258,E01005259,E01005260,E01005261,E01005262,E01005263,E01005264,E01005265,E01005266,E01005267,E01005268,E01005269,E01005270,E01005271,E01005272,E01005273,E01005274,E01005275,E01005276,E01005277,E01005278,E01005279,E01005280,E01005281,E01005284,E01005285,E01005286,E01005287,E01005288&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_010.json.gz + content_sha256: ba48282eb8ac7649cb0c8d20a7c1c9d4c0238defa6d62c2f55b584fb5b95c590 +- id: census_lsoa_tenure5a_bedrooms_011 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 011 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01005289,E01005290,E01005291,E01005292,E01005293,E01005294,E01005295,E01005296,E01005297,E01005298,E01005299,E01005300,E01005301,E01005302,E01005304,E01005305,E01005306,E01005307,E01005308,E01005309,E01005310,E01005311,E01005312,E01005313,E01005314,E01005315,E01005316,E01005317,E01005318,E01005319,E01005320,E01005321,E01005322,E01005323,E01005324,E01005325,E01005326,E01005327,E01005328,E01005329,E01005330,E01005331,E01005332,E01005333,E01005334,E01005335,E01005336,E01005337,E01005338,E01005339,E01005340,E01005341,E01005342,E01005343,E01005344,E01005345,E01005346,E01005347,E01005348,E01005349,E01005350,E01005351,E01005352,E01005353,E01005354,E01005355,E01005356,E01005357,E01005358,E01005359,E01005360,E01005361,E01005362,E01005363,E01005364,E01005365,E01005366,E01005367,E01005368,E01005369,E01005370,E01005371,E01005372,E01005373,E01005374,E01005375,E01005376,E01005377,E01005378,E01005379,E01005380,E01005381,E01005382,E01005383,E01005386,E01005387,E01005388,E01005389,E01005390,E01005391,E01005392,E01005393,E01005394,E01005395,E01005396,E01005397,E01005398,E01005399,E01005400,E01005401,E01005402,E01005403,E01005406,E01005407,E01005408,E01005409,E01005410,E01005411,E01005412,E01005413,E01005414,E01005415,E01005416,E01005417,E01005418,E01005419,E01005420,E01005421,E01005422,E01005427,E01005428,E01005429,E01005430,E01005431,E01005432,E01005433,E01005434,E01005435,E01005436,E01005437,E01005438,E01005439,E01005440,E01005441,E01005442,E01005443,E01005444,E01005445,E01005446,E01005447,E01005448,E01005449,E01005450,E01005451,E01005452,E01005453,E01005454,E01005455,E01005456,E01005459,E01005460,E01005461,E01005462,E01005463,E01005464,E01005465,E01005466,E01005467,E01005468,E01005469,E01005470,E01005471,E01005472,E01005473,E01005474,E01005475,E01005476,E01005477,E01005478,E01005479,E01005480,E01005481,E01005482,E01005483,E01005484,E01005485,E01005486,E01005487,E01005488,E01005489,E01005490,E01005491,E01005492,E01005493,E01005494,E01005495,E01005496,E01005497,E01005498,E01005499,E01005500,E01005501,E01005502,E01005503,E01005504,E01005505,E01005506,E01005507,E01005508,E01005509,E01005510,E01005511,E01005512,E01005513,E01005514,E01005515,E01005516,E01005517,E01005518,E01005519,E01005520,E01005521,E01005522,E01005523,E01005524,E01005525,E01005526,E01005527,E01005528,E01005529,E01005530,E01005531,E01005532,E01005533,E01005534,E01005535,E01005536,E01005537,E01005538,E01005539,E01005540,E01005541,E01005542,E01005543,E01005544,E01005545,E01005546,E01005547,E01005548,E01005549,E01005550,E01005551,E01005552,E01005553,E01005554,E01005555,E01005556,E01005557,E01005558,E01005559,E01005560,E01005561,E01005562,E01005563,E01005564,E01005565,E01005566,E01005567,E01005568,E01005569,E01005570,E01005571,E01005572,E01005573,E01005574,E01005575,E01005576,E01005577,E01005578,E01005579,E01005580,E01005581,E01005583,E01005585,E01005586,E01005587,E01005588,E01005589,E01005590,E01005591,E01005592,E01005593,E01005594,E01005595,E01005596,E01005597,E01005598,E01005599,E01005600,E01005601,E01005602,E01005603,E01005604,E01005605,E01005610,E01005611,E01005612,E01005613,E01005614,E01005615,E01005616,E01005617,E01005618,E01005619,E01005620,E01005621,E01005622,E01005623,E01005624,E01005625,E01005626,E01005627,E01005628,E01005629,E01005630,E01005631,E01005632,E01005633,E01005634,E01005635,E01005636,E01005637,E01005638,E01005639,E01005640,E01005641,E01005642,E01005643,E01005644,E01005645,E01005646,E01005647,E01005648,E01005649,E01005650,E01005651,E01005652,E01005653,E01005654,E01005656,E01005657,E01005659,E01005660,E01005661,E01005662,E01005663,E01005664,E01005665,E01005667,E01005670,E01005671,E01005672,E01005673,E01005674,E01005676,E01005677,E01005678,E01005679,E01005680,E01005681,E01005682,E01005683,E01005686,E01005687,E01005688,E01005689,E01005690,E01005691,E01005692,E01005693,E01005694,E01005695,E01005696,E01005697,E01005698,E01005699,E01005700,E01005701,E01005702,E01005703,E01005704,E01005706,E01005707,E01005708,E01005709,E01005710,E01005711,E01005712,E01005713,E01005714,E01005715,E01005716,E01005717,E01005718,E01005719,E01005721,E01005722,E01005723,E01005724,E01005725,E01005726,E01005727,E01005728,E01005729,E01005730,E01005731,E01005732,E01005733,E01005734,E01005735,E01005736,E01005737,E01005738,E01005739,E01005740,E01005741,E01005742,E01005743,E01005744,E01005745,E01005746,E01005747,E01005748,E01005749,E01005750,E01005751,E01005752,E01005753,E01005754,E01005755,E01005756,E01005757,E01005759,E01005760,E01005761,E01005762,E01005763,E01005764,E01005765,E01005766,E01005767,E01005768,E01005769,E01005770,E01005771,E01005772,E01005773,E01005774,E01005775,E01005776,E01005777,E01005778,E01005779,E01005780,E01005781,E01005782,E01005783,E01005784,E01005785,E01005786,E01005787,E01005788,E01005789,E01005790,E01005791,E01005792,E01005793,E01005794,E01005795,E01005796,E01005797,E01005798,E01005799,E01005800,E01005801,E01005802,E01005803,E01005804,E01005805,E01005806,E01005807,E01005808,E01005809,E01005810,E01005811,E01005812,E01005813,E01005814,E01005815,E01005816&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_011.json.gz + content_sha256: 9543fc7aabcaff820d0c6eeb943204daa12e28d8594d8016cb20e64baa971d8a +- id: census_lsoa_tenure5a_bedrooms_012 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 012 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01005817,E01005818,E01005819,E01005820,E01005821,E01005822,E01005823,E01005824,E01005825,E01005826,E01005827,E01005828,E01005829,E01005830,E01005831,E01005832,E01005833,E01005834,E01005835,E01005836,E01005837,E01005838,E01005839,E01005840,E01005841,E01005842,E01005843,E01005844,E01005845,E01005846,E01005847,E01005848,E01005849,E01005850,E01005851,E01005852,E01005853,E01005854,E01005855,E01005856,E01005857,E01005858,E01005859,E01005860,E01005861,E01005862,E01005863,E01005864,E01005865,E01005866,E01005867,E01005868,E01005869,E01005870,E01005871,E01005872,E01005873,E01005874,E01005875,E01005876,E01005877,E01005878,E01005879,E01005880,E01005881,E01005882,E01005883,E01005884,E01005885,E01005886,E01005887,E01005888,E01005889,E01005890,E01005891,E01005892,E01005893,E01005894,E01005895,E01005896,E01005897,E01005898,E01005899,E01005900,E01005901,E01005902,E01005903,E01005904,E01005905,E01005906,E01005907,E01005908,E01005909,E01005910,E01005911,E01005912,E01005913,E01005914,E01005915,E01005916,E01005917,E01005918,E01005919,E01005920,E01005921,E01005922,E01005923,E01005924,E01005925,E01005926,E01005927,E01005928,E01005929,E01005930,E01005931,E01005932,E01005933,E01005934,E01005935,E01005936,E01005937,E01005938,E01005939,E01005940,E01005941,E01005942,E01005943,E01005944,E01005945,E01005946,E01005947,E01005948,E01005949,E01005950,E01005951,E01005953,E01005954,E01005955,E01005956,E01005957,E01005958,E01005959,E01005960,E01005961,E01005962,E01005963,E01005964,E01005965,E01005966,E01005967,E01005968,E01005969,E01005970,E01005971,E01005972,E01005973,E01005974,E01005975,E01005976,E01005977,E01005978,E01005979,E01005980,E01005981,E01005982,E01005983,E01005984,E01005985,E01005986,E01005987,E01005988,E01005989,E01005990,E01005991,E01005992,E01005993,E01005994,E01005995,E01005996,E01005997,E01005998,E01005999,E01006000,E01006001,E01006002,E01006003,E01006004,E01006005,E01006006,E01006007,E01006008,E01006009,E01006010,E01006011,E01006012,E01006013,E01006014,E01006015,E01006016,E01006017,E01006018,E01006019,E01006020,E01006021,E01006022,E01006023,E01006024,E01006025,E01006026,E01006027,E01006028,E01006029,E01006030,E01006031,E01006032,E01006033,E01006034,E01006035,E01006036,E01006037,E01006038,E01006039,E01006040,E01006041,E01006042,E01006043,E01006044,E01006045,E01006046,E01006047,E01006048,E01006049,E01006050,E01006051,E01006052,E01006053,E01006054,E01006055,E01006056,E01006057,E01006058,E01006059,E01006060,E01006061,E01006062,E01006063,E01006064,E01006065,E01006066,E01006067,E01006068,E01006069,E01006070,E01006071,E01006072,E01006073,E01006074,E01006075,E01006076,E01006077,E01006078,E01006079,E01006080,E01006081,E01006082,E01006083,E01006084,E01006085,E01006086,E01006087,E01006088,E01006089,E01006090,E01006091,E01006092,E01006093,E01006094,E01006095,E01006096,E01006097,E01006098,E01006099,E01006100,E01006101,E01006102,E01006103,E01006104,E01006105,E01006106,E01006107,E01006108,E01006109,E01006110,E01006111,E01006112,E01006113,E01006114,E01006115,E01006116,E01006117,E01006118,E01006119,E01006120,E01006121,E01006122,E01006123,E01006124,E01006125,E01006126,E01006127,E01006128,E01006129,E01006130,E01006131,E01006132,E01006133,E01006134,E01006135,E01006136,E01006137,E01006138,E01006139,E01006140,E01006141,E01006142,E01006143,E01006144,E01006145,E01006146,E01006147,E01006148,E01006149,E01006150,E01006151,E01006152,E01006153,E01006154,E01006155,E01006156,E01006157,E01006158,E01006159,E01006160,E01006161,E01006162,E01006163,E01006164,E01006165,E01006166,E01006167,E01006168,E01006169,E01006170,E01006171,E01006172,E01006173,E01006174,E01006175,E01006176,E01006177,E01006178,E01006179,E01006180,E01006181,E01006182,E01006183,E01006184,E01006186,E01006187,E01006188,E01006189,E01006190,E01006191,E01006192,E01006193,E01006194,E01006195,E01006196,E01006197,E01006198,E01006199,E01006200,E01006201,E01006202,E01006203,E01006204,E01006205,E01006206,E01006207,E01006208,E01006209,E01006210,E01006211,E01006212,E01006213,E01006214,E01006215,E01006216,E01006217,E01006218,E01006219,E01006220,E01006221,E01006222,E01006223,E01006224,E01006225,E01006226,E01006227,E01006228,E01006229,E01006230,E01006231,E01006232,E01006233,E01006234,E01006235,E01006236,E01006237,E01006238,E01006239,E01006240,E01006241,E01006242,E01006243,E01006244,E01006245,E01006246,E01006247,E01006248,E01006249,E01006250,E01006251,E01006252,E01006253,E01006254,E01006255,E01006256,E01006257,E01006258,E01006259,E01006260,E01006261,E01006262,E01006263,E01006264,E01006265,E01006266,E01006267,E01006268,E01006269,E01006270,E01006271,E01006272,E01006273,E01006274,E01006275,E01006276,E01006277,E01006278,E01006279,E01006280,E01006281,E01006282,E01006283,E01006284,E01006285,E01006286,E01006287,E01006288,E01006289,E01006290,E01006291,E01006292,E01006293,E01006294,E01006295,E01006296,E01006297,E01006298,E01006299,E01006300,E01006301,E01006302,E01006303,E01006304,E01006305,E01006306,E01006307,E01006308,E01006309,E01006310,E01006311,E01006312,E01006313,E01006314,E01006315,E01006316,E01006317,E01006318&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_012.json.gz + content_sha256: d3821236bbdb8bfd3a97c066f949ff0f9b7e3a82bd0d462228f51518f57609f3 +- id: census_lsoa_tenure5a_bedrooms_013 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 013 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01006319,E01006320,E01006321,E01006322,E01006323,E01006324,E01006325,E01006326,E01006327,E01006328,E01006329,E01006330,E01006331,E01006332,E01006333,E01006334,E01006335,E01006336,E01006337,E01006338,E01006339,E01006340,E01006341,E01006342,E01006343,E01006344,E01006345,E01006346,E01006347,E01006348,E01006349,E01006350,E01006351,E01006352,E01006353,E01006354,E01006355,E01006356,E01006357,E01006358,E01006359,E01006360,E01006361,E01006362,E01006363,E01006364,E01006365,E01006366,E01006367,E01006368,E01006369,E01006370,E01006371,E01006372,E01006373,E01006374,E01006375,E01006376,E01006377,E01006378,E01006379,E01006380,E01006381,E01006382,E01006383,E01006384,E01006385,E01006386,E01006387,E01006388,E01006389,E01006390,E01006391,E01006392,E01006393,E01006394,E01006395,E01006396,E01006397,E01006398,E01006399,E01006400,E01006401,E01006402,E01006403,E01006404,E01006405,E01006406,E01006407,E01006408,E01006409,E01006410,E01006411,E01006412,E01006413,E01006414,E01006415,E01006416,E01006417,E01006418,E01006419,E01006420,E01006421,E01006422,E01006423,E01006424,E01006425,E01006426,E01006427,E01006428,E01006429,E01006430,E01006431,E01006432,E01006433,E01006434,E01006435,E01006436,E01006437,E01006438,E01006439,E01006440,E01006442,E01006443,E01006444,E01006445,E01006446,E01006447,E01006448,E01006449,E01006450,E01006451,E01006452,E01006453,E01006454,E01006455,E01006456,E01006457,E01006458,E01006459,E01006460,E01006461,E01006462,E01006464,E01006465,E01006466,E01006467,E01006470,E01006471,E01006472,E01006473,E01006474,E01006475,E01006476,E01006477,E01006478,E01006479,E01006480,E01006481,E01006482,E01006483,E01006484,E01006485,E01006486,E01006487,E01006488,E01006489,E01006490,E01006491,E01006492,E01006493,E01006494,E01006495,E01006496,E01006497,E01006498,E01006499,E01006500,E01006501,E01006502,E01006503,E01006504,E01006505,E01006506,E01006507,E01006508,E01006509,E01006510,E01006512,E01006514,E01006518,E01006519,E01006520,E01006521,E01006522,E01006523,E01006524,E01006525,E01006526,E01006527,E01006528,E01006529,E01006530,E01006531,E01006532,E01006533,E01006534,E01006535,E01006536,E01006537,E01006538,E01006539,E01006540,E01006541,E01006542,E01006543,E01006544,E01006545,E01006546,E01006547,E01006548,E01006549,E01006550,E01006551,E01006552,E01006553,E01006554,E01006556,E01006557,E01006558,E01006560,E01006562,E01006563,E01006564,E01006565,E01006566,E01006567,E01006568,E01006569,E01006570,E01006571,E01006572,E01006573,E01006574,E01006575,E01006576,E01006577,E01006578,E01006579,E01006580,E01006581,E01006582,E01006583,E01006584,E01006585,E01006586,E01006587,E01006588,E01006589,E01006590,E01006591,E01006592,E01006593,E01006594,E01006595,E01006596,E01006597,E01006598,E01006599,E01006600,E01006603,E01006604,E01006605,E01006606,E01006607,E01006608,E01006609,E01006610,E01006611,E01006612,E01006613,E01006614,E01006615,E01006616,E01006617,E01006618,E01006619,E01006620,E01006621,E01006622,E01006623,E01006624,E01006625,E01006626,E01006627,E01006628,E01006629,E01006630,E01006632,E01006633,E01006637,E01006638,E01006639,E01006640,E01006641,E01006642,E01006643,E01006644,E01006645,E01006646,E01006647,E01006648,E01006651,E01006652,E01006653,E01006654,E01006655,E01006656,E01006657,E01006658,E01006659,E01006660,E01006661,E01006662,E01006663,E01006664,E01006665,E01006666,E01006667,E01006668,E01006669,E01006670,E01006671,E01006672,E01006673,E01006674,E01006675,E01006676,E01006677,E01006678,E01006679,E01006680,E01006681,E01006682,E01006683,E01006684,E01006685,E01006686,E01006687,E01006688,E01006689,E01006690,E01006691,E01006692,E01006693,E01006694,E01006695,E01006696,E01006697,E01006698,E01006699,E01006700,E01006701,E01006702,E01006703,E01006705,E01006706,E01006707,E01006708,E01006709,E01006710,E01006711,E01006712,E01006713,E01006716,E01006717,E01006718,E01006719,E01006721,E01006722,E01006723,E01006724,E01006725,E01006726,E01006727,E01006728,E01006729,E01006730,E01006731,E01006732,E01006734,E01006735,E01006736,E01006737,E01006738,E01006739,E01006740,E01006741,E01006742,E01006743,E01006744,E01006745,E01006746,E01006747,E01006748,E01006751,E01006753,E01006754,E01006755,E01006756,E01006757,E01006758,E01006759,E01006760,E01006761,E01006762,E01006763,E01006764,E01006765,E01006766,E01006767,E01006768,E01006769,E01006770,E01006771,E01006772,E01006773,E01006774,E01006775,E01006776,E01006778,E01006779,E01006780,E01006781,E01006782,E01006783,E01006784,E01006785,E01006786,E01006787,E01006788,E01006790,E01006791,E01006792,E01006793,E01006794,E01006795,E01006796,E01006797,E01006798,E01006799,E01006800,E01006801,E01006802,E01006803,E01006804,E01006805,E01006806,E01006807,E01006808,E01006809,E01006810,E01006811,E01006812,E01006813,E01006814,E01006815,E01006816,E01006817,E01006818,E01006819,E01006820,E01006821,E01006822,E01006823,E01006824,E01006825,E01006826,E01006827,E01006828,E01006829,E01006830,E01006831,E01006832,E01006833,E01006834,E01006835,E01006836,E01006837,E01006838,E01006839,E01006840,E01006841,E01006842,E01006843,E01006844,E01006845,E01006846,E01006847,E01006848&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_013.json.gz + content_sha256: 9d09995d95ebde885e26ff7771c14d466e8b50785ea764cdd4b4fe4ccdb33d69 +- id: census_lsoa_tenure5a_bedrooms_014 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 014 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01006849,E01006850,E01006851,E01006852,E01006853,E01006854,E01006855,E01006856,E01006857,E01006858,E01006859,E01006860,E01006861,E01006862,E01006863,E01006864,E01006866,E01006867,E01006868,E01006869,E01006870,E01006871,E01006872,E01006873,E01006874,E01006875,E01006876,E01006877,E01006878,E01006879,E01006880,E01006881,E01006882,E01006883,E01006884,E01006885,E01006886,E01006887,E01006888,E01006889,E01006890,E01006891,E01006892,E01006893,E01006894,E01006895,E01006896,E01006897,E01006898,E01006899,E01006901,E01006903,E01006904,E01006905,E01006906,E01006907,E01006908,E01006909,E01006910,E01006911,E01006912,E01006913,E01006914,E01006915,E01006916,E01006917,E01006918,E01006919,E01006920,E01006921,E01006922,E01006923,E01006924,E01006925,E01006926,E01006927,E01006928,E01006929,E01006930,E01006931,E01006932,E01006933,E01006934,E01006935,E01006936,E01006937,E01006938,E01006939,E01006940,E01006941,E01006942,E01006943,E01006944,E01006945,E01006946,E01006947,E01006948,E01006949,E01006950,E01006951,E01006952,E01006953,E01006954,E01006955,E01006956,E01006957,E01006958,E01006959,E01006960,E01006961,E01006962,E01006963,E01006964,E01006965,E01006966,E01006967,E01006969,E01006970,E01006971,E01006972,E01006973,E01006974,E01006976,E01006977,E01006978,E01006979,E01006980,E01006981,E01006982,E01006983,E01006984,E01006985,E01006986,E01006987,E01006988,E01006989,E01006990,E01006991,E01006992,E01006993,E01006994,E01006995,E01006996,E01006997,E01006998,E01006999,E01007000,E01007001,E01007002,E01007003,E01007004,E01007005,E01007006,E01007007,E01007008,E01007009,E01007010,E01007011,E01007012,E01007013,E01007014,E01007016,E01007017,E01007019,E01007020,E01007021,E01007022,E01007023,E01007024,E01007025,E01007026,E01007027,E01007028,E01007029,E01007030,E01007031,E01007032,E01007033,E01007034,E01007035,E01007036,E01007037,E01007038,E01007039,E01007040,E01007041,E01007042,E01007043,E01007044,E01007045,E01007046,E01007047,E01007048,E01007049,E01007050,E01007051,E01007052,E01007053,E01007054,E01007055,E01007056,E01007057,E01007058,E01007059,E01007060,E01007061,E01007062,E01007063,E01007064,E01007065,E01007066,E01007067,E01007068,E01007069,E01007070,E01007071,E01007072,E01007073,E01007074,E01007075,E01007076,E01007077,E01007078,E01007079,E01007080,E01007081,E01007082,E01007083,E01007084,E01007085,E01007086,E01007087,E01007088,E01007089,E01007090,E01007091,E01007092,E01007095,E01007096,E01007097,E01007098,E01007099,E01007100,E01007101,E01007102,E01007103,E01007104,E01007105,E01007106,E01007107,E01007108,E01007109,E01007110,E01007111,E01007112,E01007113,E01007114,E01007115,E01007116,E01007117,E01007118,E01007119,E01007120,E01007121,E01007122,E01007123,E01007124,E01007125,E01007127,E01007128,E01007129,E01007130,E01007131,E01007133,E01007135,E01007136,E01007137,E01007138,E01007139,E01007141,E01007142,E01007143,E01007144,E01007145,E01007146,E01007147,E01007148,E01007149,E01007150,E01007151,E01007152,E01007153,E01007154,E01007155,E01007156,E01007157,E01007158,E01007159,E01007160,E01007161,E01007162,E01007163,E01007164,E01007165,E01007166,E01007167,E01007168,E01007169,E01007170,E01007171,E01007172,E01007173,E01007174,E01007175,E01007176,E01007177,E01007178,E01007179,E01007180,E01007181,E01007182,E01007183,E01007184,E01007185,E01007186,E01007187,E01007188,E01007189,E01007190,E01007191,E01007192,E01007193,E01007194,E01007195,E01007196,E01007197,E01007198,E01007199,E01007200,E01007201,E01007202,E01007203,E01007204,E01007205,E01007206,E01007207,E01007208,E01007209,E01007210,E01007211,E01007212,E01007213,E01007214,E01007215,E01007216,E01007217,E01007218,E01007219,E01007220,E01007221,E01007222,E01007223,E01007224,E01007225,E01007226,E01007227,E01007228,E01007229,E01007230,E01007231,E01007232,E01007233,E01007234,E01007235,E01007236,E01007237,E01007238,E01007239,E01007240,E01007241,E01007242,E01007243,E01007244,E01007245,E01007246,E01007247,E01007248,E01007249,E01007250,E01007251,E01007252,E01007253,E01007254,E01007255,E01007256,E01007257,E01007258,E01007259,E01007260,E01007261,E01007262,E01007263,E01007264,E01007265,E01007266,E01007267,E01007268,E01007269,E01007270,E01007271,E01007272,E01007273,E01007274,E01007275,E01007276,E01007277,E01007278,E01007279,E01007280,E01007281,E01007282,E01007283,E01007284,E01007285,E01007286,E01007287,E01007288,E01007289,E01007290,E01007291,E01007292,E01007293,E01007294,E01007295,E01007296,E01007297,E01007298,E01007299,E01007300,E01007301,E01007302,E01007303,E01007304,E01007305,E01007306,E01007307,E01007308,E01007309,E01007310,E01007311,E01007312,E01007313,E01007314,E01007315,E01007316,E01007317,E01007318,E01007319,E01007320,E01007321,E01007322,E01007323,E01007324,E01007325,E01007326,E01007327,E01007328,E01007329,E01007330,E01007331,E01007332,E01007333,E01007334,E01007335,E01007336,E01007337,E01007338,E01007339,E01007340,E01007341,E01007342,E01007343,E01007344,E01007345,E01007346,E01007347,E01007348,E01007349,E01007350,E01007351,E01007352,E01007353,E01007354,E01007355,E01007356,E01007357,E01007358,E01007359,E01007360,E01007361&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_014.json.gz + content_sha256: a2c2b270bb056313d216efe12af93fde3ba5c7fd547ebce7d31ced4cd45cca18 +- id: census_lsoa_tenure5a_bedrooms_015 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 015 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01007362,E01007363,E01007364,E01007365,E01007366,E01007367,E01007368,E01007369,E01007370,E01007371,E01007372,E01007373,E01007374,E01007375,E01007376,E01007377,E01007378,E01007379,E01007380,E01007381,E01007382,E01007383,E01007384,E01007385,E01007386,E01007387,E01007388,E01007389,E01007390,E01007391,E01007392,E01007393,E01007394,E01007395,E01007396,E01007397,E01007398,E01007399,E01007400,E01007402,E01007403,E01007404,E01007405,E01007406,E01007408,E01007409,E01007410,E01007411,E01007413,E01007414,E01007415,E01007416,E01007417,E01007418,E01007419,E01007420,E01007421,E01007422,E01007423,E01007424,E01007425,E01007426,E01007427,E01007428,E01007429,E01007430,E01007431,E01007432,E01007433,E01007434,E01007435,E01007436,E01007437,E01007438,E01007439,E01007440,E01007441,E01007442,E01007443,E01007444,E01007445,E01007446,E01007447,E01007448,E01007449,E01007450,E01007451,E01007452,E01007453,E01007455,E01007456,E01007457,E01007458,E01007459,E01007460,E01007461,E01007462,E01007463,E01007464,E01007465,E01007466,E01007467,E01007468,E01007469,E01007470,E01007471,E01007472,E01007473,E01007474,E01007475,E01007476,E01007477,E01007478,E01007479,E01007480,E01007482,E01007483,E01007484,E01007485,E01007486,E01007487,E01007488,E01007489,E01007490,E01007491,E01007492,E01007493,E01007494,E01007495,E01007496,E01007497,E01007498,E01007499,E01007500,E01007501,E01007503,E01007504,E01007505,E01007506,E01007507,E01007508,E01007509,E01007510,E01007511,E01007512,E01007513,E01007514,E01007515,E01007516,E01007517,E01007518,E01007519,E01007520,E01007521,E01007522,E01007523,E01007524,E01007525,E01007526,E01007528,E01007529,E01007530,E01007531,E01007532,E01007533,E01007534,E01007535,E01007536,E01007537,E01007538,E01007539,E01007540,E01007541,E01007542,E01007543,E01007544,E01007545,E01007546,E01007547,E01007548,E01007549,E01007550,E01007551,E01007552,E01007553,E01007554,E01007555,E01007556,E01007557,E01007558,E01007559,E01007560,E01007561,E01007562,E01007563,E01007564,E01007565,E01007566,E01007567,E01007568,E01007569,E01007570,E01007571,E01007572,E01007573,E01007574,E01007575,E01007576,E01007577,E01007578,E01007579,E01007580,E01007581,E01007582,E01007583,E01007584,E01007585,E01007586,E01007587,E01007588,E01007589,E01007590,E01007591,E01007592,E01007593,E01007594,E01007595,E01007596,E01007597,E01007598,E01007599,E01007600,E01007601,E01007602,E01007603,E01007604,E01007605,E01007606,E01007607,E01007609,E01007610,E01007611,E01007612,E01007613,E01007614,E01007615,E01007616,E01007617,E01007618,E01007619,E01007620,E01007621,E01007622,E01007623,E01007624,E01007625,E01007626,E01007627,E01007628,E01007629,E01007630,E01007631,E01007632,E01007633,E01007634,E01007635,E01007636,E01007637,E01007638,E01007639,E01007640,E01007641,E01007642,E01007643,E01007644,E01007646,E01007647,E01007649,E01007650,E01007651,E01007652,E01007653,E01007654,E01007655,E01007656,E01007657,E01007658,E01007659,E01007660,E01007661,E01007662,E01007663,E01007664,E01007666,E01007667,E01007668,E01007669,E01007670,E01007671,E01007672,E01007673,E01007674,E01007675,E01007676,E01007677,E01007678,E01007679,E01007680,E01007681,E01007682,E01007683,E01007684,E01007685,E01007686,E01007687,E01007688,E01007689,E01007690,E01007691,E01007692,E01007694,E01007695,E01007696,E01007697,E01007698,E01007699,E01007700,E01007701,E01007702,E01007703,E01007704,E01007705,E01007706,E01007707,E01007708,E01007709,E01007710,E01007711,E01007712,E01007713,E01007714,E01007715,E01007716,E01007717,E01007718,E01007719,E01007720,E01007721,E01007723,E01007724,E01007725,E01007726,E01007727,E01007728,E01007729,E01007730,E01007731,E01007732,E01007733,E01007734,E01007735,E01007736,E01007737,E01007738,E01007739,E01007740,E01007741,E01007742,E01007743,E01007744,E01007745,E01007746,E01007747,E01007748,E01007749,E01007750,E01007751,E01007752,E01007753,E01007754,E01007755,E01007756,E01007757,E01007758,E01007759,E01007760,E01007761,E01007762,E01007763,E01007764,E01007765,E01007766,E01007767,E01007768,E01007769,E01007770,E01007771,E01007772,E01007773,E01007774,E01007775,E01007776,E01007777,E01007778,E01007779,E01007780,E01007781,E01007782,E01007783,E01007784,E01007785,E01007786,E01007788,E01007789,E01007790,E01007791,E01007792,E01007793,E01007794,E01007795,E01007796,E01007797,E01007798,E01007799,E01007800,E01007801,E01007802,E01007803,E01007804,E01007805,E01007806,E01007807,E01007808,E01007809,E01007810,E01007811,E01007812,E01007815,E01007816,E01007817,E01007818,E01007819,E01007820,E01007821,E01007822,E01007823,E01007824,E01007825,E01007826,E01007827,E01007828,E01007829,E01007830,E01007831,E01007832,E01007833,E01007834,E01007835,E01007836,E01007838,E01007839,E01007841,E01007842,E01007843,E01007846,E01007847,E01007848,E01007849,E01007850,E01007851,E01007852,E01007853,E01007854,E01007855,E01007856,E01007857,E01007858,E01007861,E01007862,E01007863,E01007864,E01007865,E01007866,E01007867,E01007868,E01007869,E01007870,E01007871,E01007872,E01007873,E01007874,E01007875,E01007878,E01007879,E01007881,E01007882,E01007883,E01007885,E01007886,E01007887&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_015.json.gz + content_sha256: ff8597c95828e7376ef99d0726a528ccec1463ad5640314841c72f8f08a5ea0f +- id: census_lsoa_tenure5a_bedrooms_016 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 016 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01007888,E01007889,E01007890,E01007891,E01007892,E01007893,E01007894,E01007895,E01007896,E01007897,E01007898,E01007899,E01007900,E01007901,E01007902,E01007903,E01007904,E01007905,E01007906,E01007907,E01007908,E01007909,E01007910,E01007911,E01007912,E01007913,E01007914,E01007915,E01007916,E01007917,E01007918,E01007919,E01007920,E01007921,E01007922,E01007923,E01007924,E01007925,E01007926,E01007927,E01007928,E01007929,E01007930,E01007931,E01007932,E01007933,E01007934,E01007935,E01007936,E01007937,E01007938,E01007939,E01007940,E01007941,E01007942,E01007943,E01007945,E01007946,E01007947,E01007948,E01007949,E01007950,E01007951,E01007952,E01007953,E01007954,E01007955,E01007956,E01007957,E01007958,E01007959,E01007960,E01007961,E01007962,E01007963,E01007964,E01007965,E01007966,E01007967,E01007968,E01007969,E01007970,E01007971,E01007972,E01007973,E01007974,E01007975,E01007976,E01007977,E01007978,E01007979,E01007980,E01007981,E01007982,E01007983,E01007984,E01007985,E01007986,E01007987,E01007988,E01007989,E01007990,E01007991,E01007992,E01007993,E01007994,E01007995,E01007996,E01007997,E01007998,E01007999,E01008000,E01008001,E01008002,E01008003,E01008004,E01008005,E01008006,E01008007,E01008008,E01008009,E01008010,E01008011,E01008012,E01008013,E01008014,E01008015,E01008016,E01008017,E01008018,E01008019,E01008020,E01008021,E01008022,E01008023,E01008024,E01008025,E01008027,E01008028,E01008029,E01008030,E01008031,E01008032,E01008033,E01008034,E01008035,E01008036,E01008037,E01008038,E01008039,E01008040,E01008041,E01008042,E01008043,E01008044,E01008045,E01008046,E01008047,E01008048,E01008049,E01008050,E01008051,E01008052,E01008053,E01008054,E01008055,E01008056,E01008057,E01008058,E01008059,E01008060,E01008061,E01008062,E01008063,E01008064,E01008065,E01008066,E01008067,E01008068,E01008069,E01008072,E01008073,E01008074,E01008075,E01008076,E01008077,E01008078,E01008079,E01008080,E01008081,E01008082,E01008083,E01008084,E01008085,E01008086,E01008087,E01008088,E01008089,E01008090,E01008091,E01008092,E01008093,E01008094,E01008095,E01008096,E01008097,E01008098,E01008099,E01008100,E01008101,E01008102,E01008103,E01008104,E01008105,E01008107,E01008108,E01008109,E01008111,E01008112,E01008113,E01008114,E01008115,E01008116,E01008117,E01008118,E01008119,E01008122,E01008123,E01008124,E01008125,E01008126,E01008128,E01008129,E01008130,E01008131,E01008132,E01008133,E01008134,E01008135,E01008136,E01008137,E01008138,E01008139,E01008140,E01008141,E01008142,E01008143,E01008144,E01008145,E01008146,E01008147,E01008148,E01008149,E01008150,E01008151,E01008152,E01008153,E01008154,E01008155,E01008156,E01008157,E01008158,E01008159,E01008160,E01008161,E01008162,E01008163,E01008164,E01008165,E01008166,E01008167,E01008170,E01008172,E01008173,E01008174,E01008175,E01008176,E01008177,E01008178,E01008179,E01008180,E01008181,E01008182,E01008183,E01008184,E01008185,E01008186,E01008188,E01008189,E01008190,E01008191,E01008192,E01008193,E01008194,E01008195,E01008196,E01008197,E01008198,E01008199,E01008200,E01008201,E01008202,E01008203,E01008204,E01008205,E01008206,E01008207,E01008208,E01008209,E01008210,E01008211,E01008212,E01008213,E01008214,E01008215,E01008216,E01008217,E01008218,E01008219,E01008220,E01008221,E01008222,E01008223,E01008224,E01008225,E01008226,E01008227,E01008228,E01008229,E01008230,E01008231,E01008232,E01008233,E01008234,E01008235,E01008236,E01008237,E01008238,E01008239,E01008240,E01008241,E01008242,E01008243,E01008244,E01008245,E01008246,E01008247,E01008248,E01008249,E01008250,E01008251,E01008252,E01008253,E01008254,E01008255,E01008256,E01008257,E01008258,E01008259,E01008260,E01008261,E01008262,E01008263,E01008264,E01008265,E01008266,E01008267,E01008268,E01008269,E01008270,E01008271,E01008272,E01008273,E01008274,E01008275,E01008276,E01008277,E01008278,E01008279,E01008280,E01008281,E01008282,E01008283,E01008284,E01008285,E01008286,E01008287,E01008288,E01008289,E01008290,E01008291,E01008292,E01008293,E01008294,E01008295,E01008296,E01008297,E01008298,E01008299,E01008300,E01008301,E01008302,E01008303,E01008305,E01008306,E01008307,E01008309,E01008311,E01008312,E01008313,E01008314,E01008315,E01008316,E01008317,E01008318,E01008319,E01008320,E01008321,E01008322,E01008323,E01008324,E01008325,E01008326,E01008327,E01008328,E01008329,E01008330,E01008331,E01008333,E01008334,E01008335,E01008336,E01008337,E01008338,E01008339,E01008340,E01008341,E01008342,E01008343,E01008344,E01008345,E01008346,E01008347,E01008348,E01008349,E01008350,E01008351,E01008352,E01008353,E01008354,E01008355,E01008356,E01008357,E01008358,E01008359,E01008360,E01008361,E01008362,E01008363,E01008364,E01008365,E01008366,E01008367,E01008368,E01008369,E01008370,E01008371,E01008372,E01008373,E01008374,E01008375,E01008376,E01008377,E01008378,E01008379,E01008380,E01008381,E01008382,E01008383,E01008384,E01008385,E01008386,E01008387,E01008388,E01008389,E01008390,E01008391,E01008392,E01008396,E01008397,E01008398,E01008399,E01008400,E01008401,E01008402,E01008403,E01008404,E01008405,E01008406,E01008407&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_016.json.gz + content_sha256: 3593580efd7daa4b0443eaf636a9561a7cf930836b9649a086bc167ae3dd8359 +- id: census_lsoa_tenure5a_bedrooms_017 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 017 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01008409,E01008410,E01008411,E01008412,E01008413,E01008415,E01008416,E01008418,E01008419,E01008420,E01008421,E01008422,E01008423,E01008424,E01008425,E01008426,E01008427,E01008428,E01008429,E01008430,E01008431,E01008432,E01008433,E01008434,E01008435,E01008436,E01008437,E01008438,E01008439,E01008441,E01008442,E01008443,E01008444,E01008445,E01008446,E01008447,E01008448,E01008449,E01008450,E01008451,E01008452,E01008453,E01008455,E01008456,E01008457,E01008458,E01008459,E01008460,E01008461,E01008462,E01008463,E01008464,E01008465,E01008466,E01008467,E01008468,E01008469,E01008470,E01008471,E01008472,E01008473,E01008474,E01008475,E01008476,E01008477,E01008478,E01008479,E01008480,E01008481,E01008482,E01008483,E01008484,E01008485,E01008486,E01008487,E01008488,E01008489,E01008490,E01008491,E01008492,E01008493,E01008494,E01008495,E01008496,E01008497,E01008498,E01008499,E01008500,E01008502,E01008503,E01008504,E01008505,E01008506,E01008507,E01008508,E01008509,E01008510,E01008511,E01008512,E01008513,E01008514,E01008516,E01008517,E01008518,E01008519,E01008520,E01008521,E01008522,E01008523,E01008524,E01008525,E01008526,E01008527,E01008528,E01008529,E01008530,E01008531,E01008532,E01008533,E01008534,E01008535,E01008536,E01008537,E01008538,E01008539,E01008540,E01008541,E01008542,E01008543,E01008544,E01008545,E01008546,E01008547,E01008548,E01008549,E01008550,E01008551,E01008552,E01008553,E01008554,E01008555,E01008556,E01008557,E01008558,E01008559,E01008560,E01008561,E01008562,E01008563,E01008564,E01008567,E01008568,E01008569,E01008570,E01008571,E01008572,E01008573,E01008574,E01008575,E01008576,E01008577,E01008578,E01008579,E01008580,E01008581,E01008582,E01008583,E01008584,E01008585,E01008586,E01008587,E01008588,E01008589,E01008590,E01008591,E01008592,E01008593,E01008594,E01008595,E01008596,E01008597,E01008598,E01008599,E01008600,E01008601,E01008602,E01008603,E01008604,E01008605,E01008606,E01008607,E01008608,E01008609,E01008610,E01008611,E01008612,E01008613,E01008614,E01008615,E01008616,E01008617,E01008618,E01008619,E01008620,E01008621,E01008622,E01008624,E01008625,E01008627,E01008628,E01008629,E01008630,E01008631,E01008632,E01008633,E01008634,E01008635,E01008636,E01008637,E01008638,E01008639,E01008640,E01008641,E01008642,E01008643,E01008644,E01008645,E01008646,E01008647,E01008648,E01008649,E01008650,E01008651,E01008652,E01008653,E01008654,E01008655,E01008656,E01008657,E01008658,E01008659,E01008660,E01008661,E01008662,E01008663,E01008664,E01008665,E01008666,E01008667,E01008668,E01008669,E01008670,E01008671,E01008672,E01008673,E01008674,E01008675,E01008676,E01008677,E01008678,E01008679,E01008680,E01008681,E01008682,E01008683,E01008684,E01008685,E01008686,E01008687,E01008688,E01008689,E01008690,E01008691,E01008692,E01008693,E01008694,E01008695,E01008696,E01008697,E01008698,E01008699,E01008700,E01008701,E01008702,E01008703,E01008704,E01008705,E01008706,E01008707,E01008708,E01008709,E01008711,E01008712,E01008713,E01008714,E01008715,E01008716,E01008717,E01008718,E01008719,E01008720,E01008721,E01008722,E01008723,E01008724,E01008725,E01008726,E01008727,E01008728,E01008730,E01008732,E01008733,E01008734,E01008735,E01008736,E01008737,E01008738,E01008739,E01008740,E01008741,E01008742,E01008743,E01008744,E01008745,E01008746,E01008747,E01008748,E01008749,E01008750,E01008751,E01008752,E01008753,E01008754,E01008755,E01008756,E01008757,E01008758,E01008759,E01008760,E01008761,E01008762,E01008763,E01008764,E01008765,E01008766,E01008767,E01008768,E01008769,E01008770,E01008771,E01008772,E01008773,E01008774,E01008775,E01008776,E01008777,E01008778,E01008779,E01008780,E01008781,E01008782,E01008783,E01008784,E01008785,E01008786,E01008787,E01008788,E01008789,E01008790,E01008791,E01008792,E01008793,E01008795,E01008796,E01008799,E01008800,E01008801,E01008802,E01008803,E01008804,E01008805,E01008808,E01008809,E01008810,E01008811,E01008812,E01008813,E01008814,E01008817,E01008819,E01008821,E01008822,E01008823,E01008824,E01008825,E01008826,E01008827,E01008828,E01008829,E01008830,E01008831,E01008832,E01008833,E01008834,E01008835,E01008836,E01008837,E01008838,E01008839,E01008840,E01008841,E01008842,E01008843,E01008844,E01008845,E01008846,E01008847,E01008848,E01008849,E01008851,E01008852,E01008853,E01008854,E01008855,E01008856,E01008857,E01008858,E01008859,E01008860,E01008861,E01008862,E01008863,E01008864,E01008865,E01008866,E01008867,E01008868,E01008869,E01008870,E01008871,E01008872,E01008873,E01008874,E01008875,E01008876,E01008877,E01008878,E01008879,E01008880,E01008881,E01008882,E01008883,E01008884,E01008885,E01008886,E01008887,E01008888,E01008889,E01008890,E01008891,E01008892,E01008893,E01008894,E01008895,E01008896,E01008897,E01008898,E01008899,E01008901,E01008905,E01008906,E01008907,E01008909,E01008910,E01008911,E01008913,E01008915,E01008916,E01008917,E01008918,E01008919,E01008920,E01008921,E01008923,E01008924,E01008925,E01008927,E01008928,E01008929,E01008930,E01008931,E01008932,E01008933,E01008934,E01008935,E01008936,E01008937,E01008938,E01008939,E01008940&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_017.json.gz + content_sha256: 2735279c62a9cbaacd0fa27b5de7dbd010881e5e985f00689aadd05842c9e897 +- id: census_lsoa_tenure5a_bedrooms_018 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 018 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01008941,E01008942,E01008943,E01008944,E01008945,E01008946,E01008947,E01008948,E01008949,E01008950,E01008951,E01008952,E01008953,E01008954,E01008955,E01008956,E01008957,E01008958,E01008959,E01008960,E01008961,E01008962,E01008963,E01008964,E01008965,E01008966,E01008967,E01008968,E01008969,E01008970,E01008971,E01008972,E01008973,E01008974,E01008975,E01008976,E01008977,E01008978,E01008979,E01008980,E01008981,E01008982,E01008984,E01008986,E01008987,E01008988,E01008989,E01008990,E01008991,E01008992,E01008994,E01008995,E01008996,E01008997,E01008998,E01008999,E01009000,E01009001,E01009002,E01009003,E01009005,E01009006,E01009007,E01009008,E01009009,E01009011,E01009012,E01009013,E01009014,E01009015,E01009016,E01009017,E01009018,E01009019,E01009020,E01009021,E01009022,E01009023,E01009024,E01009025,E01009026,E01009028,E01009029,E01009030,E01009031,E01009032,E01009033,E01009034,E01009035,E01009036,E01009037,E01009039,E01009040,E01009041,E01009042,E01009043,E01009044,E01009045,E01009046,E01009047,E01009048,E01009049,E01009050,E01009051,E01009053,E01009054,E01009056,E01009057,E01009058,E01009059,E01009060,E01009061,E01009062,E01009064,E01009065,E01009066,E01009067,E01009068,E01009069,E01009070,E01009071,E01009072,E01009073,E01009074,E01009075,E01009077,E01009078,E01009079,E01009080,E01009081,E01009082,E01009083,E01009084,E01009085,E01009086,E01009087,E01009088,E01009089,E01009090,E01009091,E01009092,E01009093,E01009094,E01009095,E01009096,E01009097,E01009098,E01009099,E01009100,E01009101,E01009102,E01009103,E01009104,E01009105,E01009106,E01009107,E01009108,E01009109,E01009110,E01009111,E01009112,E01009113,E01009114,E01009115,E01009116,E01009117,E01009118,E01009119,E01009120,E01009122,E01009123,E01009124,E01009125,E01009126,E01009127,E01009128,E01009129,E01009130,E01009131,E01009132,E01009133,E01009134,E01009135,E01009136,E01009137,E01009138,E01009139,E01009140,E01009141,E01009143,E01009145,E01009146,E01009147,E01009151,E01009152,E01009153,E01009155,E01009157,E01009158,E01009159,E01009160,E01009161,E01009162,E01009164,E01009165,E01009166,E01009167,E01009168,E01009169,E01009170,E01009171,E01009172,E01009173,E01009174,E01009175,E01009176,E01009177,E01009178,E01009179,E01009182,E01009183,E01009184,E01009185,E01009186,E01009187,E01009188,E01009189,E01009192,E01009195,E01009197,E01009199,E01009200,E01009201,E01009202,E01009204,E01009205,E01009206,E01009207,E01009208,E01009209,E01009210,E01009211,E01009212,E01009213,E01009214,E01009215,E01009216,E01009217,E01009218,E01009219,E01009220,E01009221,E01009222,E01009223,E01009224,E01009225,E01009226,E01009227,E01009228,E01009229,E01009230,E01009231,E01009232,E01009233,E01009234,E01009235,E01009236,E01009237,E01009238,E01009239,E01009240,E01009241,E01009242,E01009243,E01009244,E01009245,E01009246,E01009247,E01009248,E01009249,E01009250,E01009251,E01009252,E01009253,E01009254,E01009255,E01009256,E01009257,E01009258,E01009259,E01009260,E01009261,E01009262,E01009263,E01009264,E01009265,E01009266,E01009267,E01009268,E01009269,E01009270,E01009271,E01009272,E01009273,E01009274,E01009275,E01009276,E01009278,E01009279,E01009280,E01009281,E01009282,E01009283,E01009284,E01009286,E01009288,E01009289,E01009290,E01009291,E01009292,E01009293,E01009294,E01009295,E01009296,E01009297,E01009298,E01009299,E01009300,E01009301,E01009302,E01009303,E01009304,E01009305,E01009306,E01009307,E01009308,E01009309,E01009310,E01009311,E01009312,E01009313,E01009314,E01009315,E01009316,E01009317,E01009318,E01009319,E01009320,E01009321,E01009322,E01009323,E01009324,E01009325,E01009326,E01009327,E01009328,E01009329,E01009331,E01009332,E01009333,E01009334,E01009335,E01009337,E01009338,E01009339,E01009341,E01009342,E01009343,E01009344,E01009345,E01009346,E01009347,E01009348,E01009349,E01009350,E01009351,E01009352,E01009353,E01009354,E01009355,E01009358,E01009359,E01009360,E01009361,E01009362,E01009363,E01009364,E01009365,E01009366,E01009367,E01009368,E01009371,E01009372,E01009373,E01009374,E01009375,E01009376,E01009377,E01009379,E01009380,E01009382,E01009383,E01009384,E01009385,E01009389,E01009390,E01009391,E01009392,E01009393,E01009394,E01009395,E01009396,E01009397,E01009399,E01009400,E01009401,E01009403,E01009404,E01009405,E01009406,E01009407,E01009408,E01009409,E01009410,E01009411,E01009412,E01009413,E01009414,E01009415,E01009416,E01009417,E01009418,E01009419,E01009420,E01009421,E01009422,E01009423,E01009424,E01009425,E01009426,E01009427,E01009428,E01009429,E01009430,E01009431,E01009432,E01009433,E01009434,E01009435,E01009436,E01009437,E01009438,E01009439,E01009440,E01009441,E01009442,E01009443,E01009444,E01009445,E01009446,E01009447,E01009448,E01009449,E01009450,E01009451,E01009452,E01009453,E01009454,E01009455,E01009456,E01009457,E01009458,E01009459,E01009460,E01009461,E01009462,E01009463,E01009464,E01009465,E01009466,E01009467,E01009468,E01009469,E01009470,E01009471,E01009472,E01009473,E01009474,E01009475,E01009476,E01009477,E01009478,E01009479,E01009480,E01009481,E01009482,E01009483,E01009484,E01009485,E01009486&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_018.json.gz + content_sha256: 6ab662cda6344f8b67014580148f51e5eb3e3c1b7638b9a2a8a0314940d3526d +- id: census_lsoa_tenure5a_bedrooms_019 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 019 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01009487,E01009488,E01009489,E01009490,E01009491,E01009492,E01009493,E01009494,E01009495,E01009496,E01009497,E01009498,E01009499,E01009500,E01009501,E01009502,E01009503,E01009504,E01009505,E01009506,E01009507,E01009508,E01009509,E01009510,E01009511,E01009512,E01009513,E01009514,E01009515,E01009516,E01009517,E01009518,E01009519,E01009520,E01009521,E01009522,E01009523,E01009524,E01009525,E01009526,E01009527,E01009528,E01009529,E01009530,E01009531,E01009532,E01009535,E01009536,E01009537,E01009538,E01009539,E01009540,E01009541,E01009542,E01009543,E01009544,E01009548,E01009549,E01009550,E01009552,E01009553,E01009554,E01009555,E01009556,E01009557,E01009558,E01009559,E01009560,E01009561,E01009562,E01009563,E01009564,E01009565,E01009566,E01009567,E01009568,E01009569,E01009570,E01009571,E01009573,E01009574,E01009575,E01009576,E01009577,E01009578,E01009579,E01009580,E01009581,E01009582,E01009583,E01009584,E01009585,E01009586,E01009587,E01009588,E01009589,E01009590,E01009591,E01009592,E01009593,E01009594,E01009596,E01009597,E01009598,E01009599,E01009604,E01009605,E01009606,E01009607,E01009608,E01009609,E01009610,E01009612,E01009613,E01009614,E01009615,E01009616,E01009617,E01009618,E01009620,E01009621,E01009622,E01009623,E01009624,E01009625,E01009626,E01009627,E01009628,E01009630,E01009631,E01009632,E01009633,E01009634,E01009636,E01009637,E01009638,E01009639,E01009640,E01009641,E01009643,E01009644,E01009645,E01009646,E01009647,E01009648,E01009649,E01009650,E01009651,E01009652,E01009653,E01009654,E01009655,E01009656,E01009657,E01009658,E01009659,E01009660,E01009661,E01009662,E01009663,E01009664,E01009665,E01009666,E01009667,E01009668,E01009669,E01009670,E01009671,E01009672,E01009674,E01009676,E01009679,E01009681,E01009682,E01009683,E01009684,E01009685,E01009686,E01009687,E01009688,E01009689,E01009690,E01009691,E01009692,E01009693,E01009694,E01009695,E01009696,E01009697,E01009698,E01009699,E01009700,E01009701,E01009702,E01009704,E01009705,E01009706,E01009707,E01009708,E01009709,E01009710,E01009711,E01009712,E01009713,E01009714,E01009715,E01009716,E01009717,E01009718,E01009719,E01009720,E01009721,E01009722,E01009723,E01009724,E01009725,E01009726,E01009727,E01009728,E01009729,E01009730,E01009731,E01009732,E01009733,E01009734,E01009735,E01009736,E01009737,E01009738,E01009739,E01009740,E01009741,E01009742,E01009743,E01009744,E01009745,E01009746,E01009747,E01009748,E01009749,E01009750,E01009751,E01009752,E01009753,E01009754,E01009756,E01009757,E01009758,E01009759,E01009760,E01009762,E01009763,E01009764,E01009765,E01009766,E01009767,E01009768,E01009769,E01009770,E01009771,E01009772,E01009773,E01009774,E01009775,E01009776,E01009777,E01009778,E01009779,E01009780,E01009781,E01009782,E01009783,E01009784,E01009785,E01009786,E01009787,E01009788,E01009789,E01009790,E01009791,E01009792,E01009793,E01009794,E01009795,E01009796,E01009797,E01009798,E01009799,E01009800,E01009801,E01009802,E01009803,E01009804,E01009805,E01009806,E01009807,E01009808,E01009809,E01009810,E01009811,E01009812,E01009813,E01009814,E01009815,E01009816,E01009817,E01009818,E01009819,E01009820,E01009821,E01009822,E01009823,E01009824,E01009825,E01009826,E01009827,E01009828,E01009829,E01009830,E01009831,E01009832,E01009833,E01009834,E01009835,E01009836,E01009837,E01009838,E01009839,E01009840,E01009841,E01009842,E01009843,E01009844,E01009845,E01009846,E01009847,E01009848,E01009849,E01009850,E01009851,E01009852,E01009853,E01009854,E01009855,E01009856,E01009857,E01009858,E01009859,E01009860,E01009861,E01009862,E01009863,E01009864,E01009865,E01009866,E01009867,E01009868,E01009869,E01009870,E01009871,E01009872,E01009873,E01009874,E01009875,E01009876,E01009877,E01009878,E01009879,E01009880,E01009882,E01009883,E01009884,E01009885,E01009886,E01009887,E01009888,E01009889,E01009890,E01009891,E01009892,E01009893,E01009894,E01009895,E01009896,E01009897,E01009898,E01009899,E01009900,E01009901,E01009902,E01009903,E01009904,E01009905,E01009906,E01009907,E01009908,E01009909,E01009910,E01009911,E01009912,E01009913,E01009914,E01009915,E01009916,E01009917,E01009918,E01009919,E01009920,E01009921,E01009922,E01009923,E01009924,E01009925,E01009926,E01009927,E01009928,E01009929,E01009930,E01009931,E01009932,E01009933,E01009934,E01009935,E01009936,E01009937,E01009938,E01009939,E01009940,E01009941,E01009942,E01009943,E01009944,E01009946,E01009947,E01009948,E01009949,E01009950,E01009951,E01009952,E01009953,E01009954,E01009955,E01009956,E01009957,E01009958,E01009959,E01009960,E01009961,E01009962,E01009963,E01009964,E01009965,E01009966,E01009967,E01009968,E01009969,E01009970,E01009971,E01009972,E01009973,E01009974,E01009975,E01009976,E01009977,E01009978,E01009979,E01009980,E01009981,E01009982,E01009983,E01009985,E01009986,E01009987,E01009988,E01009989,E01009991,E01009992,E01009993,E01009994,E01009995,E01009996,E01009997,E01009998,E01009999,E01010000,E01010001,E01010002,E01010003,E01010004,E01010005,E01010006,E01010007,E01010008,E01010009,E01010010,E01010011,E01010012,E01010013,E01010014,E01010015&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_019.json.gz + content_sha256: 991bfb82cfec76af908a8d96b87346722b5f82856a9bd9118a2fd9c03eacfe2d +- id: census_lsoa_tenure5a_bedrooms_020 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 020 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01010016,E01010017,E01010018,E01010019,E01010020,E01010021,E01010022,E01010023,E01010024,E01010025,E01010026,E01010027,E01010028,E01010029,E01010030,E01010031,E01010032,E01010033,E01010034,E01010035,E01010036,E01010037,E01010038,E01010039,E01010040,E01010041,E01010042,E01010043,E01010044,E01010045,E01010046,E01010047,E01010048,E01010049,E01010050,E01010051,E01010052,E01010053,E01010054,E01010055,E01010056,E01010057,E01010058,E01010059,E01010060,E01010061,E01010062,E01010064,E01010066,E01010067,E01010068,E01010069,E01010070,E01010071,E01010072,E01010073,E01010075,E01010076,E01010077,E01010078,E01010079,E01010080,E01010081,E01010082,E01010083,E01010084,E01010085,E01010086,E01010087,E01010088,E01010089,E01010090,E01010091,E01010092,E01010093,E01010094,E01010095,E01010096,E01010097,E01010098,E01010099,E01010100,E01010101,E01010102,E01010103,E01010104,E01010105,E01010106,E01010107,E01010108,E01010109,E01010110,E01010111,E01010112,E01010113,E01010114,E01010115,E01010116,E01010117,E01010118,E01010119,E01010120,E01010121,E01010122,E01010123,E01010124,E01010125,E01010126,E01010127,E01010128,E01010129,E01010130,E01010131,E01010132,E01010133,E01010134,E01010135,E01010136,E01010137,E01010138,E01010139,E01010140,E01010141,E01010142,E01010143,E01010144,E01010145,E01010146,E01010147,E01010148,E01010149,E01010150,E01010151,E01010152,E01010153,E01010154,E01010155,E01010156,E01010157,E01010158,E01010159,E01010160,E01010161,E01010162,E01010163,E01010164,E01010165,E01010166,E01010167,E01010168,E01010169,E01010170,E01010171,E01010172,E01010173,E01010174,E01010175,E01010176,E01010177,E01010178,E01010179,E01010180,E01010181,E01010183,E01010184,E01010185,E01010186,E01010187,E01010188,E01010189,E01010190,E01010191,E01010192,E01010193,E01010194,E01010195,E01010196,E01010197,E01010198,E01010199,E01010200,E01010201,E01010202,E01010203,E01010204,E01010205,E01010206,E01010207,E01010208,E01010209,E01010210,E01010211,E01010212,E01010213,E01010214,E01010215,E01010216,E01010217,E01010218,E01010219,E01010220,E01010221,E01010222,E01010223,E01010225,E01010226,E01010227,E01010228,E01010229,E01010230,E01010231,E01010232,E01010233,E01010234,E01010235,E01010236,E01010237,E01010238,E01010239,E01010240,E01010241,E01010242,E01010243,E01010244,E01010245,E01010246,E01010247,E01010248,E01010249,E01010250,E01010251,E01010252,E01010253,E01010254,E01010255,E01010256,E01010257,E01010258,E01010259,E01010260,E01010261,E01010262,E01010263,E01010264,E01010265,E01010266,E01010267,E01010268,E01010269,E01010270,E01010271,E01010272,E01010273,E01010274,E01010275,E01010277,E01010279,E01010282,E01010283,E01010284,E01010285,E01010286,E01010287,E01010288,E01010289,E01010290,E01010291,E01010292,E01010293,E01010294,E01010295,E01010296,E01010297,E01010298,E01010299,E01010300,E01010301,E01010302,E01010303,E01010304,E01010305,E01010306,E01010307,E01010308,E01010309,E01010310,E01010311,E01010312,E01010313,E01010314,E01010315,E01010316,E01010317,E01010318,E01010319,E01010320,E01010321,E01010322,E01010323,E01010324,E01010325,E01010326,E01010327,E01010328,E01010329,E01010330,E01010331,E01010332,E01010333,E01010334,E01010335,E01010336,E01010337,E01010338,E01010339,E01010340,E01010341,E01010342,E01010343,E01010344,E01010345,E01010346,E01010347,E01010348,E01010349,E01010350,E01010351,E01010352,E01010353,E01010354,E01010355,E01010356,E01010357,E01010358,E01010359,E01010360,E01010361,E01010362,E01010363,E01010364,E01010365,E01010366,E01010367,E01010368,E01010369,E01010370,E01010372,E01010373,E01010374,E01010375,E01010376,E01010377,E01010378,E01010379,E01010380,E01010381,E01010382,E01010383,E01010384,E01010385,E01010386,E01010387,E01010388,E01010389,E01010390,E01010391,E01010392,E01010393,E01010394,E01010395,E01010396,E01010397,E01010398,E01010399,E01010400,E01010401,E01010402,E01010403,E01010404,E01010405,E01010406,E01010407,E01010408,E01010409,E01010410,E01010411,E01010412,E01010413,E01010414,E01010415,E01010416,E01010417,E01010418,E01010419,E01010420,E01010421,E01010422,E01010423,E01010424,E01010425,E01010426,E01010427,E01010428,E01010429,E01010430,E01010431,E01010432,E01010433,E01010434,E01010436,E01010437,E01010438,E01010439,E01010440,E01010441,E01010442,E01010443,E01010444,E01010445,E01010446,E01010447,E01010448,E01010449,E01010450,E01010451,E01010452,E01010454,E01010455,E01010456,E01010457,E01010458,E01010459,E01010460,E01010461,E01010462,E01010463,E01010464,E01010465,E01010466,E01010467,E01010468,E01010469,E01010470,E01010471,E01010472,E01010473,E01010474,E01010475,E01010476,E01010477,E01010478,E01010479,E01010480,E01010481,E01010482,E01010483,E01010484,E01010485,E01010486,E01010487,E01010488,E01010489,E01010490,E01010491,E01010492,E01010493,E01010494,E01010495,E01010496,E01010497,E01010498,E01010499,E01010500,E01010501,E01010502,E01010503,E01010504,E01010505,E01010506,E01010507,E01010508,E01010509,E01010510,E01010511,E01010512,E01010513,E01010514,E01010515,E01010516,E01010517,E01010518,E01010519,E01010520,E01010522,E01010523,E01010524,E01010525,E01010526,E01010527,E01010528&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_020.json.gz + content_sha256: b387b523cd9cc997de28791e403126500c110014b46efc3b1fc2a75aaedff642 +- id: census_lsoa_tenure5a_bedrooms_021 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 021 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01010529,E01010530,E01010531,E01010532,E01010533,E01010534,E01010535,E01010536,E01010537,E01010538,E01010539,E01010540,E01010541,E01010542,E01010543,E01010544,E01010545,E01010546,E01010547,E01010548,E01010549,E01010550,E01010551,E01010552,E01010553,E01010554,E01010555,E01010556,E01010557,E01010558,E01010559,E01010560,E01010561,E01010562,E01010563,E01010564,E01010565,E01010566,E01010567,E01010568,E01010569,E01010570,E01010571,E01010572,E01010573,E01010574,E01010575,E01010576,E01010577,E01010578,E01010579,E01010580,E01010581,E01010582,E01010583,E01010584,E01010585,E01010586,E01010587,E01010588,E01010589,E01010590,E01010591,E01010592,E01010593,E01010594,E01010595,E01010596,E01010597,E01010598,E01010599,E01010600,E01010601,E01010602,E01010603,E01010604,E01010605,E01010606,E01010607,E01010608,E01010609,E01010610,E01010611,E01010612,E01010613,E01010614,E01010615,E01010616,E01010617,E01010618,E01010619,E01010620,E01010621,E01010622,E01010623,E01010624,E01010625,E01010626,E01010627,E01010628,E01010629,E01010630,E01010631,E01010632,E01010633,E01010634,E01010635,E01010636,E01010637,E01010638,E01010639,E01010640,E01010641,E01010642,E01010643,E01010644,E01010645,E01010646,E01010647,E01010648,E01010649,E01010650,E01010651,E01010652,E01010653,E01010654,E01010655,E01010656,E01010657,E01010658,E01010659,E01010662,E01010663,E01010664,E01010665,E01010666,E01010667,E01010668,E01010669,E01010670,E01010671,E01010672,E01010673,E01010674,E01010675,E01010676,E01010678,E01010679,E01010680,E01010681,E01010682,E01010683,E01010684,E01010685,E01010686,E01010687,E01010688,E01010689,E01010690,E01010691,E01010692,E01010693,E01010694,E01010695,E01010696,E01010697,E01010698,E01010699,E01010700,E01010701,E01010702,E01010703,E01010704,E01010705,E01010706,E01010707,E01010708,E01010709,E01010710,E01010711,E01010712,E01010713,E01010714,E01010715,E01010716,E01010717,E01010718,E01010719,E01010720,E01010721,E01010722,E01010723,E01010724,E01010725,E01010726,E01010727,E01010728,E01010729,E01010730,E01010731,E01010732,E01010733,E01010734,E01010735,E01010736,E01010737,E01010738,E01010739,E01010740,E01010741,E01010742,E01010743,E01010744,E01010745,E01010746,E01010747,E01010748,E01010749,E01010750,E01010751,E01010752,E01010753,E01010754,E01010755,E01010756,E01010758,E01010759,E01010760,E01010761,E01010762,E01010763,E01010764,E01010765,E01010766,E01010767,E01010768,E01010769,E01010770,E01010771,E01010772,E01010773,E01010774,E01010775,E01010776,E01010777,E01010778,E01010779,E01010781,E01010782,E01010783,E01010784,E01010785,E01010786,E01010787,E01010788,E01010789,E01010790,E01010791,E01010792,E01010793,E01010794,E01010795,E01010796,E01010797,E01010798,E01010799,E01010800,E01010801,E01010802,E01010803,E01010804,E01010805,E01010806,E01010807,E01010808,E01010809,E01010810,E01010811,E01010812,E01010813,E01010814,E01010815,E01010816,E01010817,E01010818,E01010819,E01010820,E01010821,E01010822,E01010824,E01010825,E01010826,E01010827,E01010828,E01010829,E01010830,E01010831,E01010832,E01010833,E01010834,E01010836,E01010837,E01010838,E01010839,E01010840,E01010841,E01010842,E01010843,E01010845,E01010846,E01010847,E01010848,E01010849,E01010850,E01010851,E01010852,E01010853,E01010854,E01010855,E01010856,E01010857,E01010858,E01010859,E01010860,E01010861,E01010862,E01010863,E01010864,E01010865,E01010866,E01010867,E01010868,E01010869,E01010870,E01010871,E01010872,E01010873,E01010874,E01010875,E01010876,E01010877,E01010878,E01010879,E01010880,E01010881,E01010882,E01010883,E01010884,E01010885,E01010886,E01010887,E01010888,E01010889,E01010890,E01010891,E01010892,E01010893,E01010894,E01010895,E01010896,E01010897,E01010898,E01010899,E01010900,E01010901,E01010902,E01010903,E01010904,E01010905,E01010906,E01010907,E01010908,E01010909,E01010910,E01010911,E01010912,E01010913,E01010914,E01010915,E01010916,E01010917,E01010918,E01010919,E01010920,E01010921,E01010922,E01010923,E01010924,E01010925,E01010926,E01010927,E01010928,E01010929,E01010930,E01010931,E01010932,E01010933,E01010934,E01010935,E01010936,E01010937,E01010938,E01010939,E01010940,E01010941,E01010942,E01010943,E01010944,E01010945,E01010946,E01010947,E01010948,E01010949,E01010950,E01010951,E01010952,E01010953,E01010954,E01010955,E01010956,E01010957,E01010958,E01010959,E01010960,E01010961,E01010964,E01010965,E01010966,E01010967,E01010968,E01010969,E01010970,E01010971,E01010972,E01010973,E01010974,E01010975,E01010976,E01010977,E01010978,E01010979,E01010980,E01010981,E01010982,E01010983,E01010984,E01010985,E01010986,E01010987,E01010988,E01010989,E01010990,E01010991,E01010992,E01010993,E01010995,E01010996,E01010997,E01010998,E01010999,E01011000,E01011001,E01011002,E01011003,E01011004,E01011005,E01011006,E01011007,E01011008,E01011009,E01011010,E01011011,E01011012,E01011013,E01011014,E01011015,E01011016,E01011017,E01011018,E01011019,E01011020,E01011021,E01011022,E01011023,E01011024,E01011025,E01011026,E01011027,E01011028,E01011029,E01011030,E01011031,E01011032,E01011033,E01011034,E01011035,E01011036,E01011037,E01011038,E01011039&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_021.json.gz + content_sha256: 3637e46ec114e299a839f91b0862f412e352fcf6e80e3f7fb2e57462be3e0310 +- id: census_lsoa_tenure5a_bedrooms_022 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 022 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01011040,E01011041,E01011042,E01011043,E01011044,E01011045,E01011046,E01011047,E01011048,E01011049,E01011050,E01011051,E01011052,E01011053,E01011054,E01011055,E01011056,E01011057,E01011058,E01011059,E01011060,E01011061,E01011062,E01011063,E01011064,E01011065,E01011066,E01011067,E01011068,E01011069,E01011070,E01011071,E01011072,E01011073,E01011074,E01011075,E01011076,E01011077,E01011078,E01011079,E01011080,E01011081,E01011082,E01011083,E01011084,E01011085,E01011086,E01011087,E01011088,E01011089,E01011090,E01011091,E01011092,E01011093,E01011094,E01011095,E01011096,E01011097,E01011098,E01011099,E01011100,E01011101,E01011102,E01011103,E01011104,E01011105,E01011106,E01011107,E01011108,E01011109,E01011110,E01011111,E01011112,E01011113,E01011114,E01011115,E01011116,E01011117,E01011118,E01011119,E01011120,E01011121,E01011122,E01011123,E01011124,E01011125,E01011126,E01011127,E01011128,E01011129,E01011130,E01011132,E01011133,E01011134,E01011136,E01011137,E01011138,E01011139,E01011140,E01011141,E01011142,E01011143,E01011144,E01011145,E01011146,E01011147,E01011148,E01011149,E01011150,E01011151,E01011152,E01011153,E01011154,E01011155,E01011156,E01011157,E01011158,E01011159,E01011160,E01011161,E01011162,E01011163,E01011164,E01011165,E01011166,E01011167,E01011168,E01011169,E01011170,E01011173,E01011174,E01011175,E01011176,E01011177,E01011178,E01011179,E01011180,E01011181,E01011182,E01011183,E01011184,E01011185,E01011186,E01011187,E01011188,E01011189,E01011190,E01011191,E01011192,E01011193,E01011194,E01011195,E01011196,E01011197,E01011198,E01011199,E01011200,E01011201,E01011202,E01011203,E01011204,E01011205,E01011206,E01011207,E01011208,E01011210,E01011211,E01011212,E01011213,E01011214,E01011215,E01011216,E01011217,E01011218,E01011219,E01011220,E01011221,E01011222,E01011223,E01011224,E01011225,E01011226,E01011227,E01011228,E01011229,E01011230,E01011231,E01011232,E01011233,E01011234,E01011235,E01011236,E01011237,E01011238,E01011239,E01011240,E01011241,E01011242,E01011243,E01011244,E01011245,E01011246,E01011247,E01011248,E01011249,E01011250,E01011251,E01011252,E01011253,E01011254,E01011255,E01011256,E01011257,E01011258,E01011259,E01011260,E01011261,E01011262,E01011263,E01011264,E01011265,E01011266,E01011267,E01011268,E01011269,E01011270,E01011271,E01011272,E01011273,E01011274,E01011275,E01011276,E01011277,E01011278,E01011279,E01011280,E01011281,E01011282,E01011283,E01011284,E01011286,E01011287,E01011292,E01011293,E01011294,E01011295,E01011296,E01011297,E01011298,E01011299,E01011300,E01011301,E01011302,E01011303,E01011304,E01011305,E01011306,E01011307,E01011308,E01011309,E01011310,E01011311,E01011312,E01011313,E01011314,E01011315,E01011316,E01011317,E01011318,E01011319,E01011320,E01011321,E01011322,E01011323,E01011324,E01011325,E01011326,E01011327,E01011328,E01011329,E01011330,E01011331,E01011332,E01011333,E01011334,E01011335,E01011336,E01011337,E01011338,E01011339,E01011340,E01011341,E01011342,E01011343,E01011344,E01011345,E01011346,E01011347,E01011348,E01011349,E01011350,E01011351,E01011352,E01011353,E01011354,E01011355,E01011356,E01011357,E01011358,E01011359,E01011360,E01011361,E01011362,E01011363,E01011364,E01011366,E01011368,E01011369,E01011370,E01011371,E01011372,E01011373,E01011374,E01011375,E01011376,E01011377,E01011378,E01011379,E01011380,E01011381,E01011382,E01011383,E01011384,E01011385,E01011386,E01011388,E01011390,E01011391,E01011392,E01011393,E01011394,E01011395,E01011396,E01011397,E01011398,E01011399,E01011400,E01011401,E01011402,E01011403,E01011404,E01011405,E01011406,E01011407,E01011408,E01011409,E01011410,E01011411,E01011412,E01011413,E01011414,E01011415,E01011416,E01011417,E01011418,E01011419,E01011420,E01011421,E01011422,E01011423,E01011424,E01011425,E01011426,E01011427,E01011428,E01011429,E01011430,E01011431,E01011432,E01011433,E01011434,E01011435,E01011440,E01011441,E01011442,E01011443,E01011444,E01011445,E01011446,E01011447,E01011448,E01011449,E01011450,E01011451,E01011452,E01011453,E01011454,E01011455,E01011456,E01011457,E01011458,E01011459,E01011460,E01011461,E01011462,E01011463,E01011464,E01011465,E01011466,E01011467,E01011468,E01011469,E01011470,E01011471,E01011472,E01011473,E01011474,E01011475,E01011476,E01011477,E01011478,E01011479,E01011480,E01011481,E01011482,E01011483,E01011484,E01011485,E01011488,E01011489,E01011491,E01011492,E01011493,E01011494,E01011496,E01011497,E01011500,E01011502,E01011504,E01011505,E01011506,E01011507,E01011508,E01011509,E01011510,E01011511,E01011512,E01011513,E01011514,E01011515,E01011516,E01011517,E01011518,E01011519,E01011520,E01011521,E01011522,E01011523,E01011524,E01011525,E01011526,E01011527,E01011528,E01011529,E01011530,E01011531,E01011532,E01011533,E01011534,E01011535,E01011536,E01011537,E01011538,E01011540,E01011541,E01011545,E01011546,E01011547,E01011548,E01011549,E01011550,E01011551,E01011552,E01011553,E01011554,E01011555,E01011556,E01011557,E01011558,E01011559,E01011560,E01011561,E01011562,E01011563,E01011564,E01011565,E01011566,E01011567,E01011568,E01011569&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_022.json.gz + content_sha256: 8fc48dd1a975c7afdc6c0136da9be4ce29438d6fcabe9e845d120841126630b8 +- id: census_lsoa_tenure5a_bedrooms_023 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 023 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01011570,E01011571,E01011572,E01011573,E01011574,E01011576,E01011578,E01011579,E01011580,E01011581,E01011582,E01011583,E01011584,E01011585,E01011586,E01011587,E01011588,E01011589,E01011590,E01011591,E01011592,E01011593,E01011594,E01011595,E01011596,E01011597,E01011598,E01011599,E01011600,E01011601,E01011602,E01011603,E01011604,E01011605,E01011606,E01011607,E01011608,E01011609,E01011610,E01011611,E01011612,E01011613,E01011614,E01011615,E01011616,E01011617,E01011618,E01011620,E01011621,E01011622,E01011623,E01011624,E01011625,E01011626,E01011627,E01011628,E01011629,E01011630,E01011632,E01011633,E01011634,E01011635,E01011636,E01011637,E01011638,E01011639,E01011641,E01011642,E01011643,E01011644,E01011645,E01011646,E01011647,E01011648,E01011649,E01011650,E01011651,E01011652,E01011653,E01011654,E01011655,E01011656,E01011657,E01011658,E01011659,E01011660,E01011661,E01011662,E01011663,E01011664,E01011665,E01011666,E01011667,E01011668,E01011669,E01011670,E01011671,E01011673,E01011677,E01011678,E01011681,E01011684,E01011685,E01011686,E01011687,E01011688,E01011689,E01011690,E01011691,E01011692,E01011693,E01011694,E01011695,E01011696,E01011697,E01011698,E01011699,E01011700,E01011701,E01011702,E01011703,E01011704,E01011705,E01011706,E01011707,E01011708,E01011709,E01011710,E01011711,E01011712,E01011713,E01011714,E01011715,E01011716,E01011717,E01011718,E01011719,E01011720,E01011721,E01011722,E01011723,E01011724,E01011725,E01011726,E01011727,E01011728,E01011729,E01011730,E01011731,E01011732,E01011733,E01011734,E01011735,E01011736,E01011737,E01011738,E01011739,E01011740,E01011741,E01011742,E01011743,E01011744,E01011745,E01011746,E01011747,E01011748,E01011750,E01011751,E01011752,E01011753,E01011754,E01011755,E01011756,E01011757,E01011758,E01011759,E01011761,E01011762,E01011763,E01011764,E01011765,E01011766,E01011767,E01011768,E01011770,E01011771,E01011772,E01011773,E01011774,E01011776,E01011777,E01011778,E01011779,E01011780,E01011781,E01011782,E01011783,E01011784,E01011785,E01011786,E01011787,E01011788,E01011789,E01011790,E01011791,E01011792,E01011793,E01011794,E01011795,E01011796,E01011797,E01011798,E01011799,E01011800,E01011801,E01011802,E01011803,E01011804,E01011805,E01011806,E01011807,E01011808,E01011809,E01011810,E01011811,E01011812,E01011813,E01011814,E01011815,E01011816,E01011817,E01011818,E01011819,E01011820,E01011821,E01011822,E01011823,E01011824,E01011825,E01011826,E01011827,E01011828,E01011829,E01011830,E01011831,E01011832,E01011833,E01011834,E01011835,E01011836,E01011837,E01011838,E01011839,E01011840,E01011841,E01011842,E01011843,E01011844,E01011845,E01011846,E01011847,E01011848,E01011849,E01011850,E01011851,E01011852,E01011853,E01011854,E01011855,E01011856,E01011857,E01011858,E01011859,E01011860,E01011861,E01011862,E01011863,E01011864,E01011865,E01011866,E01011867,E01011868,E01011869,E01011870,E01011871,E01011872,E01011873,E01011874,E01011875,E01011876,E01011877,E01011878,E01011879,E01011880,E01011881,E01011882,E01011883,E01011884,E01011885,E01011886,E01011887,E01011888,E01011889,E01011890,E01011891,E01011892,E01011893,E01011894,E01011895,E01011896,E01011897,E01011898,E01011899,E01011900,E01011901,E01011902,E01011903,E01011904,E01011905,E01011906,E01011907,E01011908,E01011909,E01011910,E01011911,E01011912,E01011913,E01011914,E01011915,E01011917,E01011918,E01011919,E01011920,E01011921,E01011922,E01011923,E01011924,E01011925,E01011926,E01011927,E01011928,E01011929,E01011930,E01011931,E01011932,E01011933,E01011934,E01011935,E01011936,E01011937,E01011938,E01011939,E01011940,E01011941,E01011942,E01011943,E01011944,E01011945,E01011946,E01011947,E01011948,E01011949,E01011950,E01011951,E01011952,E01011953,E01011954,E01011955,E01011957,E01011959,E01011960,E01011961,E01011962,E01011963,E01011964,E01011965,E01011966,E01011967,E01011968,E01011969,E01011970,E01011971,E01011974,E01011975,E01011977,E01011978,E01011979,E01011980,E01011981,E01011982,E01011983,E01011984,E01011985,E01011986,E01011987,E01011988,E01011989,E01011990,E01011991,E01011992,E01011993,E01011994,E01011995,E01011996,E01011997,E01011998,E01011999,E01012000,E01012001,E01012002,E01012003,E01012005,E01012007,E01012008,E01012009,E01012010,E01012011,E01012012,E01012013,E01012014,E01012015,E01012016,E01012017,E01012018,E01012019,E01012020,E01012021,E01012022,E01012023,E01012024,E01012025,E01012026,E01012027,E01012030,E01012031,E01012032,E01012033,E01012034,E01012035,E01012036,E01012037,E01012038,E01012039,E01012040,E01012041,E01012042,E01012043,E01012044,E01012045,E01012046,E01012047,E01012048,E01012049,E01012050,E01012051,E01012052,E01012053,E01012054,E01012055,E01012056,E01012057,E01012058,E01012059,E01012060,E01012061,E01012062,E01012063,E01012065,E01012066,E01012067,E01012068,E01012071,E01012073,E01012074,E01012075,E01012076,E01012077,E01012078,E01012079,E01012080,E01012081,E01012082,E01012083,E01012084,E01012085,E01012088,E01012089,E01012090,E01012091,E01012092,E01012093,E01012095,E01012096,E01012097,E01012098,E01012099,E01012100,E01012101,E01012102,E01012105&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_023.json.gz + content_sha256: 7e28d330b422adf7961aee0fc36b52cb1d9fcc12f6f033c071c901a0a8e2dcf3 +- id: census_lsoa_tenure5a_bedrooms_024 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 024 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01012106,E01012107,E01012108,E01012109,E01012110,E01012111,E01012112,E01012113,E01012114,E01012117,E01012118,E01012119,E01012120,E01012121,E01012124,E01012127,E01012128,E01012129,E01012130,E01012131,E01012132,E01012134,E01012136,E01012137,E01012138,E01012139,E01012140,E01012141,E01012142,E01012143,E01012144,E01012145,E01012146,E01012147,E01012148,E01012149,E01012150,E01012151,E01012152,E01012154,E01012155,E01012156,E01012157,E01012158,E01012159,E01012160,E01012161,E01012162,E01012163,E01012164,E01012165,E01012167,E01012168,E01012170,E01012171,E01012172,E01012173,E01012174,E01012175,E01012176,E01012177,E01012178,E01012179,E01012180,E01012181,E01012182,E01012183,E01012184,E01012185,E01012186,E01012187,E01012188,E01012189,E01012190,E01012191,E01012192,E01012193,E01012194,E01012195,E01012196,E01012197,E01012198,E01012199,E01012200,E01012201,E01012202,E01012203,E01012204,E01012205,E01012206,E01012207,E01012208,E01012209,E01012210,E01012211,E01012212,E01012213,E01012214,E01012215,E01012216,E01012217,E01012218,E01012219,E01012223,E01012224,E01012225,E01012226,E01012227,E01012228,E01012230,E01012231,E01012232,E01012233,E01012234,E01012235,E01012236,E01012237,E01012238,E01012239,E01012240,E01012241,E01012242,E01012243,E01012244,E01012245,E01012246,E01012247,E01012248,E01012249,E01012250,E01012251,E01012252,E01012253,E01012254,E01012255,E01012256,E01012257,E01012258,E01012259,E01012260,E01012261,E01012262,E01012267,E01012268,E01012269,E01012270,E01012271,E01012272,E01012273,E01012274,E01012275,E01012276,E01012277,E01012278,E01012279,E01012280,E01012281,E01012282,E01012283,E01012284,E01012285,E01012286,E01012287,E01012288,E01012289,E01012290,E01012291,E01012292,E01012294,E01012295,E01012296,E01012297,E01012298,E01012299,E01012300,E01012301,E01012302,E01012303,E01012304,E01012305,E01012306,E01012307,E01012309,E01012310,E01012311,E01012312,E01012313,E01012314,E01012315,E01012317,E01012318,E01012319,E01012323,E01012324,E01012325,E01012326,E01012327,E01012328,E01012329,E01012330,E01012331,E01012332,E01012333,E01012334,E01012335,E01012336,E01012337,E01012338,E01012339,E01012340,E01012341,E01012342,E01012344,E01012345,E01012346,E01012347,E01012348,E01012349,E01012350,E01012351,E01012352,E01012353,E01012354,E01012355,E01012356,E01012357,E01012358,E01012359,E01012360,E01012361,E01012362,E01012363,E01012364,E01012365,E01012366,E01012367,E01012368,E01012369,E01012370,E01012371,E01012372,E01012373,E01012374,E01012375,E01012376,E01012377,E01012378,E01012379,E01012380,E01012381,E01012382,E01012383,E01012384,E01012385,E01012386,E01012387,E01012388,E01012389,E01012390,E01012391,E01012392,E01012393,E01012394,E01012395,E01012396,E01012397,E01012398,E01012399,E01012400,E01012401,E01012402,E01012403,E01012404,E01012405,E01012406,E01012407,E01012408,E01012409,E01012410,E01012411,E01012412,E01012413,E01012414,E01012415,E01012416,E01012417,E01012418,E01012419,E01012420,E01012421,E01012422,E01012423,E01012424,E01012425,E01012426,E01012427,E01012429,E01012430,E01012431,E01012432,E01012433,E01012434,E01012435,E01012436,E01012437,E01012438,E01012439,E01012440,E01012441,E01012442,E01012443,E01012444,E01012445,E01012446,E01012447,E01012448,E01012449,E01012450,E01012451,E01012452,E01012453,E01012454,E01012455,E01012457,E01012458,E01012459,E01012460,E01012461,E01012462,E01012463,E01012464,E01012465,E01012466,E01012467,E01012468,E01012469,E01012470,E01012471,E01012472,E01012473,E01012474,E01012475,E01012476,E01012477,E01012478,E01012479,E01012480,E01012481,E01012482,E01012483,E01012485,E01012486,E01012487,E01012488,E01012489,E01012490,E01012491,E01012492,E01012493,E01012494,E01012495,E01012496,E01012497,E01012498,E01012499,E01012500,E01012501,E01012502,E01012503,E01012505,E01012506,E01012507,E01012508,E01012509,E01012510,E01012511,E01012512,E01012513,E01012514,E01012515,E01012516,E01012517,E01012518,E01012519,E01012520,E01012521,E01012522,E01012523,E01012524,E01012525,E01012526,E01012527,E01012528,E01012529,E01012530,E01012531,E01012532,E01012533,E01012534,E01012535,E01012536,E01012537,E01012538,E01012539,E01012540,E01012541,E01012542,E01012543,E01012544,E01012545,E01012546,E01012547,E01012548,E01012549,E01012550,E01012551,E01012552,E01012553,E01012554,E01012555,E01012556,E01012557,E01012558,E01012559,E01012560,E01012561,E01012562,E01012563,E01012564,E01012566,E01012569,E01012570,E01012571,E01012572,E01012573,E01012574,E01012575,E01012576,E01012577,E01012578,E01012579,E01012580,E01012581,E01012582,E01012583,E01012584,E01012585,E01012586,E01012587,E01012588,E01012589,E01012590,E01012591,E01012592,E01012593,E01012594,E01012595,E01012596,E01012597,E01012598,E01012599,E01012600,E01012601,E01012602,E01012603,E01012604,E01012605,E01012606,E01012607,E01012608,E01012609,E01012610,E01012611,E01012612,E01012613,E01012614,E01012615,E01012616,E01012617,E01012618,E01012619,E01012620,E01012621,E01012622,E01012623,E01012624,E01012625,E01012628,E01012629,E01012630,E01012631,E01012632,E01012633,E01012634,E01012635,E01012636,E01012637,E01012638,E01012639,E01012640&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_024.json.gz + content_sha256: da3732c1ecbaa0c915735de41d5fbeb2986a7ec2d6a1f2ca3afc4266d8e66058 +- id: census_lsoa_tenure5a_bedrooms_025 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 025 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01012641,E01012642,E01012643,E01012644,E01012645,E01012646,E01012647,E01012648,E01012649,E01012650,E01012651,E01012652,E01012653,E01012654,E01012655,E01012656,E01012657,E01012658,E01012659,E01012660,E01012661,E01012662,E01012663,E01012664,E01012665,E01012666,E01012667,E01012668,E01012669,E01012670,E01012671,E01012672,E01012673,E01012674,E01012675,E01012676,E01012677,E01012678,E01012679,E01012680,E01012681,E01012682,E01012683,E01012684,E01012685,E01012686,E01012687,E01012688,E01012689,E01012690,E01012691,E01012692,E01012693,E01012694,E01012695,E01012696,E01012697,E01012698,E01012699,E01012700,E01012701,E01012702,E01012703,E01012704,E01012705,E01012706,E01012707,E01012708,E01012709,E01012710,E01012711,E01012712,E01012713,E01012714,E01012715,E01012716,E01012717,E01012718,E01012719,E01012720,E01012721,E01012722,E01012723,E01012724,E01012725,E01012726,E01012727,E01012728,E01012729,E01012730,E01012731,E01012732,E01012733,E01012734,E01012735,E01012736,E01012737,E01012738,E01012739,E01012740,E01012741,E01012742,E01012743,E01012744,E01012745,E01012746,E01012747,E01012748,E01012749,E01012750,E01012751,E01012752,E01012753,E01012754,E01012755,E01012756,E01012757,E01012758,E01012759,E01012760,E01012761,E01012762,E01012763,E01012764,E01012765,E01012766,E01012767,E01012768,E01012769,E01012770,E01012771,E01012772,E01012773,E01012774,E01012775,E01012776,E01012777,E01012778,E01012779,E01012780,E01012781,E01012782,E01012783,E01012784,E01012785,E01012786,E01012787,E01012788,E01012789,E01012790,E01012791,E01012792,E01012793,E01012794,E01012795,E01012796,E01012797,E01012798,E01012799,E01012800,E01012801,E01012802,E01012803,E01012804,E01012805,E01012806,E01012807,E01012808,E01012809,E01012810,E01012811,E01012812,E01012813,E01012814,E01012815,E01012816,E01012817,E01012818,E01012819,E01012820,E01012821,E01012822,E01012824,E01012825,E01012827,E01012828,E01012829,E01012830,E01012832,E01012833,E01012834,E01012835,E01012836,E01012837,E01012838,E01012839,E01012840,E01012841,E01012842,E01012843,E01012844,E01012845,E01012846,E01012847,E01012848,E01012849,E01012850,E01012852,E01012854,E01012855,E01012856,E01012857,E01012858,E01012859,E01012860,E01012862,E01012863,E01012864,E01012865,E01012866,E01012867,E01012868,E01012869,E01012870,E01012871,E01012872,E01012873,E01012874,E01012875,E01012876,E01012877,E01012878,E01012879,E01012880,E01012881,E01012882,E01012883,E01012884,E01012886,E01012887,E01012888,E01012889,E01012891,E01012892,E01012893,E01012894,E01012896,E01012898,E01012899,E01012900,E01012901,E01012902,E01012903,E01012904,E01012905,E01012906,E01012907,E01012908,E01012909,E01012910,E01012911,E01012912,E01012913,E01012914,E01012915,E01012916,E01012917,E01012918,E01012919,E01012921,E01012922,E01012923,E01012924,E01012925,E01012926,E01012927,E01012928,E01012929,E01012930,E01012931,E01012932,E01012933,E01012934,E01012935,E01012936,E01012937,E01012939,E01012940,E01012941,E01012942,E01012943,E01012944,E01012945,E01012946,E01012947,E01012948,E01012949,E01012950,E01012951,E01012952,E01012953,E01012954,E01012955,E01012956,E01012957,E01012958,E01012959,E01012960,E01012961,E01012962,E01012963,E01012964,E01012966,E01012967,E01012968,E01012969,E01012970,E01012972,E01012974,E01012975,E01012976,E01012978,E01012979,E01012980,E01012981,E01012982,E01012983,E01012984,E01012985,E01012986,E01012987,E01012988,E01012989,E01012990,E01012991,E01012992,E01012993,E01012994,E01012995,E01012996,E01012997,E01012998,E01012999,E01013000,E01013001,E01013002,E01013003,E01013004,E01013005,E01013006,E01013007,E01013008,E01013009,E01013010,E01013011,E01013012,E01013013,E01013014,E01013015,E01013016,E01013017,E01013018,E01013019,E01013020,E01013021,E01013022,E01013023,E01013024,E01013025,E01013026,E01013027,E01013028,E01013029,E01013030,E01013031,E01013032,E01013033,E01013034,E01013035,E01013036,E01013037,E01013038,E01013039,E01013040,E01013041,E01013042,E01013043,E01013044,E01013045,E01013046,E01013047,E01013048,E01013049,E01013050,E01013051,E01013053,E01013054,E01013055,E01013056,E01013057,E01013058,E01013059,E01013060,E01013061,E01013062,E01013063,E01013064,E01013065,E01013066,E01013067,E01013068,E01013069,E01013070,E01013071,E01013072,E01013073,E01013074,E01013075,E01013076,E01013077,E01013078,E01013079,E01013080,E01013081,E01013082,E01013083,E01013084,E01013085,E01013086,E01013087,E01013088,E01013089,E01013090,E01013091,E01013092,E01013093,E01013094,E01013095,E01013096,E01013097,E01013098,E01013099,E01013100,E01013101,E01013102,E01013103,E01013104,E01013105,E01013106,E01013107,E01013108,E01013110,E01013111,E01013112,E01013113,E01013114,E01013115,E01013116,E01013117,E01013118,E01013119,E01013120,E01013121,E01013122,E01013123,E01013124,E01013125,E01013126,E01013127,E01013128,E01013129,E01013130,E01013131,E01013132,E01013133,E01013134,E01013135,E01013136,E01013138,E01013140,E01013141,E01013142,E01013143,E01013144,E01013145,E01013146,E01013147,E01013148,E01013149,E01013150,E01013151,E01013152,E01013153,E01013154,E01013155,E01013156,E01013157,E01013158,E01013159,E01013160&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_025.json.gz + content_sha256: f6958c55f0bad4b336e3ee602e95a6f813a22e166131886e60f4c4532dbf3bce +- id: census_lsoa_tenure5a_bedrooms_026 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 026 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01013161,E01013162,E01013163,E01013164,E01013165,E01013167,E01013170,E01013171,E01013172,E01013173,E01013174,E01013175,E01013176,E01013177,E01013178,E01013179,E01013180,E01013181,E01013182,E01013183,E01013184,E01013185,E01013186,E01013187,E01013188,E01013189,E01013191,E01013192,E01013193,E01013194,E01013195,E01013196,E01013197,E01013198,E01013199,E01013200,E01013201,E01013202,E01013203,E01013204,E01013205,E01013206,E01013207,E01013208,E01013209,E01013210,E01013211,E01013212,E01013213,E01013214,E01013215,E01013216,E01013217,E01013218,E01013219,E01013220,E01013221,E01013222,E01013224,E01013225,E01013226,E01013227,E01013228,E01013229,E01013231,E01013233,E01013234,E01013236,E01013237,E01013238,E01013239,E01013241,E01013242,E01013243,E01013244,E01013245,E01013246,E01013247,E01013248,E01013249,E01013250,E01013251,E01013252,E01013253,E01013254,E01013255,E01013256,E01013257,E01013258,E01013259,E01013260,E01013261,E01013262,E01013263,E01013264,E01013265,E01013266,E01013267,E01013268,E01013269,E01013270,E01013271,E01013272,E01013273,E01013274,E01013275,E01013276,E01013277,E01013278,E01013279,E01013280,E01013281,E01013282,E01013283,E01013284,E01013285,E01013286,E01013287,E01013288,E01013289,E01013290,E01013291,E01013292,E01013293,E01013294,E01013295,E01013296,E01013297,E01013298,E01013299,E01013300,E01013301,E01013302,E01013303,E01013304,E01013305,E01013306,E01013307,E01013308,E01013309,E01013310,E01013311,E01013312,E01013313,E01013314,E01013315,E01013316,E01013317,E01013318,E01013319,E01013320,E01013321,E01013322,E01013323,E01013324,E01013325,E01013326,E01013327,E01013328,E01013329,E01013330,E01013331,E01013332,E01013333,E01013334,E01013335,E01013336,E01013337,E01013338,E01013339,E01013340,E01013341,E01013342,E01013343,E01013344,E01013345,E01013346,E01013347,E01013348,E01013349,E01013350,E01013351,E01013352,E01013353,E01013354,E01013355,E01013356,E01013357,E01013358,E01013359,E01013360,E01013361,E01013362,E01013363,E01013364,E01013366,E01013368,E01013369,E01013370,E01013371,E01013372,E01013373,E01013374,E01013375,E01013376,E01013377,E01013378,E01013379,E01013380,E01013381,E01013382,E01013383,E01013384,E01013385,E01013386,E01013387,E01013388,E01013389,E01013390,E01013391,E01013392,E01013393,E01013394,E01013395,E01013396,E01013397,E01013398,E01013399,E01013400,E01013401,E01013402,E01013403,E01013404,E01013405,E01013406,E01013407,E01013408,E01013409,E01013410,E01013411,E01013412,E01013413,E01013414,E01013416,E01013417,E01013418,E01013419,E01013420,E01013421,E01013422,E01013423,E01013424,E01013425,E01013426,E01013427,E01013428,E01013429,E01013430,E01013431,E01013432,E01013433,E01013434,E01013435,E01013436,E01013437,E01013438,E01013439,E01013440,E01013441,E01013442,E01013443,E01013444,E01013445,E01013446,E01013447,E01013448,E01013449,E01013450,E01013451,E01013452,E01013453,E01013454,E01013455,E01013456,E01013457,E01013458,E01013459,E01013460,E01013461,E01013462,E01013463,E01013464,E01013465,E01013466,E01013467,E01013468,E01013469,E01013470,E01013471,E01013472,E01013473,E01013474,E01013475,E01013476,E01013477,E01013480,E01013481,E01013483,E01013484,E01013485,E01013486,E01013487,E01013488,E01013489,E01013490,E01013491,E01013492,E01013493,E01013494,E01013495,E01013496,E01013497,E01013498,E01013499,E01013500,E01013501,E01013502,E01013503,E01013504,E01013505,E01013506,E01013507,E01013508,E01013509,E01013510,E01013511,E01013512,E01013513,E01013514,E01013515,E01013517,E01013518,E01013519,E01013520,E01013521,E01013522,E01013523,E01013524,E01013525,E01013526,E01013527,E01013528,E01013529,E01013530,E01013531,E01013532,E01013533,E01013534,E01013535,E01013536,E01013537,E01013538,E01013539,E01013540,E01013541,E01013542,E01013543,E01013544,E01013545,E01013546,E01013547,E01013548,E01013549,E01013550,E01013551,E01013552,E01013553,E01013554,E01013555,E01013556,E01013557,E01013558,E01013559,E01013560,E01013561,E01013562,E01013563,E01013564,E01013565,E01013566,E01013567,E01013568,E01013569,E01013570,E01013571,E01013572,E01013573,E01013574,E01013575,E01013576,E01013577,E01013578,E01013579,E01013580,E01013581,E01013582,E01013583,E01013584,E01013585,E01013586,E01013587,E01013588,E01013589,E01013590,E01013591,E01013592,E01013593,E01013594,E01013595,E01013596,E01013597,E01013598,E01013599,E01013600,E01013601,E01013602,E01013603,E01013604,E01013605,E01013606,E01013608,E01013609,E01013610,E01013611,E01013612,E01013613,E01013614,E01013615,E01013616,E01013617,E01013618,E01013619,E01013620,E01013621,E01013622,E01013623,E01013624,E01013625,E01013626,E01013627,E01013628,E01013629,E01013630,E01013631,E01013632,E01013633,E01013634,E01013635,E01013636,E01013637,E01013638,E01013639,E01013640,E01013641,E01013642,E01013643,E01013648,E01013649,E01013650,E01013651,E01013652,E01013653,E01013654,E01013655,E01013656,E01013657,E01013658,E01013659,E01013660,E01013661,E01013662,E01013663,E01013664,E01013665,E01013666,E01013667,E01013668,E01013669,E01013670,E01013671,E01013672,E01013673,E01013674,E01013675,E01013676,E01013677,E01013678,E01013679,E01013680,E01013681&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_026.json.gz + content_sha256: 45c57944d2375289f8bdc5cede4578974725dc05589dba58eba2042ff9fd98d8 +- id: census_lsoa_tenure5a_bedrooms_027 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 027 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01013682,E01013683,E01013684,E01013685,E01013686,E01013687,E01013688,E01013689,E01013690,E01013691,E01013692,E01013693,E01013694,E01013696,E01013698,E01013699,E01013700,E01013702,E01013703,E01013704,E01013705,E01013706,E01013707,E01013709,E01013710,E01013711,E01013712,E01013713,E01013714,E01013715,E01013717,E01013718,E01013719,E01013720,E01013721,E01013722,E01013723,E01013725,E01013726,E01013727,E01013728,E01013729,E01013730,E01013731,E01013732,E01013733,E01013734,E01013735,E01013736,E01013737,E01013738,E01013739,E01013740,E01013741,E01013742,E01013743,E01013744,E01013745,E01013746,E01013747,E01013748,E01013749,E01013750,E01013751,E01013752,E01013753,E01013754,E01013755,E01013756,E01013757,E01013758,E01013759,E01013760,E01013761,E01013762,E01013763,E01013764,E01013765,E01013766,E01013767,E01013768,E01013770,E01013771,E01013772,E01013773,E01013774,E01013776,E01013777,E01013779,E01013780,E01013781,E01013782,E01013783,E01013784,E01013785,E01013786,E01013787,E01013788,E01013789,E01013790,E01013791,E01013792,E01013793,E01013794,E01013795,E01013796,E01013797,E01013798,E01013799,E01013800,E01013803,E01013804,E01013805,E01013806,E01013807,E01013808,E01013809,E01013810,E01013811,E01013812,E01013813,E01013814,E01013815,E01013816,E01013817,E01013818,E01013819,E01013820,E01013821,E01013822,E01013823,E01013824,E01013825,E01013826,E01013827,E01013828,E01013829,E01013830,E01013831,E01013832,E01013833,E01013834,E01013835,E01013836,E01013837,E01013838,E01013839,E01013840,E01013841,E01013842,E01013843,E01013844,E01013845,E01013846,E01013847,E01013849,E01013850,E01013851,E01013852,E01013853,E01013854,E01013855,E01013856,E01013858,E01013859,E01013860,E01013861,E01013862,E01013863,E01013864,E01013865,E01013866,E01013867,E01013868,E01013870,E01013871,E01013872,E01013873,E01013874,E01013875,E01013876,E01013877,E01013878,E01013879,E01013880,E01013881,E01013882,E01013883,E01013884,E01013885,E01013886,E01013887,E01013888,E01013889,E01013890,E01013891,E01013892,E01013893,E01013894,E01013895,E01013896,E01013897,E01013898,E01013899,E01013900,E01013901,E01013902,E01013903,E01013904,E01013905,E01013906,E01013907,E01013908,E01013909,E01013910,E01013911,E01013912,E01013913,E01013914,E01013915,E01013916,E01013917,E01013918,E01013919,E01013920,E01013921,E01013928,E01013929,E01013930,E01013931,E01013932,E01013933,E01013934,E01013935,E01013936,E01013937,E01013938,E01013939,E01013940,E01013941,E01013942,E01013943,E01013944,E01013945,E01013947,E01013948,E01013953,E01013954,E01013957,E01013958,E01013959,E01013960,E01013961,E01013962,E01013963,E01013964,E01013965,E01013966,E01013967,E01013968,E01013969,E01013970,E01013971,E01013972,E01013977,E01013978,E01013979,E01013980,E01013981,E01013982,E01013983,E01013984,E01013985,E01013986,E01013987,E01013988,E01013989,E01013990,E01013991,E01013992,E01013993,E01013994,E01013995,E01013996,E01013997,E01013998,E01013999,E01014000,E01014001,E01014002,E01014003,E01014004,E01014005,E01014006,E01014008,E01014009,E01014010,E01014011,E01014012,E01014013,E01014014,E01014015,E01014016,E01014017,E01014018,E01014019,E01014020,E01014021,E01014022,E01014023,E01014024,E01014025,E01014026,E01014027,E01014028,E01014029,E01014030,E01014031,E01014032,E01014033,E01014034,E01014035,E01014036,E01014037,E01014038,E01014039,E01014040,E01014041,E01014042,E01014043,E01014044,E01014045,E01014046,E01014047,E01014048,E01014049,E01014050,E01014051,E01014052,E01014053,E01014054,E01014055,E01014056,E01014057,E01014058,E01014059,E01014060,E01014061,E01014062,E01014063,E01014064,E01014065,E01014066,E01014067,E01014068,E01014069,E01014070,E01014071,E01014072,E01014073,E01014074,E01014075,E01014076,E01014077,E01014078,E01014079,E01014080,E01014081,E01014082,E01014083,E01014084,E01014085,E01014086,E01014087,E01014088,E01014089,E01014090,E01014091,E01014092,E01014093,E01014094,E01014095,E01014096,E01014097,E01014098,E01014099,E01014100,E01014101,E01014102,E01014103,E01014104,E01014105,E01014106,E01014107,E01014108,E01014109,E01014110,E01014111,E01014112,E01014113,E01014114,E01014115,E01014116,E01014117,E01014118,E01014119,E01014120,E01014121,E01014122,E01014123,E01014124,E01014125,E01014126,E01014127,E01014128,E01014129,E01014130,E01014131,E01014132,E01014133,E01014134,E01014135,E01014136,E01014137,E01014138,E01014139,E01014140,E01014142,E01014143,E01014144,E01014145,E01014148,E01014149,E01014150,E01014151,E01014152,E01014153,E01014154,E01014155,E01014157,E01014158,E01014159,E01014160,E01014161,E01014162,E01014163,E01014164,E01014165,E01014166,E01014167,E01014168,E01014169,E01014170,E01014171,E01014172,E01014173,E01014174,E01014175,E01014176,E01014177,E01014178,E01014179,E01014180,E01014181,E01014182,E01014183,E01014184,E01014185,E01014186,E01014187,E01014188,E01014189,E01014190,E01014191,E01014192,E01014193,E01014194,E01014195,E01014196,E01014197,E01014198,E01014199,E01014200,E01014201,E01014202,E01014203,E01014204,E01014205,E01014206,E01014207,E01014208,E01014209,E01014210,E01014211,E01014212,E01014213,E01014214,E01014215,E01014216,E01014217&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_027.json.gz + content_sha256: 424bd7326a6d94ecd9f66de11e20fa771a1286e48c234c0268377fc58071e59f +- id: census_lsoa_tenure5a_bedrooms_028 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 028 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01014218,E01014219,E01014220,E01014221,E01014222,E01014223,E01014224,E01014225,E01014226,E01014228,E01014229,E01014230,E01014233,E01014234,E01014235,E01014237,E01014238,E01014239,E01014240,E01014241,E01014242,E01014243,E01014244,E01014245,E01014246,E01014247,E01014248,E01014249,E01014250,E01014252,E01014253,E01014254,E01014255,E01014256,E01014257,E01014258,E01014259,E01014260,E01014261,E01014262,E01014263,E01014264,E01014265,E01014266,E01014267,E01014268,E01014269,E01014270,E01014271,E01014272,E01014273,E01014274,E01014275,E01014276,E01014277,E01014278,E01014279,E01014280,E01014281,E01014282,E01014283,E01014284,E01014285,E01014286,E01014287,E01014288,E01014289,E01014290,E01014291,E01014292,E01014293,E01014294,E01014295,E01014296,E01014297,E01014298,E01014299,E01014300,E01014301,E01014302,E01014303,E01014304,E01014305,E01014306,E01014307,E01014308,E01014309,E01014310,E01014311,E01014312,E01014313,E01014314,E01014315,E01014316,E01014317,E01014318,E01014319,E01014320,E01014321,E01014322,E01014323,E01014324,E01014325,E01014326,E01014327,E01014328,E01014329,E01014330,E01014331,E01014332,E01014333,E01014334,E01014335,E01014336,E01014337,E01014338,E01014339,E01014340,E01014341,E01014342,E01014343,E01014344,E01014345,E01014346,E01014347,E01014348,E01014349,E01014350,E01014351,E01014352,E01014353,E01014354,E01014355,E01014356,E01014357,E01014358,E01014359,E01014360,E01014361,E01014362,E01014363,E01014364,E01014365,E01014366,E01014367,E01014368,E01014369,E01014370,E01014371,E01014372,E01014373,E01014374,E01014375,E01014376,E01014377,E01014380,E01014381,E01014382,E01014383,E01014384,E01014385,E01014386,E01014387,E01014388,E01014389,E01014390,E01014391,E01014392,E01014393,E01014394,E01014395,E01014396,E01014397,E01014398,E01014399,E01014400,E01014401,E01014403,E01014404,E01014405,E01014407,E01014409,E01014410,E01014411,E01014412,E01014413,E01014414,E01014415,E01014416,E01014417,E01014418,E01014419,E01014420,E01014421,E01014422,E01014423,E01014424,E01014425,E01014426,E01014427,E01014428,E01014429,E01014430,E01014431,E01014432,E01014433,E01014434,E01014435,E01014436,E01014437,E01014438,E01014439,E01014440,E01014441,E01014442,E01014443,E01014444,E01014445,E01014446,E01014447,E01014448,E01014449,E01014450,E01014451,E01014452,E01014453,E01014454,E01014455,E01014456,E01014457,E01014458,E01014459,E01014460,E01014461,E01014462,E01014463,E01014464,E01014465,E01014466,E01014467,E01014468,E01014469,E01014470,E01014471,E01014472,E01014473,E01014474,E01014476,E01014477,E01014478,E01014479,E01014480,E01014481,E01014482,E01014483,E01014484,E01014485,E01014486,E01014487,E01014488,E01014489,E01014491,E01014492,E01014493,E01014494,E01014495,E01014496,E01014497,E01014498,E01014499,E01014500,E01014501,E01014502,E01014504,E01014505,E01014506,E01014507,E01014508,E01014509,E01014510,E01014511,E01014512,E01014513,E01014514,E01014515,E01014516,E01014517,E01014518,E01014519,E01014520,E01014521,E01014522,E01014523,E01014524,E01014525,E01014526,E01014527,E01014528,E01014529,E01014530,E01014531,E01014532,E01014533,E01014534,E01014535,E01014536,E01014537,E01014538,E01014539,E01014544,E01014545,E01014546,E01014547,E01014548,E01014549,E01014550,E01014551,E01014552,E01014553,E01014554,E01014555,E01014556,E01014557,E01014558,E01014559,E01014560,E01014561,E01014562,E01014563,E01014564,E01014565,E01014566,E01014567,E01014568,E01014569,E01014570,E01014571,E01014572,E01014573,E01014574,E01014575,E01014576,E01014577,E01014578,E01014579,E01014580,E01014581,E01014582,E01014583,E01014584,E01014585,E01014586,E01014587,E01014588,E01014589,E01014590,E01014591,E01014592,E01014593,E01014594,E01014595,E01014596,E01014597,E01014598,E01014599,E01014600,E01014601,E01014602,E01014603,E01014605,E01014607,E01014608,E01014609,E01014610,E01014611,E01014612,E01014613,E01014614,E01014615,E01014616,E01014617,E01014618,E01014619,E01014620,E01014621,E01014622,E01014623,E01014624,E01014626,E01014627,E01014629,E01014630,E01014631,E01014632,E01014633,E01014634,E01014635,E01014636,E01014637,E01014638,E01014639,E01014640,E01014641,E01014642,E01014643,E01014644,E01014645,E01014646,E01014647,E01014648,E01014649,E01014650,E01014651,E01014653,E01014654,E01014655,E01014658,E01014659,E01014660,E01014661,E01014662,E01014663,E01014664,E01014665,E01014666,E01014667,E01014668,E01014669,E01014670,E01014671,E01014672,E01014673,E01014674,E01014675,E01014676,E01014677,E01014678,E01014679,E01014680,E01014681,E01014682,E01014683,E01014684,E01014685,E01014686,E01014687,E01014688,E01014689,E01014690,E01014691,E01014692,E01014693,E01014694,E01014695,E01014696,E01014698,E01014699,E01014700,E01014701,E01014702,E01014703,E01014704,E01014705,E01014706,E01014707,E01014708,E01014709,E01014710,E01014711,E01014712,E01014713,E01014714,E01014715,E01014716,E01014717,E01014718,E01014719,E01014720,E01014721,E01014722,E01014723,E01014725,E01014726,E01014727,E01014729,E01014730,E01014731,E01014732,E01014733,E01014734,E01014735,E01014736,E01014737,E01014738,E01014739,E01014740,E01014741,E01014742,E01014744,E01014745&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_028.json.gz + content_sha256: bc064e5a9155549f19b9102719aac41dd3e2a121dff9a89a0c0bd88fb082ebc7 +- id: census_lsoa_tenure5a_bedrooms_029 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 029 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01014746,E01014747,E01014748,E01014749,E01014750,E01014751,E01014752,E01014753,E01014754,E01014755,E01014756,E01014757,E01014758,E01014759,E01014760,E01014761,E01014762,E01014763,E01014764,E01014765,E01014766,E01014767,E01014770,E01014771,E01014772,E01014773,E01014774,E01014775,E01014776,E01014777,E01014778,E01014779,E01014780,E01014781,E01014782,E01014783,E01014784,E01014785,E01014786,E01014787,E01014789,E01014790,E01014792,E01014793,E01014794,E01014795,E01014796,E01014797,E01014798,E01014799,E01014800,E01014801,E01014802,E01014803,E01014804,E01014805,E01014806,E01014807,E01014808,E01014809,E01014811,E01014812,E01014813,E01014815,E01014816,E01014817,E01014818,E01014819,E01014820,E01014821,E01014822,E01014823,E01014824,E01014825,E01014826,E01014827,E01014828,E01014829,E01014830,E01014831,E01014832,E01014833,E01014834,E01014835,E01014836,E01014838,E01014839,E01014840,E01014841,E01014842,E01014843,E01014844,E01014845,E01014846,E01014847,E01014848,E01014849,E01014850,E01014851,E01014852,E01014853,E01014854,E01014855,E01014856,E01014857,E01014858,E01014859,E01014860,E01014861,E01014862,E01014863,E01014864,E01014865,E01014866,E01014867,E01014869,E01014870,E01014871,E01014872,E01014873,E01014874,E01014875,E01014876,E01014877,E01014878,E01014879,E01014880,E01014881,E01014882,E01014883,E01014884,E01014885,E01014886,E01014887,E01014888,E01014889,E01014890,E01014891,E01014892,E01014893,E01014894,E01014895,E01014896,E01014897,E01014898,E01014899,E01014900,E01014901,E01014902,E01014903,E01014904,E01014905,E01014906,E01014907,E01014908,E01014909,E01014910,E01014911,E01014912,E01014913,E01014914,E01014915,E01014916,E01014917,E01014918,E01014919,E01014920,E01014921,E01014922,E01014923,E01014924,E01014925,E01014926,E01014927,E01014928,E01014929,E01014930,E01014931,E01014932,E01014933,E01014934,E01014935,E01014936,E01014937,E01014938,E01014939,E01014940,E01014941,E01014942,E01014943,E01014944,E01014945,E01014946,E01014947,E01014948,E01014950,E01014951,E01014953,E01014954,E01014955,E01014956,E01014957,E01014958,E01014959,E01014960,E01014961,E01014962,E01014963,E01014964,E01014965,E01014967,E01014968,E01014969,E01014972,E01014973,E01014974,E01014975,E01014976,E01014977,E01014978,E01014979,E01014980,E01014982,E01014983,E01014984,E01014985,E01014986,E01014987,E01014988,E01014989,E01014990,E01014991,E01014992,E01014993,E01014994,E01014995,E01014996,E01014998,E01014999,E01015000,E01015001,E01015002,E01015003,E01015004,E01015005,E01015006,E01015007,E01015008,E01015009,E01015010,E01015011,E01015012,E01015013,E01015014,E01015015,E01015016,E01015017,E01015018,E01015019,E01015020,E01015021,E01015022,E01015023,E01015024,E01015025,E01015026,E01015027,E01015028,E01015029,E01015030,E01015031,E01015032,E01015033,E01015034,E01015035,E01015036,E01015037,E01015038,E01015039,E01015040,E01015041,E01015042,E01015043,E01015044,E01015045,E01015046,E01015047,E01015048,E01015049,E01015050,E01015051,E01015052,E01015053,E01015054,E01015055,E01015056,E01015057,E01015058,E01015059,E01015060,E01015061,E01015062,E01015063,E01015064,E01015065,E01015066,E01015067,E01015068,E01015069,E01015070,E01015071,E01015072,E01015073,E01015074,E01015075,E01015076,E01015077,E01015078,E01015079,E01015080,E01015081,E01015082,E01015083,E01015084,E01015085,E01015086,E01015087,E01015088,E01015089,E01015090,E01015091,E01015092,E01015093,E01015094,E01015095,E01015096,E01015097,E01015098,E01015099,E01015100,E01015101,E01015102,E01015103,E01015104,E01015105,E01015106,E01015107,E01015108,E01015109,E01015110,E01015111,E01015112,E01015113,E01015114,E01015115,E01015116,E01015117,E01015118,E01015119,E01015120,E01015121,E01015122,E01015123,E01015124,E01015125,E01015126,E01015127,E01015128,E01015129,E01015130,E01015131,E01015132,E01015133,E01015134,E01015136,E01015137,E01015138,E01015139,E01015140,E01015141,E01015142,E01015143,E01015144,E01015145,E01015146,E01015147,E01015148,E01015149,E01015150,E01015151,E01015152,E01015153,E01015155,E01015156,E01015159,E01015160,E01015161,E01015162,E01015163,E01015164,E01015165,E01015166,E01015167,E01015168,E01015169,E01015170,E01015171,E01015172,E01015173,E01015174,E01015175,E01015176,E01015177,E01015178,E01015179,E01015180,E01015181,E01015182,E01015183,E01015184,E01015185,E01015186,E01015187,E01015188,E01015189,E01015190,E01015191,E01015192,E01015193,E01015194,E01015196,E01015197,E01015198,E01015199,E01015200,E01015201,E01015202,E01015203,E01015204,E01015205,E01015206,E01015207,E01015208,E01015209,E01015210,E01015211,E01015212,E01015213,E01015214,E01015215,E01015216,E01015217,E01015218,E01015219,E01015220,E01015221,E01015222,E01015223,E01015224,E01015225,E01015226,E01015227,E01015228,E01015229,E01015230,E01015231,E01015232,E01015233,E01015234,E01015235,E01015236,E01015237,E01015238,E01015239,E01015240,E01015241,E01015242,E01015243,E01015244,E01015245,E01015246,E01015247,E01015248,E01015249,E01015250,E01015251,E01015253,E01015254,E01015255,E01015256,E01015257,E01015258,E01015259,E01015260,E01015261,E01015262,E01015263,E01015264,E01015265,E01015266&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_029.json.gz + content_sha256: 859b8604c440fcf38b5d0afe2f4c9b12d2a74077f8e553df4a30b4d035dd8e0a +- id: census_lsoa_tenure5a_bedrooms_030 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 030 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01015267,E01015268,E01015269,E01015270,E01015271,E01015272,E01015273,E01015274,E01015275,E01015276,E01015277,E01015279,E01015280,E01015281,E01015286,E01015288,E01015289,E01015290,E01015291,E01015292,E01015293,E01015294,E01015295,E01015296,E01015297,E01015298,E01015299,E01015300,E01015301,E01015302,E01015303,E01015304,E01015305,E01015306,E01015307,E01015308,E01015309,E01015310,E01015311,E01015312,E01015313,E01015314,E01015315,E01015316,E01015317,E01015318,E01015319,E01015320,E01015321,E01015322,E01015323,E01015324,E01015325,E01015326,E01015327,E01015328,E01015329,E01015330,E01015331,E01015332,E01015333,E01015334,E01015335,E01015336,E01015337,E01015338,E01015339,E01015340,E01015341,E01015342,E01015343,E01015344,E01015345,E01015346,E01015348,E01015349,E01015350,E01015351,E01015352,E01015353,E01015354,E01015355,E01015356,E01015357,E01015358,E01015359,E01015360,E01015361,E01015362,E01015363,E01015364,E01015365,E01015366,E01015367,E01015368,E01015369,E01015370,E01015371,E01015372,E01015373,E01015374,E01015375,E01015376,E01015377,E01015378,E01015379,E01015380,E01015381,E01015382,E01015383,E01015384,E01015385,E01015386,E01015387,E01015388,E01015389,E01015390,E01015391,E01015392,E01015393,E01015394,E01015395,E01015396,E01015397,E01015398,E01015399,E01015400,E01015401,E01015402,E01015403,E01015404,E01015405,E01015406,E01015407,E01015408,E01015409,E01015410,E01015411,E01015412,E01015413,E01015414,E01015415,E01015416,E01015417,E01015418,E01015419,E01015420,E01015421,E01015422,E01015424,E01015425,E01015426,E01015427,E01015428,E01015429,E01015430,E01015431,E01015432,E01015433,E01015434,E01015435,E01015436,E01015437,E01015438,E01015439,E01015440,E01015441,E01015442,E01015443,E01015444,E01015445,E01015446,E01015447,E01015448,E01015449,E01015450,E01015451,E01015452,E01015453,E01015454,E01015455,E01015456,E01015457,E01015458,E01015459,E01015460,E01015461,E01015462,E01015463,E01015464,E01015465,E01015466,E01015471,E01015473,E01015475,E01015477,E01015478,E01015479,E01015480,E01015481,E01015482,E01015483,E01015484,E01015485,E01015486,E01015487,E01015488,E01015489,E01015490,E01015491,E01015492,E01015493,E01015494,E01015496,E01015497,E01015498,E01015499,E01015500,E01015501,E01015502,E01015503,E01015504,E01015505,E01015506,E01015507,E01015508,E01015509,E01015510,E01015511,E01015512,E01015513,E01015514,E01015515,E01015516,E01015517,E01015518,E01015519,E01015520,E01015521,E01015522,E01015523,E01015524,E01015525,E01015526,E01015527,E01015528,E01015529,E01015530,E01015532,E01015533,E01015534,E01015535,E01015537,E01015538,E01015539,E01015540,E01015541,E01015542,E01015543,E01015544,E01015545,E01015546,E01015547,E01015548,E01015549,E01015550,E01015551,E01015552,E01015553,E01015554,E01015555,E01015556,E01015557,E01015558,E01015559,E01015560,E01015561,E01015562,E01015563,E01015564,E01015565,E01015566,E01015567,E01015568,E01015569,E01015570,E01015571,E01015572,E01015573,E01015574,E01015575,E01015576,E01015578,E01015579,E01015580,E01015581,E01015582,E01015584,E01015585,E01015586,E01015587,E01015588,E01015589,E01015590,E01015591,E01015592,E01015593,E01015594,E01015595,E01015596,E01015597,E01015598,E01015599,E01015600,E01015601,E01015603,E01015604,E01015605,E01015606,E01015607,E01015608,E01015609,E01015610,E01015612,E01015613,E01015614,E01015615,E01015616,E01015617,E01015618,E01015619,E01015620,E01015622,E01015623,E01015624,E01015626,E01015627,E01015628,E01015629,E01015630,E01015631,E01015632,E01015633,E01015634,E01015635,E01015636,E01015637,E01015638,E01015639,E01015640,E01015641,E01015642,E01015643,E01015644,E01015645,E01015646,E01015647,E01015648,E01015650,E01015651,E01015652,E01015653,E01015654,E01015655,E01015656,E01015658,E01015659,E01015660,E01015661,E01015662,E01015663,E01015664,E01015665,E01015666,E01015667,E01015668,E01015669,E01015670,E01015672,E01015673,E01015674,E01015675,E01015676,E01015677,E01015678,E01015679,E01015680,E01015681,E01015682,E01015683,E01015684,E01015685,E01015686,E01015687,E01015688,E01015689,E01015690,E01015691,E01015692,E01015693,E01015694,E01015695,E01015696,E01015697,E01015698,E01015699,E01015700,E01015701,E01015702,E01015703,E01015704,E01015705,E01015706,E01015707,E01015708,E01015709,E01015710,E01015711,E01015712,E01015713,E01015714,E01015715,E01015716,E01015717,E01015718,E01015719,E01015720,E01015721,E01015722,E01015723,E01015724,E01015725,E01015726,E01015727,E01015728,E01015729,E01015730,E01015731,E01015732,E01015733,E01015734,E01015735,E01015736,E01015737,E01015738,E01015739,E01015740,E01015742,E01015743,E01015744,E01015745,E01015746,E01015747,E01015748,E01015749,E01015750,E01015751,E01015752,E01015753,E01015754,E01015755,E01015756,E01015757,E01015758,E01015759,E01015760,E01015761,E01015762,E01015763,E01015764,E01015765,E01015766,E01015767,E01015768,E01015769,E01015770,E01015771,E01015772,E01015773,E01015774,E01015775,E01015776,E01015777,E01015778,E01015779,E01015780,E01015781,E01015782,E01015783,E01015784,E01015785,E01015786,E01015787,E01015788,E01015789,E01015790,E01015792,E01015793,E01015795,E01015796&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_030.json.gz + content_sha256: e9f395b9d484259865c3d93a015f9e2f42e8f60172de2333666f1d650d1380c1 +- id: census_lsoa_tenure5a_bedrooms_031 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 031 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01015797,E01015798,E01015799,E01015800,E01015801,E01015802,E01015803,E01015804,E01015805,E01015806,E01015807,E01015808,E01015809,E01015810,E01015811,E01015812,E01015813,E01015814,E01015815,E01015816,E01015817,E01015818,E01015819,E01015820,E01015821,E01015822,E01015823,E01015824,E01015825,E01015826,E01015827,E01015828,E01015829,E01015830,E01015831,E01015832,E01015833,E01015834,E01015835,E01015836,E01015837,E01015838,E01015839,E01015840,E01015841,E01015842,E01015843,E01015844,E01015845,E01015846,E01015847,E01015848,E01015849,E01015850,E01015851,E01015852,E01015853,E01015854,E01015855,E01015856,E01015857,E01015858,E01015859,E01015860,E01015861,E01015862,E01015863,E01015864,E01015865,E01015866,E01015867,E01015868,E01015869,E01015870,E01015871,E01015872,E01015873,E01015874,E01015875,E01015876,E01015877,E01015878,E01015879,E01015880,E01015881,E01015882,E01015883,E01015884,E01015885,E01015886,E01015887,E01015888,E01015889,E01015890,E01015891,E01015892,E01015893,E01015894,E01015896,E01015897,E01015898,E01015899,E01015900,E01015901,E01015902,E01015903,E01015904,E01015905,E01015906,E01015907,E01015908,E01015909,E01015910,E01015911,E01015912,E01015913,E01015914,E01015915,E01015916,E01015917,E01015918,E01015919,E01015920,E01015921,E01015922,E01015923,E01015924,E01015925,E01015926,E01015927,E01015928,E01015929,E01015930,E01015931,E01015932,E01015933,E01015934,E01015935,E01015936,E01015937,E01015938,E01015939,E01015941,E01015942,E01015943,E01015944,E01015945,E01015946,E01015947,E01015948,E01015949,E01015950,E01015951,E01015952,E01015953,E01015954,E01015955,E01015956,E01015957,E01015958,E01015959,E01015960,E01015961,E01015962,E01015963,E01015964,E01015965,E01015966,E01015967,E01015968,E01015969,E01015970,E01015971,E01015972,E01015974,E01015975,E01015976,E01015977,E01015978,E01015979,E01015980,E01015982,E01015983,E01015984,E01015985,E01015986,E01015987,E01015988,E01015989,E01015990,E01015991,E01015992,E01015993,E01015994,E01015995,E01015996,E01015997,E01015998,E01015999,E01016000,E01016001,E01016002,E01016003,E01016004,E01016005,E01016006,E01016007,E01016008,E01016009,E01016010,E01016011,E01016013,E01016014,E01016015,E01016016,E01016017,E01016018,E01016019,E01016020,E01016021,E01016022,E01016023,E01016024,E01016025,E01016026,E01016027,E01016028,E01016029,E01016030,E01016031,E01016032,E01016033,E01016034,E01016035,E01016037,E01016038,E01016039,E01016040,E01016041,E01016042,E01016043,E01016044,E01016045,E01016046,E01016047,E01016048,E01016049,E01016050,E01016051,E01016052,E01016053,E01016054,E01016055,E01016056,E01016057,E01016058,E01016059,E01016060,E01016061,E01016062,E01016063,E01016064,E01016065,E01016066,E01016067,E01016068,E01016069,E01016070,E01016071,E01016072,E01016073,E01016074,E01016075,E01016077,E01016078,E01016079,E01016080,E01016081,E01016082,E01016083,E01016084,E01016085,E01016086,E01016087,E01016088,E01016089,E01016090,E01016091,E01016092,E01016093,E01016094,E01016095,E01016096,E01016097,E01016098,E01016099,E01016100,E01016101,E01016102,E01016103,E01016104,E01016105,E01016106,E01016107,E01016111,E01016113,E01016114,E01016115,E01016116,E01016117,E01016118,E01016119,E01016120,E01016121,E01016122,E01016123,E01016124,E01016125,E01016126,E01016127,E01016128,E01016129,E01016130,E01016131,E01016132,E01016133,E01016134,E01016135,E01016136,E01016137,E01016138,E01016139,E01016140,E01016141,E01016142,E01016143,E01016144,E01016145,E01016147,E01016148,E01016149,E01016150,E01016151,E01016152,E01016153,E01016154,E01016155,E01016156,E01016157,E01016158,E01016159,E01016160,E01016161,E01016162,E01016163,E01016164,E01016165,E01016166,E01016167,E01016168,E01016169,E01016170,E01016171,E01016172,E01016173,E01016174,E01016175,E01016176,E01016177,E01016178,E01016179,E01016180,E01016181,E01016182,E01016183,E01016185,E01016187,E01016188,E01016190,E01016191,E01016192,E01016193,E01016194,E01016195,E01016196,E01016197,E01016198,E01016199,E01016200,E01016201,E01016202,E01016203,E01016204,E01016205,E01016206,E01016207,E01016208,E01016209,E01016210,E01016212,E01016213,E01016214,E01016215,E01016216,E01016217,E01016218,E01016219,E01016220,E01016221,E01016222,E01016223,E01016224,E01016226,E01016227,E01016228,E01016229,E01016230,E01016231,E01016232,E01016233,E01016234,E01016235,E01016236,E01016237,E01016238,E01016239,E01016240,E01016241,E01016242,E01016243,E01016245,E01016246,E01016247,E01016248,E01016250,E01016252,E01016253,E01016254,E01016255,E01016256,E01016257,E01016258,E01016259,E01016260,E01016261,E01016262,E01016263,E01016264,E01016265,E01016266,E01016267,E01016268,E01016269,E01016270,E01016271,E01016272,E01016273,E01016274,E01016275,E01016276,E01016277,E01016279,E01016280,E01016281,E01016282,E01016284,E01016285,E01016286,E01016287,E01016288,E01016289,E01016290,E01016291,E01016292,E01016294,E01016295,E01016296,E01016297,E01016298,E01016299,E01016300,E01016301,E01016302,E01016303,E01016304,E01016305,E01016306,E01016307,E01016308,E01016309,E01016310,E01016311,E01016312,E01016313,E01016314,E01016315,E01016316,E01016317,E01016318,E01016319&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_031.json.gz + content_sha256: 57fca429d8f8b27e39be04352b2cedbc7b95ab3869e3509031b3a4b5708b4334 +- id: census_lsoa_tenure5a_bedrooms_032 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 032 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01016320,E01016321,E01016322,E01016323,E01016324,E01016325,E01016326,E01016327,E01016328,E01016329,E01016330,E01016331,E01016332,E01016333,E01016334,E01016335,E01016336,E01016337,E01016338,E01016339,E01016340,E01016341,E01016342,E01016343,E01016344,E01016345,E01016346,E01016348,E01016349,E01016350,E01016353,E01016355,E01016356,E01016357,E01016358,E01016359,E01016360,E01016361,E01016362,E01016363,E01016364,E01016365,E01016366,E01016367,E01016368,E01016369,E01016370,E01016371,E01016372,E01016373,E01016374,E01016375,E01016376,E01016377,E01016379,E01016380,E01016381,E01016382,E01016383,E01016384,E01016385,E01016386,E01016387,E01016388,E01016389,E01016390,E01016391,E01016392,E01016393,E01016394,E01016395,E01016396,E01016397,E01016398,E01016399,E01016400,E01016401,E01016402,E01016403,E01016404,E01016405,E01016406,E01016407,E01016408,E01016409,E01016410,E01016411,E01016412,E01016413,E01016414,E01016415,E01016416,E01016417,E01016418,E01016419,E01016420,E01016421,E01016422,E01016423,E01016424,E01016425,E01016426,E01016427,E01016428,E01016429,E01016430,E01016431,E01016432,E01016433,E01016434,E01016435,E01016436,E01016437,E01016438,E01016439,E01016440,E01016441,E01016443,E01016444,E01016445,E01016446,E01016447,E01016448,E01016449,E01016450,E01016451,E01016452,E01016453,E01016454,E01016455,E01016456,E01016457,E01016458,E01016460,E01016462,E01016463,E01016464,E01016465,E01016467,E01016468,E01016469,E01016470,E01016471,E01016472,E01016473,E01016474,E01016475,E01016476,E01016477,E01016478,E01016479,E01016480,E01016481,E01016482,E01016483,E01016484,E01016485,E01016486,E01016487,E01016488,E01016490,E01016491,E01016492,E01016493,E01016494,E01016495,E01016496,E01016497,E01016498,E01016499,E01016500,E01016501,E01016502,E01016503,E01016504,E01016505,E01016506,E01016507,E01016508,E01016509,E01016510,E01016511,E01016514,E01016515,E01016516,E01016517,E01016518,E01016519,E01016520,E01016521,E01016522,E01016523,E01016524,E01016525,E01016526,E01016527,E01016528,E01016529,E01016530,E01016531,E01016532,E01016533,E01016534,E01016535,E01016536,E01016537,E01016538,E01016539,E01016540,E01016541,E01016542,E01016543,E01016544,E01016545,E01016546,E01016547,E01016548,E01016549,E01016550,E01016551,E01016552,E01016553,E01016554,E01016555,E01016556,E01016557,E01016558,E01016559,E01016560,E01016561,E01016562,E01016563,E01016564,E01016565,E01016566,E01016567,E01016568,E01016569,E01016570,E01016571,E01016572,E01016573,E01016574,E01016575,E01016576,E01016577,E01016578,E01016579,E01016580,E01016581,E01016582,E01016583,E01016584,E01016585,E01016586,E01016587,E01016588,E01016589,E01016591,E01016592,E01016593,E01016594,E01016595,E01016596,E01016597,E01016598,E01016599,E01016600,E01016601,E01016602,E01016603,E01016604,E01016605,E01016606,E01016607,E01016608,E01016609,E01016611,E01016612,E01016613,E01016614,E01016615,E01016616,E01016617,E01016618,E01016619,E01016620,E01016621,E01016622,E01016623,E01016624,E01016625,E01016626,E01016627,E01016628,E01016629,E01016630,E01016631,E01016632,E01016633,E01016634,E01016635,E01016636,E01016637,E01016638,E01016639,E01016640,E01016643,E01016644,E01016645,E01016646,E01016647,E01016648,E01016649,E01016651,E01016652,E01016653,E01016654,E01016655,E01016656,E01016657,E01016658,E01016659,E01016660,E01016661,E01016662,E01016663,E01016664,E01016665,E01016666,E01016668,E01016669,E01016670,E01016671,E01016672,E01016673,E01016674,E01016676,E01016678,E01016679,E01016680,E01016684,E01016685,E01016686,E01016687,E01016688,E01016689,E01016690,E01016691,E01016692,E01016693,E01016694,E01016695,E01016696,E01016699,E01016700,E01016701,E01016702,E01016703,E01016704,E01016705,E01016706,E01016707,E01016708,E01016709,E01016710,E01016711,E01016713,E01016714,E01016715,E01016716,E01016717,E01016718,E01016719,E01016720,E01016721,E01016722,E01016723,E01016724,E01016725,E01016726,E01016727,E01016728,E01016729,E01016731,E01016732,E01016733,E01016734,E01016736,E01016737,E01016738,E01016739,E01016740,E01016741,E01016742,E01016744,E01016745,E01016746,E01016748,E01016750,E01016751,E01016752,E01016753,E01016754,E01016755,E01016756,E01016757,E01016758,E01016759,E01016761,E01016762,E01016763,E01016764,E01016765,E01016766,E01016767,E01016768,E01016769,E01016770,E01016771,E01016772,E01016773,E01016774,E01016775,E01016776,E01016777,E01016778,E01016780,E01016781,E01016783,E01016784,E01016786,E01016787,E01016788,E01016789,E01016790,E01016791,E01016792,E01016793,E01016794,E01016795,E01016796,E01016797,E01016798,E01016799,E01016800,E01016801,E01016802,E01016803,E01016804,E01016805,E01016806,E01016807,E01016808,E01016809,E01016810,E01016811,E01016812,E01016813,E01016815,E01016816,E01016817,E01016818,E01016819,E01016820,E01016821,E01016822,E01016823,E01016824,E01016825,E01016826,E01016827,E01016828,E01016829,E01016830,E01016831,E01016832,E01016833,E01016834,E01016835,E01016836,E01016837,E01016838,E01016839,E01016841,E01016842,E01016843,E01016844,E01016845,E01016846,E01016847,E01016848,E01016849,E01016850,E01016851,E01016852,E01016853,E01016854,E01016855,E01016856&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_032.json.gz + content_sha256: 32df4eb7f57a5eec6f0921bf99fc84a9a6e86b7ef8bfbfbfb1d27fdbf89f2c09 +- id: census_lsoa_tenure5a_bedrooms_033 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 033 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01016857,E01016858,E01016859,E01016860,E01016861,E01016862,E01016863,E01016864,E01016865,E01016866,E01016867,E01016868,E01016869,E01016870,E01016871,E01016872,E01016873,E01016874,E01016875,E01016876,E01016877,E01016878,E01016879,E01016880,E01016881,E01016882,E01016883,E01016884,E01016885,E01016886,E01016887,E01016888,E01016889,E01016890,E01016891,E01016892,E01016893,E01016894,E01016895,E01016896,E01016897,E01016898,E01016899,E01016900,E01016901,E01016902,E01016903,E01016904,E01016905,E01016906,E01016907,E01016908,E01016909,E01016910,E01016911,E01016912,E01016913,E01016914,E01016915,E01016916,E01016917,E01016918,E01016919,E01016920,E01016921,E01016922,E01016923,E01016924,E01016925,E01016926,E01016927,E01016928,E01016929,E01016930,E01016931,E01016932,E01016933,E01016934,E01016935,E01016936,E01016937,E01016938,E01016939,E01016940,E01016941,E01016942,E01016943,E01016944,E01016945,E01016946,E01016947,E01016948,E01016949,E01016950,E01016951,E01016952,E01016953,E01016954,E01016955,E01016956,E01016957,E01016958,E01016959,E01016960,E01016961,E01016962,E01016963,E01016964,E01016966,E01016967,E01016968,E01016969,E01016970,E01016971,E01016972,E01016973,E01016974,E01016975,E01016976,E01016977,E01016978,E01016979,E01016980,E01016981,E01016982,E01016983,E01016984,E01016985,E01016986,E01016987,E01016988,E01016989,E01016990,E01016991,E01016992,E01016993,E01016994,E01016995,E01016996,E01016997,E01016998,E01016999,E01017000,E01017001,E01017002,E01017003,E01017004,E01017005,E01017006,E01017007,E01017008,E01017009,E01017010,E01017011,E01017012,E01017013,E01017014,E01017015,E01017016,E01017017,E01017018,E01017019,E01017020,E01017021,E01017022,E01017023,E01017024,E01017025,E01017026,E01017027,E01017028,E01017029,E01017030,E01017031,E01017032,E01017034,E01017035,E01017036,E01017037,E01017038,E01017039,E01017040,E01017041,E01017042,E01017043,E01017044,E01017045,E01017046,E01017047,E01017048,E01017049,E01017050,E01017051,E01017052,E01017053,E01017054,E01017055,E01017056,E01017057,E01017058,E01017059,E01017060,E01017061,E01017062,E01017063,E01017064,E01017066,E01017067,E01017068,E01017069,E01017070,E01017071,E01017072,E01017073,E01017074,E01017075,E01017076,E01017077,E01017078,E01017079,E01017080,E01017081,E01017082,E01017083,E01017084,E01017085,E01017086,E01017087,E01017088,E01017089,E01017090,E01017091,E01017092,E01017093,E01017094,E01017095,E01017096,E01017097,E01017098,E01017099,E01017100,E01017101,E01017102,E01017103,E01017104,E01017105,E01017106,E01017107,E01017108,E01017109,E01017110,E01017111,E01017112,E01017113,E01017114,E01017115,E01017116,E01017117,E01017118,E01017119,E01017120,E01017121,E01017122,E01017123,E01017124,E01017125,E01017126,E01017127,E01017128,E01017129,E01017130,E01017131,E01017132,E01017133,E01017135,E01017136,E01017137,E01017138,E01017139,E01017144,E01017145,E01017146,E01017147,E01017148,E01017149,E01017150,E01017151,E01017152,E01017153,E01017154,E01017155,E01017156,E01017157,E01017158,E01017159,E01017160,E01017162,E01017163,E01017164,E01017165,E01017166,E01017167,E01017168,E01017169,E01017170,E01017171,E01017172,E01017173,E01017174,E01017175,E01017176,E01017177,E01017178,E01017179,E01017180,E01017181,E01017182,E01017183,E01017184,E01017185,E01017186,E01017187,E01017188,E01017189,E01017190,E01017191,E01017192,E01017193,E01017194,E01017195,E01017196,E01017197,E01017198,E01017199,E01017200,E01017201,E01017202,E01017203,E01017204,E01017205,E01017206,E01017207,E01017208,E01017209,E01017210,E01017211,E01017212,E01017213,E01017214,E01017215,E01017216,E01017217,E01017218,E01017219,E01017220,E01017221,E01017222,E01017223,E01017224,E01017225,E01017226,E01017227,E01017228,E01017231,E01017232,E01017233,E01017234,E01017235,E01017236,E01017237,E01017238,E01017239,E01017240,E01017241,E01017242,E01017243,E01017244,E01017245,E01017246,E01017247,E01017248,E01017249,E01017250,E01017251,E01017252,E01017253,E01017254,E01017255,E01017256,E01017257,E01017258,E01017259,E01017260,E01017261,E01017262,E01017263,E01017264,E01017265,E01017266,E01017267,E01017270,E01017271,E01017272,E01017273,E01017274,E01017275,E01017276,E01017277,E01017279,E01017280,E01017281,E01017282,E01017283,E01017284,E01017285,E01017286,E01017287,E01017288,E01017289,E01017290,E01017291,E01017292,E01017293,E01017294,E01017295,E01017296,E01017297,E01017298,E01017299,E01017300,E01017301,E01017302,E01017303,E01017304,E01017305,E01017306,E01017307,E01017308,E01017309,E01017310,E01017311,E01017312,E01017315,E01017316,E01017317,E01017318,E01017319,E01017320,E01017321,E01017322,E01017323,E01017324,E01017325,E01017326,E01017327,E01017328,E01017329,E01017331,E01017332,E01017333,E01017334,E01017335,E01017336,E01017337,E01017338,E01017339,E01017340,E01017341,E01017342,E01017343,E01017344,E01017345,E01017346,E01017347,E01017348,E01017349,E01017350,E01017351,E01017352,E01017353,E01017354,E01017355,E01017356,E01017357,E01017358,E01017359,E01017360,E01017361,E01017362,E01017363,E01017364,E01017365,E01017366,E01017367,E01017368,E01017369,E01017370,E01017371,E01017372,E01017373&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_033.json.gz + content_sha256: 9b842fec612087a0d3d37c52d1c1134627bdc8b7d2628e5a019dfed0586ac19e +- id: census_lsoa_tenure5a_bedrooms_034 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 034 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01017374,E01017375,E01017376,E01017377,E01017378,E01017379,E01017380,E01017381,E01017382,E01017383,E01017384,E01017385,E01017386,E01017387,E01017388,E01017391,E01017392,E01017393,E01017394,E01017397,E01017398,E01017399,E01017400,E01017401,E01017402,E01017403,E01017404,E01017405,E01017406,E01017407,E01017408,E01017409,E01017410,E01017412,E01017413,E01017414,E01017415,E01017418,E01017419,E01017420,E01017421,E01017422,E01017423,E01017424,E01017425,E01017426,E01017427,E01017428,E01017429,E01017430,E01017431,E01017432,E01017433,E01017434,E01017435,E01017436,E01017437,E01017438,E01017439,E01017440,E01017441,E01017442,E01017443,E01017444,E01017446,E01017447,E01017449,E01017450,E01017451,E01017452,E01017453,E01017454,E01017455,E01017456,E01017457,E01017458,E01017459,E01017460,E01017462,E01017463,E01017464,E01017465,E01017466,E01017467,E01017468,E01017470,E01017471,E01017472,E01017474,E01017475,E01017476,E01017477,E01017478,E01017479,E01017480,E01017481,E01017482,E01017483,E01017484,E01017485,E01017487,E01017488,E01017489,E01017490,E01017491,E01017492,E01017493,E01017495,E01017496,E01017497,E01017498,E01017499,E01017500,E01017501,E01017502,E01017503,E01017504,E01017505,E01017506,E01017507,E01017508,E01017509,E01017510,E01017511,E01017512,E01017513,E01017514,E01017515,E01017517,E01017518,E01017519,E01017520,E01017521,E01017522,E01017523,E01017524,E01017525,E01017526,E01017527,E01017528,E01017529,E01017530,E01017531,E01017532,E01017533,E01017534,E01017535,E01017536,E01017537,E01017538,E01017539,E01017540,E01017541,E01017542,E01017543,E01017544,E01017545,E01017546,E01017547,E01017549,E01017552,E01017553,E01017554,E01017555,E01017556,E01017557,E01017558,E01017559,E01017560,E01017561,E01017562,E01017563,E01017564,E01017565,E01017566,E01017567,E01017568,E01017569,E01017571,E01017572,E01017573,E01017576,E01017577,E01017578,E01017579,E01017580,E01017581,E01017582,E01017583,E01017584,E01017585,E01017586,E01017587,E01017588,E01017589,E01017590,E01017591,E01017592,E01017593,E01017594,E01017595,E01017596,E01017597,E01017598,E01017600,E01017601,E01017602,E01017603,E01017604,E01017605,E01017606,E01017607,E01017608,E01017609,E01017610,E01017611,E01017612,E01017613,E01017614,E01017616,E01017617,E01017618,E01017619,E01017620,E01017621,E01017622,E01017623,E01017624,E01017625,E01017626,E01017627,E01017628,E01017629,E01017630,E01017631,E01017634,E01017635,E01017636,E01017637,E01017638,E01017639,E01017640,E01017642,E01017643,E01017644,E01017645,E01017646,E01017647,E01017648,E01017649,E01017651,E01017652,E01017653,E01017654,E01017655,E01017657,E01017658,E01017659,E01017660,E01017661,E01017662,E01017663,E01017664,E01017665,E01017666,E01017667,E01017668,E01017669,E01017670,E01017671,E01017672,E01017673,E01017674,E01017675,E01017676,E01017677,E01017678,E01017679,E01017680,E01017681,E01017682,E01017683,E01017684,E01017687,E01017688,E01017689,E01017690,E01017691,E01017692,E01017693,E01017694,E01017695,E01017696,E01017697,E01017698,E01017699,E01017700,E01017701,E01017702,E01017703,E01017704,E01017706,E01017707,E01017708,E01017709,E01017710,E01017711,E01017712,E01017713,E01017714,E01017715,E01017716,E01017717,E01017718,E01017719,E01017720,E01017721,E01017722,E01017723,E01017724,E01017726,E01017727,E01017729,E01017730,E01017731,E01017732,E01017733,E01017734,E01017735,E01017736,E01017737,E01017738,E01017739,E01017740,E01017741,E01017742,E01017743,E01017744,E01017745,E01017746,E01017747,E01017748,E01017749,E01017750,E01017751,E01017752,E01017753,E01017754,E01017755,E01017756,E01017757,E01017758,E01017759,E01017760,E01017761,E01017762,E01017763,E01017764,E01017765,E01017766,E01017767,E01017768,E01017769,E01017770,E01017771,E01017773,E01017774,E01017775,E01017776,E01017777,E01017778,E01017779,E01017780,E01017781,E01017782,E01017783,E01017784,E01017785,E01017786,E01017787,E01017788,E01017789,E01017790,E01017791,E01017792,E01017793,E01017794,E01017795,E01017796,E01017797,E01017798,E01017799,E01017800,E01017801,E01017802,E01017803,E01017804,E01017805,E01017806,E01017807,E01017808,E01017809,E01017810,E01017811,E01017812,E01017813,E01017814,E01017815,E01017816,E01017817,E01017818,E01017819,E01017820,E01017821,E01017822,E01017823,E01017824,E01017825,E01017826,E01017827,E01017828,E01017829,E01017830,E01017831,E01017832,E01017833,E01017834,E01017835,E01017836,E01017837,E01017838,E01017839,E01017840,E01017841,E01017843,E01017844,E01017845,E01017846,E01017847,E01017848,E01017849,E01017850,E01017851,E01017852,E01017853,E01017854,E01017855,E01017856,E01017857,E01017858,E01017859,E01017860,E01017861,E01017862,E01017863,E01017864,E01017865,E01017866,E01017867,E01017868,E01017869,E01017870,E01017871,E01017872,E01017873,E01017874,E01017875,E01017876,E01017877,E01017878,E01017880,E01017881,E01017882,E01017883,E01017884,E01017885,E01017886,E01017887,E01017888,E01017889,E01017890,E01017891,E01017892,E01017893,E01017894,E01017895,E01017896,E01017897,E01017898,E01017899,E01017900,E01017901,E01017902,E01017903,E01017904,E01017905,E01017906,E01017907,E01017910,E01017911&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_034.json.gz + content_sha256: 33728b44fb8ccaf3cd3c512a4778b037be091b47bd34c6eaf674a9d77b896642 +- id: census_lsoa_tenure5a_bedrooms_035 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 035 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01017912,E01017913,E01017914,E01017915,E01017916,E01017917,E01017918,E01017919,E01017920,E01017921,E01017922,E01017923,E01017924,E01017925,E01017926,E01017927,E01017928,E01017929,E01017930,E01017931,E01017932,E01017933,E01017934,E01017935,E01017936,E01017937,E01017938,E01017939,E01017940,E01017941,E01017942,E01017943,E01017944,E01017945,E01017946,E01017947,E01017948,E01017949,E01017950,E01017951,E01017952,E01017953,E01017954,E01017955,E01017959,E01017960,E01017961,E01017962,E01017963,E01017964,E01017965,E01017967,E01017968,E01017969,E01017971,E01017972,E01017973,E01017974,E01017975,E01017976,E01017977,E01017978,E01017979,E01017980,E01017983,E01017985,E01017987,E01017988,E01017989,E01017990,E01017991,E01017992,E01017994,E01017995,E01017996,E01017997,E01017998,E01017999,E01018000,E01018001,E01018005,E01018006,E01018007,E01018008,E01018009,E01018010,E01018011,E01018012,E01018013,E01018014,E01018015,E01018016,E01018017,E01018019,E01018020,E01018021,E01018022,E01018023,E01018024,E01018025,E01018026,E01018027,E01018032,E01018033,E01018034,E01018035,E01018036,E01018037,E01018038,E01018039,E01018040,E01018041,E01018042,E01018043,E01018044,E01018045,E01018047,E01018049,E01018050,E01018051,E01018052,E01018053,E01018054,E01018055,E01018056,E01018057,E01018058,E01018059,E01018060,E01018061,E01018062,E01018063,E01018064,E01018065,E01018066,E01018067,E01018068,E01018069,E01018070,E01018071,E01018072,E01018073,E01018074,E01018075,E01018076,E01018077,E01018078,E01018079,E01018080,E01018081,E01018082,E01018083,E01018084,E01018085,E01018086,E01018087,E01018088,E01018090,E01018091,E01018092,E01018093,E01018094,E01018095,E01018096,E01018097,E01018098,E01018099,E01018100,E01018101,E01018103,E01018104,E01018105,E01018106,E01018107,E01018108,E01018109,E01018110,E01018111,E01018113,E01018115,E01018116,E01018117,E01018118,E01018119,E01018120,E01018121,E01018122,E01018123,E01018124,E01018125,E01018126,E01018127,E01018128,E01018129,E01018130,E01018131,E01018133,E01018134,E01018136,E01018137,E01018138,E01018139,E01018140,E01018141,E01018142,E01018143,E01018144,E01018145,E01018146,E01018147,E01018148,E01018149,E01018150,E01018152,E01018153,E01018154,E01018155,E01018156,E01018157,E01018158,E01018159,E01018160,E01018161,E01018162,E01018163,E01018164,E01018165,E01018166,E01018167,E01018168,E01018169,E01018170,E01018171,E01018172,E01018173,E01018174,E01018175,E01018176,E01018177,E01018178,E01018179,E01018180,E01018182,E01018183,E01018184,E01018185,E01018186,E01018187,E01018188,E01018189,E01018190,E01018191,E01018192,E01018193,E01018194,E01018195,E01018196,E01018197,E01018198,E01018199,E01018200,E01018201,E01018202,E01018203,E01018204,E01018205,E01018206,E01018207,E01018208,E01018209,E01018210,E01018211,E01018212,E01018213,E01018214,E01018215,E01018216,E01018217,E01018218,E01018219,E01018220,E01018221,E01018222,E01018223,E01018224,E01018225,E01018226,E01018227,E01018228,E01018231,E01018232,E01018233,E01018234,E01018235,E01018236,E01018237,E01018238,E01018239,E01018240,E01018241,E01018242,E01018243,E01018244,E01018245,E01018246,E01018247,E01018248,E01018251,E01018252,E01018253,E01018254,E01018255,E01018256,E01018257,E01018258,E01018259,E01018260,E01018262,E01018263,E01018264,E01018265,E01018267,E01018268,E01018269,E01018270,E01018271,E01018272,E01018273,E01018274,E01018275,E01018276,E01018277,E01018278,E01018279,E01018280,E01018281,E01018282,E01018283,E01018284,E01018285,E01018286,E01018287,E01018288,E01018289,E01018290,E01018291,E01018292,E01018293,E01018294,E01018295,E01018296,E01018297,E01018298,E01018299,E01018300,E01018301,E01018302,E01018303,E01018304,E01018305,E01018306,E01018307,E01018308,E01018309,E01018310,E01018311,E01018312,E01018313,E01018314,E01018315,E01018316,E01018317,E01018319,E01018320,E01018321,E01018322,E01018323,E01018324,E01018325,E01018328,E01018330,E01018331,E01018332,E01018333,E01018334,E01018335,E01018336,E01018337,E01018338,E01018339,E01018340,E01018341,E01018342,E01018343,E01018344,E01018345,E01018346,E01018347,E01018349,E01018350,E01018351,E01018352,E01018353,E01018354,E01018355,E01018356,E01018357,E01018358,E01018359,E01018360,E01018361,E01018362,E01018363,E01018364,E01018365,E01018366,E01018367,E01018368,E01018369,E01018370,E01018371,E01018372,E01018373,E01018374,E01018375,E01018376,E01018377,E01018378,E01018379,E01018380,E01018381,E01018382,E01018383,E01018384,E01018385,E01018386,E01018387,E01018388,E01018389,E01018390,E01018391,E01018392,E01018395,E01018396,E01018397,E01018398,E01018399,E01018400,E01018401,E01018402,E01018403,E01018404,E01018405,E01018406,E01018407,E01018408,E01018409,E01018410,E01018411,E01018412,E01018413,E01018414,E01018415,E01018416,E01018417,E01018418,E01018419,E01018420,E01018421,E01018422,E01018423,E01018424,E01018425,E01018426,E01018427,E01018428,E01018429,E01018430,E01018431,E01018432,E01018433,E01018434,E01018435,E01018436,E01018437,E01018438,E01018439,E01018440,E01018441,E01018443,E01018445,E01018446,E01018447,E01018448,E01018449,E01018450,E01018451,E01018452,E01018454,E01018455&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_035.json.gz + content_sha256: 0473b6621f33b1e32877e6d78c25ed9080626371ce5fe2d25a625c128e2b98d7 +- id: census_lsoa_tenure5a_bedrooms_036 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 036 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01018456,E01018457,E01018458,E01018459,E01018460,E01018461,E01018462,E01018463,E01018464,E01018466,E01018468,E01018469,E01018470,E01018471,E01018472,E01018473,E01018474,E01018475,E01018476,E01018477,E01018478,E01018479,E01018481,E01018482,E01018483,E01018484,E01018485,E01018486,E01018487,E01018488,E01018489,E01018490,E01018491,E01018492,E01018493,E01018494,E01018495,E01018496,E01018497,E01018498,E01018499,E01018500,E01018501,E01018502,E01018503,E01018504,E01018505,E01018506,E01018507,E01018508,E01018509,E01018510,E01018511,E01018512,E01018513,E01018514,E01018515,E01018516,E01018518,E01018519,E01018520,E01018521,E01018522,E01018523,E01018524,E01018525,E01018526,E01018527,E01018528,E01018529,E01018530,E01018531,E01018532,E01018533,E01018534,E01018535,E01018536,E01018537,E01018538,E01018539,E01018540,E01018541,E01018542,E01018543,E01018544,E01018545,E01018546,E01018547,E01018548,E01018549,E01018550,E01018551,E01018552,E01018553,E01018554,E01018555,E01018556,E01018557,E01018558,E01018559,E01018560,E01018561,E01018562,E01018565,E01018566,E01018567,E01018568,E01018569,E01018570,E01018571,E01018572,E01018573,E01018574,E01018575,E01018576,E01018577,E01018578,E01018579,E01018580,E01018581,E01018582,E01018583,E01018584,E01018585,E01018586,E01018587,E01018588,E01018589,E01018590,E01018591,E01018592,E01018593,E01018594,E01018595,E01018596,E01018598,E01018599,E01018600,E01018601,E01018602,E01018603,E01018604,E01018605,E01018606,E01018607,E01018608,E01018609,E01018610,E01018611,E01018612,E01018613,E01018614,E01018615,E01018616,E01018617,E01018618,E01018619,E01018620,E01018621,E01018622,E01018623,E01018624,E01018625,E01018627,E01018628,E01018629,E01018630,E01018631,E01018632,E01018633,E01018634,E01018635,E01018636,E01018637,E01018638,E01018639,E01018640,E01018641,E01018642,E01018643,E01018644,E01018645,E01018646,E01018647,E01018648,E01018649,E01018650,E01018651,E01018652,E01018653,E01018654,E01018655,E01018657,E01018658,E01018659,E01018661,E01018662,E01018663,E01018664,E01018665,E01018666,E01018667,E01018668,E01018669,E01018670,E01018671,E01018672,E01018673,E01018674,E01018675,E01018676,E01018677,E01018678,E01018679,E01018680,E01018681,E01018682,E01018683,E01018684,E01018685,E01018686,E01018687,E01018688,E01018689,E01018690,E01018691,E01018692,E01018693,E01018694,E01018695,E01018696,E01018697,E01018698,E01018699,E01018700,E01018701,E01018702,E01018703,E01018704,E01018705,E01018706,E01018707,E01018708,E01018709,E01018710,E01018711,E01018712,E01018714,E01018715,E01018716,E01018717,E01018718,E01018719,E01018720,E01018721,E01018722,E01018723,E01018724,E01018725,E01018726,E01018727,E01018728,E01018729,E01018730,E01018731,E01018732,E01018733,E01018734,E01018735,E01018736,E01018737,E01018738,E01018739,E01018740,E01018741,E01018742,E01018743,E01018744,E01018745,E01018746,E01018747,E01018748,E01018749,E01018750,E01018751,E01018752,E01018753,E01018754,E01018755,E01018756,E01018757,E01018759,E01018760,E01018761,E01018762,E01018763,E01018764,E01018765,E01018766,E01018767,E01018768,E01018770,E01018771,E01018772,E01018773,E01018774,E01018777,E01018778,E01018779,E01018780,E01018781,E01018782,E01018783,E01018784,E01018785,E01018786,E01018787,E01018788,E01018789,E01018790,E01018791,E01018792,E01018793,E01018794,E01018795,E01018796,E01018797,E01018798,E01018799,E01018800,E01018803,E01018804,E01018805,E01018806,E01018807,E01018808,E01018810,E01018811,E01018812,E01018813,E01018814,E01018815,E01018816,E01018817,E01018818,E01018819,E01018820,E01018821,E01018822,E01018823,E01018824,E01018825,E01018826,E01018827,E01018828,E01018829,E01018830,E01018831,E01018832,E01018833,E01018834,E01018835,E01018836,E01018837,E01018838,E01018839,E01018840,E01018841,E01018842,E01018843,E01018844,E01018845,E01018846,E01018847,E01018848,E01018849,E01018850,E01018851,E01018853,E01018854,E01018856,E01018857,E01018858,E01018859,E01018860,E01018861,E01018862,E01018863,E01018864,E01018865,E01018866,E01018868,E01018869,E01018870,E01018871,E01018872,E01018873,E01018874,E01018875,E01018876,E01018877,E01018878,E01018879,E01018880,E01018881,E01018882,E01018883,E01018884,E01018885,E01018886,E01018887,E01018888,E01018889,E01018890,E01018891,E01018892,E01018893,E01018894,E01018895,E01018896,E01018897,E01018898,E01018899,E01018900,E01018901,E01018902,E01018903,E01018904,E01018905,E01018906,E01018907,E01018908,E01018909,E01018910,E01018911,E01018912,E01018913,E01018914,E01018915,E01018916,E01018917,E01018918,E01018919,E01018920,E01018921,E01018922,E01018923,E01018924,E01018925,E01018926,E01018927,E01018928,E01018929,E01018930,E01018931,E01018932,E01018933,E01018934,E01018935,E01018936,E01018937,E01018938,E01018940,E01018941,E01018942,E01018943,E01018944,E01018945,E01018946,E01018948,E01018949,E01018950,E01018951,E01018952,E01018953,E01018954,E01018955,E01018956,E01018957,E01018958,E01018959,E01018960,E01018961,E01018962,E01018965,E01018966,E01018967,E01018968,E01018969,E01018970,E01018971,E01018972,E01018973,E01018974,E01018975,E01018976,E01018977,E01018978,E01018979,E01018980&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_036.json.gz + content_sha256: 7dd4f617f9e0b3fda9109c9065fdba482cd3830361f5c985ee268b2f237b30ee +- id: census_lsoa_tenure5a_bedrooms_037 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 037 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01018981,E01018982,E01018983,E01018985,E01018986,E01018987,E01018988,E01018989,E01018990,E01018991,E01018992,E01018993,E01018994,E01018995,E01018996,E01018997,E01018998,E01018999,E01019000,E01019001,E01019002,E01019003,E01019004,E01019005,E01019006,E01019007,E01019008,E01019009,E01019010,E01019011,E01019012,E01019013,E01019014,E01019015,E01019016,E01019018,E01019019,E01019020,E01019021,E01019022,E01019023,E01019024,E01019025,E01019026,E01019027,E01019028,E01019029,E01019030,E01019031,E01019032,E01019033,E01019034,E01019035,E01019036,E01019037,E01019038,E01019039,E01019040,E01019041,E01019042,E01019043,E01019044,E01019045,E01019046,E01019047,E01019048,E01019050,E01019051,E01019052,E01019053,E01019054,E01019055,E01019056,E01019057,E01019058,E01019059,E01019060,E01019061,E01019062,E01019063,E01019064,E01019065,E01019066,E01019067,E01019068,E01019069,E01019070,E01019071,E01019072,E01019073,E01019074,E01019075,E01019076,E01019077,E01019078,E01019079,E01019080,E01019081,E01019082,E01019083,E01019084,E01019085,E01019086,E01019087,E01019088,E01019089,E01019090,E01019091,E01019092,E01019093,E01019094,E01019095,E01019096,E01019097,E01019098,E01019099,E01019100,E01019101,E01019102,E01019104,E01019106,E01019107,E01019108,E01019109,E01019110,E01019111,E01019112,E01019113,E01019114,E01019115,E01019116,E01019117,E01019118,E01019119,E01019120,E01019121,E01019122,E01019123,E01019124,E01019125,E01019126,E01019127,E01019128,E01019129,E01019130,E01019131,E01019132,E01019133,E01019134,E01019135,E01019136,E01019137,E01019138,E01019139,E01019141,E01019142,E01019143,E01019144,E01019145,E01019146,E01019147,E01019148,E01019149,E01019150,E01019151,E01019152,E01019153,E01019154,E01019155,E01019156,E01019157,E01019158,E01019159,E01019160,E01019161,E01019162,E01019163,E01019164,E01019165,E01019166,E01019167,E01019168,E01019169,E01019170,E01019171,E01019172,E01019173,E01019175,E01019176,E01019177,E01019178,E01019179,E01019180,E01019182,E01019184,E01019185,E01019186,E01019187,E01019188,E01019189,E01019190,E01019191,E01019192,E01019193,E01019194,E01019195,E01019196,E01019197,E01019198,E01019199,E01019200,E01019201,E01019202,E01019203,E01019204,E01019205,E01019206,E01019207,E01019208,E01019209,E01019210,E01019211,E01019212,E01019215,E01019216,E01019217,E01019218,E01019219,E01019220,E01019221,E01019222,E01019223,E01019224,E01019225,E01019226,E01019227,E01019228,E01019229,E01019230,E01019231,E01019232,E01019233,E01019234,E01019235,E01019236,E01019237,E01019238,E01019239,E01019240,E01019241,E01019242,E01019243,E01019244,E01019245,E01019246,E01019247,E01019248,E01019249,E01019250,E01019251,E01019252,E01019253,E01019254,E01019255,E01019256,E01019257,E01019258,E01019259,E01019260,E01019261,E01019262,E01019264,E01019265,E01019266,E01019267,E01019269,E01019270,E01019271,E01019272,E01019273,E01019274,E01019275,E01019277,E01019278,E01019279,E01019280,E01019281,E01019282,E01019283,E01019284,E01019285,E01019286,E01019287,E01019288,E01019289,E01019290,E01019291,E01019292,E01019293,E01019294,E01019295,E01019296,E01019298,E01019299,E01019300,E01019301,E01019302,E01019303,E01019304,E01019305,E01019306,E01019307,E01019308,E01019309,E01019310,E01019311,E01019312,E01019313,E01019314,E01019315,E01019316,E01019317,E01019318,E01019319,E01019320,E01019321,E01019322,E01019323,E01019324,E01019325,E01019326,E01019327,E01019328,E01019329,E01019330,E01019331,E01019332,E01019333,E01019334,E01019335,E01019336,E01019337,E01019338,E01019339,E01019340,E01019343,E01019344,E01019345,E01019346,E01019347,E01019348,E01019349,E01019350,E01019351,E01019352,E01019353,E01019354,E01019355,E01019356,E01019357,E01019358,E01019359,E01019360,E01019361,E01019362,E01019363,E01019364,E01019365,E01019366,E01019368,E01019369,E01019370,E01019371,E01019372,E01019373,E01019374,E01019375,E01019376,E01019377,E01019378,E01019379,E01019380,E01019381,E01019382,E01019383,E01019384,E01019385,E01019386,E01019387,E01019388,E01019389,E01019390,E01019391,E01019392,E01019393,E01019394,E01019395,E01019396,E01019397,E01019400,E01019401,E01019402,E01019403,E01019404,E01019405,E01019406,E01019407,E01019408,E01019409,E01019410,E01019411,E01019412,E01019413,E01019414,E01019415,E01019416,E01019417,E01019418,E01019419,E01019420,E01019421,E01019422,E01019423,E01019424,E01019425,E01019426,E01019427,E01019428,E01019429,E01019430,E01019431,E01019432,E01019433,E01019434,E01019435,E01019436,E01019437,E01019438,E01019439,E01019440,E01019441,E01019442,E01019443,E01019444,E01019445,E01019446,E01019447,E01019448,E01019449,E01019450,E01019451,E01019452,E01019453,E01019454,E01019455,E01019456,E01019457,E01019458,E01019459,E01019460,E01019461,E01019462,E01019463,E01019464,E01019465,E01019467,E01019468,E01019469,E01019470,E01019471,E01019473,E01019474,E01019475,E01019476,E01019477,E01019478,E01019479,E01019480,E01019481,E01019482,E01019483,E01019484,E01019485,E01019486,E01019487,E01019488,E01019489,E01019490,E01019491,E01019492,E01019493,E01019495,E01019496,E01019497,E01019498,E01019499,E01019500,E01019501,E01019502,E01019503&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_037.json.gz + content_sha256: f22929bfe86ffe13f66ce567b290b51a1e471466c3271864d42d0312ad6bdc19 +- id: census_lsoa_tenure5a_bedrooms_038 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 038 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01019504,E01019505,E01019506,E01019507,E01019508,E01019509,E01019510,E01019511,E01019513,E01019514,E01019515,E01019516,E01019517,E01019518,E01019519,E01019520,E01019521,E01019522,E01019523,E01019524,E01019525,E01019526,E01019527,E01019528,E01019529,E01019530,E01019531,E01019532,E01019533,E01019534,E01019535,E01019536,E01019537,E01019538,E01019539,E01019540,E01019541,E01019542,E01019543,E01019544,E01019545,E01019546,E01019547,E01019548,E01019549,E01019550,E01019551,E01019552,E01019553,E01019554,E01019555,E01019556,E01019557,E01019558,E01019559,E01019560,E01019561,E01019562,E01019563,E01019564,E01019565,E01019566,E01019567,E01019568,E01019569,E01019570,E01019571,E01019572,E01019573,E01019574,E01019575,E01019576,E01019577,E01019578,E01019579,E01019580,E01019581,E01019582,E01019583,E01019585,E01019586,E01019587,E01019588,E01019589,E01019590,E01019591,E01019592,E01019593,E01019594,E01019595,E01019596,E01019597,E01019598,E01019599,E01019600,E01019601,E01019602,E01019603,E01019604,E01019605,E01019606,E01019607,E01019608,E01019609,E01019610,E01019611,E01019612,E01019613,E01019614,E01019615,E01019616,E01019617,E01019618,E01019619,E01019620,E01019621,E01019622,E01019623,E01019624,E01019625,E01019626,E01019627,E01019628,E01019629,E01019630,E01019631,E01019632,E01019633,E01019634,E01019635,E01019636,E01019638,E01019639,E01019640,E01019641,E01019642,E01019643,E01019644,E01019645,E01019646,E01019647,E01019648,E01019649,E01019650,E01019651,E01019652,E01019653,E01019654,E01019655,E01019656,E01019657,E01019658,E01019659,E01019660,E01019661,E01019662,E01019663,E01019664,E01019665,E01019666,E01019667,E01019668,E01019669,E01019670,E01019671,E01019672,E01019673,E01019674,E01019675,E01019676,E01019677,E01019678,E01019679,E01019680,E01019681,E01019682,E01019683,E01019684,E01019685,E01019686,E01019687,E01019688,E01019689,E01019690,E01019691,E01019692,E01019693,E01019694,E01019695,E01019696,E01019697,E01019698,E01019699,E01019700,E01019701,E01019702,E01019703,E01019704,E01019705,E01019706,E01019707,E01019708,E01019709,E01019710,E01019711,E01019712,E01019713,E01019714,E01019715,E01019716,E01019717,E01019718,E01019719,E01019720,E01019721,E01019722,E01019723,E01019724,E01019725,E01019726,E01019727,E01019728,E01019729,E01019730,E01019731,E01019732,E01019733,E01019734,E01019735,E01019736,E01019737,E01019738,E01019739,E01019740,E01019741,E01019742,E01019743,E01019744,E01019745,E01019746,E01019747,E01019748,E01019749,E01019750,E01019751,E01019752,E01019753,E01019754,E01019755,E01019756,E01019757,E01019758,E01019759,E01019760,E01019761,E01019762,E01019763,E01019764,E01019765,E01019766,E01019767,E01019768,E01019769,E01019770,E01019771,E01019772,E01019774,E01019775,E01019776,E01019777,E01019778,E01019779,E01019780,E01019781,E01019782,E01019783,E01019784,E01019785,E01019786,E01019787,E01019788,E01019790,E01019791,E01019792,E01019793,E01019794,E01019795,E01019796,E01019797,E01019798,E01019799,E01019800,E01019801,E01019802,E01019803,E01019804,E01019805,E01019806,E01019807,E01019808,E01019809,E01019810,E01019811,E01019812,E01019813,E01019814,E01019815,E01019816,E01019817,E01019818,E01019819,E01019820,E01019821,E01019822,E01019823,E01019824,E01019825,E01019826,E01019827,E01019828,E01019829,E01019830,E01019831,E01019832,E01019833,E01019834,E01019837,E01019838,E01019839,E01019840,E01019841,E01019842,E01019843,E01019844,E01019845,E01019846,E01019847,E01019848,E01019850,E01019851,E01019852,E01019853,E01019854,E01019855,E01019856,E01019857,E01019858,E01019859,E01019860,E01019861,E01019862,E01019863,E01019864,E01019865,E01019866,E01019867,E01019868,E01019869,E01019870,E01019871,E01019872,E01019873,E01019874,E01019875,E01019876,E01019877,E01019878,E01019879,E01019880,E01019882,E01019885,E01019887,E01019888,E01019889,E01019890,E01019891,E01019894,E01019895,E01019896,E01019897,E01019898,E01019899,E01019900,E01019901,E01019902,E01019903,E01019904,E01019905,E01019906,E01019907,E01019908,E01019909,E01019910,E01019911,E01019912,E01019913,E01019914,E01019915,E01019916,E01019917,E01019918,E01019919,E01019920,E01019921,E01019922,E01019923,E01019924,E01019925,E01019927,E01019928,E01019929,E01019930,E01019931,E01019932,E01019933,E01019934,E01019935,E01019936,E01019937,E01019938,E01019939,E01019940,E01019942,E01019943,E01019944,E01019945,E01019946,E01019947,E01019948,E01019949,E01019950,E01019951,E01019952,E01019953,E01019954,E01019955,E01019956,E01019957,E01019958,E01019959,E01019960,E01019961,E01019962,E01019963,E01019964,E01019965,E01019966,E01019967,E01019968,E01019969,E01019970,E01019971,E01019972,E01019973,E01019974,E01019975,E01019976,E01019977,E01019980,E01019981,E01019982,E01019983,E01019984,E01019985,E01019986,E01019987,E01019988,E01019989,E01019990,E01019991,E01019992,E01019993,E01019994,E01019995,E01019996,E01019997,E01019998,E01019999,E01020000,E01020001,E01020002,E01020003,E01020004,E01020005,E01020006,E01020007,E01020008,E01020009,E01020010,E01020011,E01020012,E01020013,E01020014,E01020015,E01020017,E01020018,E01020019,E01020020,E01020021,E01020022&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_038.json.gz + content_sha256: 03b5c98a1fcfed82f84aad564392c4d0efb72a86c3235a33a5ec3c493c5d4ece +- id: census_lsoa_tenure5a_bedrooms_039 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 039 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01020023,E01020024,E01020025,E01020026,E01020027,E01020029,E01020030,E01020031,E01020032,E01020036,E01020037,E01020038,E01020039,E01020040,E01020041,E01020042,E01020043,E01020044,E01020045,E01020046,E01020047,E01020048,E01020049,E01020050,E01020051,E01020052,E01020053,E01020054,E01020055,E01020056,E01020057,E01020058,E01020059,E01020060,E01020061,E01020062,E01020063,E01020064,E01020065,E01020067,E01020068,E01020069,E01020070,E01020071,E01020072,E01020073,E01020074,E01020075,E01020076,E01020077,E01020078,E01020079,E01020080,E01020081,E01020082,E01020083,E01020084,E01020085,E01020087,E01020088,E01020089,E01020090,E01020091,E01020092,E01020093,E01020094,E01020095,E01020096,E01020097,E01020098,E01020099,E01020100,E01020101,E01020102,E01020103,E01020104,E01020105,E01020106,E01020107,E01020108,E01020109,E01020110,E01020111,E01020112,E01020113,E01020114,E01020115,E01020116,E01020117,E01020118,E01020119,E01020120,E01020121,E01020122,E01020123,E01020124,E01020125,E01020126,E01020127,E01020128,E01020129,E01020130,E01020131,E01020132,E01020133,E01020134,E01020135,E01020136,E01020137,E01020138,E01020139,E01020140,E01020141,E01020142,E01020143,E01020144,E01020145,E01020146,E01020147,E01020148,E01020149,E01020150,E01020151,E01020152,E01020153,E01020154,E01020155,E01020156,E01020157,E01020158,E01020159,E01020160,E01020161,E01020162,E01020163,E01020164,E01020165,E01020166,E01020167,E01020168,E01020169,E01020170,E01020171,E01020172,E01020173,E01020174,E01020175,E01020176,E01020177,E01020178,E01020179,E01020180,E01020181,E01020182,E01020183,E01020184,E01020185,E01020186,E01020187,E01020188,E01020189,E01020190,E01020191,E01020192,E01020193,E01020194,E01020195,E01020196,E01020197,E01020198,E01020199,E01020200,E01020201,E01020202,E01020203,E01020204,E01020205,E01020206,E01020208,E01020209,E01020210,E01020211,E01020212,E01020213,E01020214,E01020215,E01020216,E01020217,E01020218,E01020219,E01020220,E01020221,E01020222,E01020223,E01020224,E01020225,E01020226,E01020227,E01020228,E01020230,E01020231,E01020232,E01020233,E01020234,E01020235,E01020236,E01020237,E01020238,E01020239,E01020240,E01020241,E01020242,E01020243,E01020244,E01020245,E01020246,E01020247,E01020248,E01020249,E01020250,E01020251,E01020252,E01020253,E01020254,E01020255,E01020256,E01020257,E01020258,E01020259,E01020260,E01020261,E01020262,E01020263,E01020264,E01020265,E01020266,E01020267,E01020268,E01020269,E01020270,E01020271,E01020272,E01020273,E01020274,E01020275,E01020276,E01020277,E01020278,E01020279,E01020280,E01020281,E01020282,E01020283,E01020284,E01020285,E01020286,E01020287,E01020288,E01020289,E01020290,E01020291,E01020292,E01020293,E01020294,E01020295,E01020296,E01020297,E01020298,E01020299,E01020300,E01020301,E01020302,E01020303,E01020304,E01020305,E01020306,E01020307,E01020308,E01020309,E01020311,E01020312,E01020313,E01020314,E01020315,E01020316,E01020317,E01020318,E01020319,E01020320,E01020321,E01020323,E01020324,E01020325,E01020326,E01020327,E01020328,E01020329,E01020330,E01020331,E01020332,E01020333,E01020334,E01020335,E01020336,E01020337,E01020338,E01020339,E01020340,E01020341,E01020342,E01020343,E01020344,E01020345,E01020346,E01020347,E01020348,E01020349,E01020350,E01020351,E01020352,E01020353,E01020354,E01020355,E01020356,E01020357,E01020358,E01020359,E01020360,E01020361,E01020362,E01020363,E01020364,E01020365,E01020366,E01020367,E01020369,E01020370,E01020371,E01020372,E01020373,E01020374,E01020375,E01020376,E01020377,E01020378,E01020379,E01020380,E01020381,E01020382,E01020383,E01020384,E01020385,E01020386,E01020387,E01020388,E01020389,E01020390,E01020391,E01020392,E01020393,E01020394,E01020395,E01020396,E01020397,E01020398,E01020400,E01020401,E01020402,E01020403,E01020404,E01020405,E01020406,E01020407,E01020408,E01020409,E01020410,E01020411,E01020412,E01020413,E01020414,E01020415,E01020416,E01020417,E01020418,E01020419,E01020420,E01020421,E01020422,E01020423,E01020424,E01020425,E01020426,E01020427,E01020428,E01020429,E01020430,E01020431,E01020432,E01020433,E01020434,E01020435,E01020436,E01020437,E01020438,E01020439,E01020440,E01020441,E01020442,E01020443,E01020444,E01020445,E01020446,E01020447,E01020448,E01020449,E01020450,E01020452,E01020453,E01020454,E01020456,E01020457,E01020458,E01020460,E01020462,E01020463,E01020464,E01020465,E01020466,E01020467,E01020468,E01020469,E01020470,E01020471,E01020472,E01020473,E01020474,E01020475,E01020476,E01020477,E01020478,E01020479,E01020480,E01020481,E01020482,E01020483,E01020484,E01020485,E01020486,E01020487,E01020488,E01020489,E01020490,E01020491,E01020492,E01020493,E01020494,E01020495,E01020496,E01020497,E01020498,E01020499,E01020500,E01020501,E01020502,E01020503,E01020504,E01020505,E01020506,E01020507,E01020508,E01020509,E01020510,E01020511,E01020512,E01020513,E01020514,E01020515,E01020516,E01020517,E01020518,E01020519,E01020520,E01020523,E01020524,E01020525,E01020526,E01020527,E01020528,E01020529,E01020530,E01020531,E01020532,E01020533,E01020534,E01020535,E01020536,E01020537,E01020538,E01020539,E01020540&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_039.json.gz + content_sha256: 531f6983a6f07c6a9f956d457a99a8c0c8171469887d4180e5d2b37a863e249c +- id: census_lsoa_tenure5a_bedrooms_040 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 040 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01020541,E01020542,E01020543,E01020544,E01020546,E01020547,E01020549,E01020550,E01020551,E01020552,E01020553,E01020554,E01020555,E01020556,E01020557,E01020558,E01020559,E01020560,E01020561,E01020562,E01020563,E01020564,E01020565,E01020566,E01020569,E01020570,E01020571,E01020572,E01020573,E01020574,E01020575,E01020576,E01020577,E01020578,E01020579,E01020580,E01020581,E01020582,E01020583,E01020584,E01020585,E01020586,E01020587,E01020588,E01020589,E01020590,E01020591,E01020592,E01020593,E01020594,E01020595,E01020596,E01020597,E01020598,E01020599,E01020600,E01020601,E01020602,E01020603,E01020604,E01020605,E01020606,E01020607,E01020608,E01020609,E01020610,E01020611,E01020612,E01020613,E01020614,E01020615,E01020616,E01020617,E01020618,E01020619,E01020620,E01020621,E01020622,E01020623,E01020624,E01020625,E01020626,E01020627,E01020628,E01020629,E01020630,E01020631,E01020632,E01020633,E01020634,E01020635,E01020636,E01020637,E01020638,E01020639,E01020640,E01020641,E01020642,E01020643,E01020645,E01020646,E01020647,E01020648,E01020649,E01020650,E01020651,E01020652,E01020653,E01020654,E01020655,E01020656,E01020657,E01020658,E01020659,E01020660,E01020661,E01020662,E01020663,E01020664,E01020665,E01020666,E01020667,E01020668,E01020669,E01020670,E01020671,E01020672,E01020673,E01020674,E01020675,E01020676,E01020677,E01020678,E01020679,E01020680,E01020681,E01020682,E01020683,E01020685,E01020686,E01020687,E01020688,E01020689,E01020690,E01020691,E01020692,E01020693,E01020694,E01020695,E01020696,E01020697,E01020698,E01020699,E01020700,E01020701,E01020702,E01020703,E01020704,E01020705,E01020706,E01020708,E01020709,E01020710,E01020711,E01020712,E01020714,E01020716,E01020717,E01020718,E01020719,E01020720,E01020721,E01020722,E01020723,E01020724,E01020725,E01020726,E01020727,E01020729,E01020730,E01020731,E01020732,E01020733,E01020734,E01020735,E01020737,E01020739,E01020740,E01020741,E01020742,E01020743,E01020744,E01020745,E01020746,E01020747,E01020748,E01020749,E01020750,E01020751,E01020752,E01020753,E01020754,E01020756,E01020757,E01020758,E01020759,E01020760,E01020761,E01020762,E01020763,E01020764,E01020765,E01020766,E01020767,E01020768,E01020769,E01020770,E01020771,E01020772,E01020773,E01020775,E01020776,E01020777,E01020778,E01020779,E01020780,E01020782,E01020783,E01020784,E01020786,E01020787,E01020788,E01020789,E01020790,E01020791,E01020792,E01020793,E01020794,E01020795,E01020796,E01020797,E01020798,E01020799,E01020801,E01020802,E01020805,E01020806,E01020807,E01020808,E01020809,E01020810,E01020811,E01020812,E01020813,E01020814,E01020815,E01020816,E01020817,E01020818,E01020819,E01020820,E01020821,E01020822,E01020823,E01020824,E01020825,E01020826,E01020827,E01020828,E01020829,E01020830,E01020831,E01020832,E01020833,E01020834,E01020835,E01020837,E01020838,E01020839,E01020840,E01020841,E01020843,E01020844,E01020845,E01020846,E01020847,E01020848,E01020849,E01020850,E01020851,E01020852,E01020853,E01020854,E01020855,E01020856,E01020857,E01020858,E01020859,E01020860,E01020861,E01020862,E01020863,E01020864,E01020867,E01020868,E01020869,E01020870,E01020871,E01020872,E01020873,E01020874,E01020875,E01020876,E01020877,E01020878,E01020879,E01020880,E01020881,E01020882,E01020883,E01020884,E01020885,E01020886,E01020887,E01020888,E01020889,E01020890,E01020891,E01020892,E01020893,E01020894,E01020895,E01020896,E01020898,E01020899,E01020900,E01020901,E01020902,E01020903,E01020904,E01020905,E01020906,E01020907,E01020908,E01020909,E01020910,E01020911,E01020912,E01020913,E01020914,E01020915,E01020916,E01020917,E01020918,E01020919,E01020920,E01020921,E01020922,E01020923,E01020924,E01020925,E01020926,E01020927,E01020928,E01020929,E01020930,E01020931,E01020932,E01020935,E01020936,E01020937,E01020938,E01020939,E01020940,E01020941,E01020942,E01020943,E01020944,E01020945,E01020946,E01020947,E01020948,E01020949,E01020950,E01020951,E01020952,E01020953,E01020954,E01020955,E01020956,E01020957,E01020959,E01020960,E01020961,E01020962,E01020963,E01020964,E01020965,E01020966,E01020967,E01020968,E01020969,E01020970,E01020971,E01020972,E01020973,E01020974,E01020975,E01020976,E01020977,E01020978,E01020979,E01020980,E01020981,E01020982,E01020983,E01020984,E01020985,E01020986,E01020987,E01020988,E01020989,E01020990,E01020991,E01020992,E01020993,E01020994,E01020995,E01020996,E01020997,E01020998,E01020999,E01021000,E01021001,E01021002,E01021003,E01021004,E01021005,E01021006,E01021007,E01021008,E01021009,E01021010,E01021011,E01021012,E01021013,E01021014,E01021015,E01021016,E01021017,E01021018,E01021019,E01021020,E01021021,E01021022,E01021023,E01021024,E01021025,E01021026,E01021027,E01021028,E01021029,E01021030,E01021031,E01021032,E01021033,E01021034,E01021035,E01021036,E01021037,E01021038,E01021039,E01021040,E01021041,E01021042,E01021043,E01021044,E01021045,E01021046,E01021047,E01021048,E01021049,E01021050,E01021051,E01021052,E01021053,E01021054,E01021055,E01021056,E01021057,E01021058,E01021059,E01021060,E01021061,E01021062,E01021063,E01021064,E01021065,E01021066,E01021067&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_040.json.gz + content_sha256: f0faf625f73a08164b4c9204785c5477af3f6c88bea5a35ffb9f182451bcce7f +- id: census_lsoa_tenure5a_bedrooms_041 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 041 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01021068,E01021069,E01021070,E01021071,E01021072,E01021073,E01021074,E01021075,E01021076,E01021077,E01021078,E01021079,E01021080,E01021081,E01021082,E01021083,E01021084,E01021085,E01021086,E01021087,E01021088,E01021089,E01021090,E01021091,E01021092,E01021093,E01021094,E01021095,E01021096,E01021097,E01021098,E01021099,E01021100,E01021101,E01021102,E01021103,E01021104,E01021105,E01021106,E01021107,E01021108,E01021109,E01021110,E01021111,E01021112,E01021113,E01021114,E01021115,E01021116,E01021117,E01021118,E01021119,E01021120,E01021121,E01021122,E01021123,E01021124,E01021125,E01021126,E01021127,E01021128,E01021129,E01021130,E01021131,E01021132,E01021133,E01021134,E01021135,E01021136,E01021137,E01021138,E01021139,E01021140,E01021141,E01021142,E01021143,E01021144,E01021145,E01021146,E01021147,E01021148,E01021149,E01021150,E01021151,E01021152,E01021153,E01021154,E01021155,E01021156,E01021157,E01021158,E01021159,E01021160,E01021161,E01021162,E01021163,E01021164,E01021165,E01021166,E01021167,E01021168,E01021169,E01021170,E01021171,E01021172,E01021173,E01021174,E01021175,E01021176,E01021177,E01021178,E01021179,E01021180,E01021181,E01021182,E01021183,E01021184,E01021185,E01021186,E01021187,E01021188,E01021189,E01021190,E01021191,E01021192,E01021193,E01021194,E01021196,E01021198,E01021199,E01021200,E01021201,E01021202,E01021203,E01021204,E01021205,E01021206,E01021207,E01021208,E01021209,E01021210,E01021211,E01021213,E01021214,E01021215,E01021216,E01021217,E01021218,E01021219,E01021220,E01021221,E01021222,E01021223,E01021224,E01021225,E01021226,E01021227,E01021228,E01021229,E01021230,E01021231,E01021232,E01021233,E01021234,E01021235,E01021236,E01021237,E01021238,E01021239,E01021240,E01021241,E01021242,E01021243,E01021244,E01021245,E01021246,E01021247,E01021248,E01021249,E01021250,E01021251,E01021252,E01021253,E01021254,E01021255,E01021256,E01021257,E01021258,E01021259,E01021260,E01021261,E01021262,E01021263,E01021264,E01021265,E01021266,E01021267,E01021268,E01021269,E01021270,E01021271,E01021272,E01021273,E01021274,E01021275,E01021276,E01021277,E01021279,E01021280,E01021281,E01021282,E01021283,E01021284,E01021285,E01021286,E01021287,E01021288,E01021289,E01021290,E01021291,E01021292,E01021293,E01021294,E01021295,E01021297,E01021298,E01021299,E01021300,E01021301,E01021302,E01021303,E01021304,E01021305,E01021306,E01021307,E01021308,E01021309,E01021310,E01021311,E01021312,E01021313,E01021314,E01021315,E01021316,E01021317,E01021318,E01021319,E01021320,E01021321,E01021322,E01021323,E01021324,E01021325,E01021326,E01021327,E01021328,E01021329,E01021330,E01021331,E01021332,E01021333,E01021334,E01021335,E01021336,E01021337,E01021338,E01021339,E01021340,E01021341,E01021342,E01021343,E01021344,E01021345,E01021346,E01021347,E01021348,E01021349,E01021350,E01021351,E01021352,E01021353,E01021354,E01021355,E01021356,E01021357,E01021358,E01021359,E01021360,E01021361,E01021362,E01021364,E01021365,E01021366,E01021367,E01021368,E01021369,E01021370,E01021371,E01021372,E01021374,E01021375,E01021376,E01021377,E01021378,E01021379,E01021380,E01021381,E01021382,E01021383,E01021384,E01021385,E01021386,E01021387,E01021388,E01021389,E01021390,E01021391,E01021392,E01021393,E01021394,E01021395,E01021396,E01021397,E01021398,E01021399,E01021400,E01021401,E01021402,E01021403,E01021404,E01021405,E01021406,E01021407,E01021408,E01021409,E01021410,E01021411,E01021412,E01021413,E01021414,E01021415,E01021416,E01021417,E01021418,E01021419,E01021420,E01021422,E01021423,E01021424,E01021425,E01021426,E01021427,E01021428,E01021429,E01021430,E01021431,E01021432,E01021433,E01021434,E01021435,E01021437,E01021438,E01021441,E01021442,E01021443,E01021444,E01021445,E01021446,E01021447,E01021448,E01021449,E01021450,E01021451,E01021452,E01021453,E01021454,E01021455,E01021456,E01021457,E01021458,E01021459,E01021460,E01021461,E01021462,E01021463,E01021464,E01021465,E01021466,E01021467,E01021468,E01021469,E01021470,E01021471,E01021472,E01021473,E01021474,E01021475,E01021476,E01021477,E01021478,E01021479,E01021480,E01021481,E01021482,E01021483,E01021484,E01021485,E01021486,E01021487,E01021488,E01021489,E01021490,E01021491,E01021492,E01021493,E01021494,E01021495,E01021496,E01021497,E01021498,E01021499,E01021500,E01021501,E01021502,E01021503,E01021504,E01021505,E01021506,E01021507,E01021508,E01021509,E01021510,E01021511,E01021512,E01021513,E01021514,E01021515,E01021516,E01021517,E01021518,E01021519,E01021520,E01021521,E01021522,E01021523,E01021524,E01021525,E01021526,E01021527,E01021528,E01021529,E01021530,E01021531,E01021532,E01021533,E01021535,E01021536,E01021537,E01021538,E01021539,E01021540,E01021542,E01021543,E01021544,E01021545,E01021546,E01021547,E01021548,E01021550,E01021551,E01021552,E01021553,E01021554,E01021555,E01021556,E01021557,E01021558,E01021559,E01021560,E01021561,E01021562,E01021563,E01021564,E01021565,E01021566,E01021568,E01021569,E01021570,E01021571,E01021572,E01021573,E01021575,E01021576,E01021577,E01021578,E01021579,E01021580,E01021582,E01021583,E01021584&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_041.json.gz + content_sha256: 22a5cb7d451e2a391d832b5783d1135e7570d5afe2c4229c938d0a197de5f139 +- id: census_lsoa_tenure5a_bedrooms_042 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 042 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01021585,E01021586,E01021587,E01021588,E01021589,E01021590,E01021591,E01021592,E01021593,E01021595,E01021596,E01021597,E01021598,E01021599,E01021600,E01021601,E01021602,E01021604,E01021605,E01021606,E01021607,E01021608,E01021609,E01021610,E01021611,E01021612,E01021613,E01021614,E01021615,E01021617,E01021618,E01021619,E01021620,E01021621,E01021622,E01021623,E01021624,E01021625,E01021626,E01021627,E01021628,E01021629,E01021631,E01021632,E01021633,E01021634,E01021635,E01021636,E01021637,E01021638,E01021639,E01021640,E01021641,E01021642,E01021643,E01021644,E01021645,E01021646,E01021647,E01021648,E01021650,E01021651,E01021652,E01021653,E01021655,E01021656,E01021657,E01021658,E01021659,E01021660,E01021661,E01021662,E01021663,E01021664,E01021665,E01021666,E01021667,E01021668,E01021669,E01021670,E01021671,E01021673,E01021674,E01021675,E01021676,E01021677,E01021678,E01021683,E01021684,E01021687,E01021688,E01021689,E01021690,E01021691,E01021692,E01021693,E01021694,E01021695,E01021696,E01021697,E01021698,E01021699,E01021700,E01021702,E01021703,E01021704,E01021705,E01021706,E01021707,E01021708,E01021709,E01021710,E01021711,E01021713,E01021714,E01021715,E01021717,E01021718,E01021719,E01021720,E01021721,E01021722,E01021723,E01021724,E01021725,E01021726,E01021727,E01021728,E01021729,E01021730,E01021731,E01021732,E01021733,E01021734,E01021735,E01021736,E01021737,E01021738,E01021739,E01021740,E01021741,E01021742,E01021743,E01021744,E01021745,E01021746,E01021747,E01021748,E01021749,E01021750,E01021751,E01021752,E01021753,E01021754,E01021755,E01021756,E01021757,E01021758,E01021759,E01021760,E01021761,E01021762,E01021763,E01021764,E01021765,E01021766,E01021767,E01021768,E01021769,E01021770,E01021771,E01021772,E01021773,E01021774,E01021775,E01021776,E01021777,E01021778,E01021779,E01021780,E01021781,E01021782,E01021783,E01021784,E01021785,E01021786,E01021787,E01021788,E01021789,E01021790,E01021791,E01021792,E01021793,E01021794,E01021795,E01021796,E01021797,E01021798,E01021799,E01021800,E01021801,E01021802,E01021803,E01021804,E01021805,E01021806,E01021807,E01021808,E01021809,E01021810,E01021811,E01021812,E01021813,E01021814,E01021815,E01021816,E01021817,E01021818,E01021819,E01021820,E01021821,E01021822,E01021823,E01021824,E01021825,E01021826,E01021827,E01021828,E01021829,E01021830,E01021831,E01021832,E01021833,E01021834,E01021835,E01021836,E01021837,E01021838,E01021839,E01021840,E01021841,E01021842,E01021843,E01021844,E01021845,E01021846,E01021847,E01021848,E01021849,E01021850,E01021851,E01021852,E01021853,E01021854,E01021855,E01021856,E01021858,E01021859,E01021860,E01021861,E01021862,E01021863,E01021864,E01021865,E01021866,E01021867,E01021868,E01021869,E01021870,E01021871,E01021873,E01021874,E01021875,E01021878,E01021879,E01021880,E01021881,E01021882,E01021883,E01021884,E01021885,E01021886,E01021887,E01021888,E01021889,E01021890,E01021891,E01021892,E01021893,E01021894,E01021895,E01021896,E01021897,E01021898,E01021899,E01021900,E01021901,E01021902,E01021903,E01021904,E01021905,E01021906,E01021907,E01021908,E01021909,E01021910,E01021911,E01021912,E01021913,E01021914,E01021915,E01021916,E01021917,E01021918,E01021919,E01021920,E01021921,E01021922,E01021923,E01021924,E01021925,E01021926,E01021927,E01021928,E01021929,E01021930,E01021931,E01021932,E01021933,E01021934,E01021935,E01021936,E01021937,E01021938,E01021939,E01021940,E01021941,E01021942,E01021943,E01021944,E01021945,E01021946,E01021947,E01021948,E01021949,E01021950,E01021951,E01021952,E01021953,E01021954,E01021955,E01021956,E01021957,E01021958,E01021959,E01021960,E01021961,E01021962,E01021963,E01021964,E01021965,E01021966,E01021967,E01021968,E01021969,E01021970,E01021971,E01021972,E01021973,E01021974,E01021975,E01021976,E01021977,E01021978,E01021979,E01021980,E01021981,E01021982,E01021983,E01021984,E01021985,E01021986,E01021987,E01021988,E01021989,E01021990,E01021991,E01021992,E01021993,E01021994,E01021995,E01021996,E01021997,E01021998,E01021999,E01022000,E01022001,E01022002,E01022004,E01022005,E01022006,E01022007,E01022008,E01022009,E01022010,E01022011,E01022012,E01022013,E01022014,E01022015,E01022016,E01022017,E01022018,E01022019,E01022020,E01022021,E01022022,E01022023,E01022024,E01022025,E01022026,E01022027,E01022028,E01022030,E01022031,E01022032,E01022033,E01022034,E01022035,E01022036,E01022037,E01022038,E01022039,E01022040,E01022041,E01022042,E01022043,E01022044,E01022045,E01022046,E01022047,E01022048,E01022049,E01022050,E01022051,E01022052,E01022053,E01022054,E01022055,E01022056,E01022057,E01022058,E01022059,E01022060,E01022061,E01022062,E01022063,E01022064,E01022067,E01022068,E01022069,E01022070,E01022071,E01022072,E01022073,E01022074,E01022075,E01022076,E01022077,E01022078,E01022079,E01022080,E01022081,E01022082,E01022083,E01022084,E01022085,E01022086,E01022087,E01022088,E01022089,E01022092,E01022093,E01022094,E01022095,E01022096,E01022097,E01022098,E01022099,E01022100,E01022101,E01022102,E01022104,E01022105,E01022106,E01022107,E01022108,E01022109,E01022110,E01022111&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_042.json.gz + content_sha256: 0fd5cbe06f11236f70e81e4ba0aee77e275b9af226dc0da175b9fb99196e383c +- id: census_lsoa_tenure5a_bedrooms_043 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 043 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01022112,E01022113,E01022114,E01022115,E01022116,E01022117,E01022118,E01022119,E01022120,E01022121,E01022122,E01022123,E01022124,E01022125,E01022126,E01022127,E01022128,E01022129,E01022130,E01022131,E01022132,E01022133,E01022134,E01022135,E01022136,E01022137,E01022138,E01022139,E01022140,E01022141,E01022142,E01022143,E01022144,E01022145,E01022146,E01022147,E01022148,E01022149,E01022150,E01022151,E01022152,E01022153,E01022154,E01022155,E01022156,E01022157,E01022159,E01022160,E01022161,E01022162,E01022163,E01022164,E01022165,E01022166,E01022167,E01022168,E01022169,E01022170,E01022171,E01022172,E01022173,E01022174,E01022175,E01022176,E01022177,E01022178,E01022179,E01022181,E01022182,E01022183,E01022184,E01022185,E01022186,E01022187,E01022188,E01022189,E01022190,E01022191,E01022192,E01022193,E01022194,E01022195,E01022196,E01022197,E01022198,E01022199,E01022200,E01022201,E01022202,E01022203,E01022204,E01022205,E01022207,E01022208,E01022209,E01022210,E01022211,E01022213,E01022215,E01022216,E01022217,E01022218,E01022219,E01022220,E01022221,E01022222,E01022224,E01022225,E01022226,E01022227,E01022228,E01022229,E01022230,E01022231,E01022232,E01022233,E01022234,E01022235,E01022236,E01022237,E01022238,E01022239,E01022240,E01022241,E01022242,E01022243,E01022244,E01022245,E01022246,E01022247,E01022248,E01022249,E01022250,E01022251,E01022252,E01022253,E01022254,E01022255,E01022256,E01022257,E01022258,E01022259,E01022260,E01022261,E01022262,E01022263,E01022264,E01022265,E01022266,E01022267,E01022268,E01022269,E01022270,E01022271,E01022272,E01022273,E01022274,E01022275,E01022276,E01022277,E01022278,E01022279,E01022280,E01022281,E01022282,E01022283,E01022284,E01022285,E01022286,E01022287,E01022288,E01022289,E01022290,E01022291,E01022292,E01022293,E01022294,E01022295,E01022296,E01022297,E01022298,E01022299,E01022300,E01022301,E01022302,E01022303,E01022304,E01022305,E01022306,E01022307,E01022308,E01022309,E01022310,E01022311,E01022312,E01022313,E01022314,E01022315,E01022316,E01022317,E01022318,E01022319,E01022320,E01022321,E01022322,E01022323,E01022324,E01022325,E01022326,E01022327,E01022328,E01022329,E01022330,E01022331,E01022332,E01022333,E01022334,E01022335,E01022336,E01022338,E01022339,E01022340,E01022341,E01022342,E01022343,E01022344,E01022345,E01022346,E01022350,E01022351,E01022352,E01022353,E01022354,E01022355,E01022356,E01022357,E01022358,E01022359,E01022360,E01022361,E01022362,E01022363,E01022364,E01022366,E01022367,E01022368,E01022369,E01022370,E01022371,E01022372,E01022373,E01022374,E01022375,E01022376,E01022377,E01022378,E01022379,E01022381,E01022382,E01022383,E01022384,E01022385,E01022386,E01022387,E01022388,E01022389,E01022390,E01022391,E01022392,E01022393,E01022394,E01022395,E01022396,E01022397,E01022398,E01022399,E01022400,E01022401,E01022402,E01022403,E01022404,E01022405,E01022406,E01022407,E01022408,E01022409,E01022410,E01022411,E01022413,E01022414,E01022415,E01022416,E01022417,E01022418,E01022421,E01022423,E01022424,E01022425,E01022426,E01022427,E01022428,E01022429,E01022430,E01022431,E01022432,E01022433,E01022434,E01022437,E01022438,E01022439,E01022440,E01022441,E01022442,E01022443,E01022444,E01022445,E01022447,E01022448,E01022449,E01022450,E01022451,E01022452,E01022453,E01022454,E01022455,E01022456,E01022457,E01022458,E01022459,E01022460,E01022461,E01022462,E01022463,E01022464,E01022465,E01022466,E01022467,E01022468,E01022469,E01022470,E01022471,E01022472,E01022473,E01022474,E01022475,E01022476,E01022477,E01022478,E01022479,E01022480,E01022481,E01022482,E01022484,E01022485,E01022486,E01022487,E01022488,E01022489,E01022490,E01022491,E01022492,E01022493,E01022494,E01022495,E01022497,E01022498,E01022499,E01022500,E01022502,E01022503,E01022504,E01022505,E01022506,E01022507,E01022509,E01022510,E01022511,E01022512,E01022513,E01022514,E01022515,E01022516,E01022517,E01022518,E01022519,E01022520,E01022521,E01022522,E01022523,E01022524,E01022525,E01022527,E01022528,E01022529,E01022530,E01022531,E01022532,E01022533,E01022534,E01022535,E01022536,E01022537,E01022539,E01022540,E01022542,E01022543,E01022547,E01022548,E01022549,E01022550,E01022551,E01022552,E01022553,E01022554,E01022555,E01022556,E01022557,E01022558,E01022559,E01022560,E01022561,E01022562,E01022563,E01022564,E01022565,E01022566,E01022567,E01022568,E01022569,E01022570,E01022571,E01022572,E01022573,E01022574,E01022575,E01022576,E01022577,E01022579,E01022580,E01022581,E01022582,E01022583,E01022584,E01022585,E01022586,E01022587,E01022588,E01022589,E01022590,E01022591,E01022592,E01022593,E01022594,E01022595,E01022596,E01022597,E01022599,E01022600,E01022601,E01022602,E01022603,E01022604,E01022605,E01022606,E01022607,E01022608,E01022610,E01022611,E01022613,E01022614,E01022615,E01022616,E01022617,E01022618,E01022619,E01022620,E01022621,E01022622,E01022623,E01022624,E01022625,E01022627,E01022628,E01022629,E01022630,E01022631,E01022632,E01022633,E01022634,E01022635,E01022636,E01022639,E01022640,E01022641,E01022642,E01022643,E01022644,E01022645,E01022646,E01022647&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_043.json.gz + content_sha256: 85496d2afd977e20c5df255fefc7583af1f62228ac8156c6dad975de9c425c01 +- id: census_lsoa_tenure5a_bedrooms_044 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 044 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01022648,E01022649,E01022650,E01022651,E01022652,E01022654,E01022655,E01022656,E01022657,E01022658,E01022659,E01022660,E01022661,E01022662,E01022663,E01022664,E01022665,E01022666,E01022667,E01022669,E01022670,E01022671,E01022672,E01022673,E01022674,E01022675,E01022676,E01022678,E01022679,E01022680,E01022681,E01022682,E01022683,E01022684,E01022685,E01022686,E01022687,E01022688,E01022689,E01022690,E01022691,E01022693,E01022694,E01022695,E01022696,E01022697,E01022698,E01022699,E01022700,E01022701,E01022702,E01022703,E01022704,E01022705,E01022706,E01022707,E01022708,E01022709,E01022710,E01022711,E01022712,E01022713,E01022714,E01022715,E01022716,E01022717,E01022718,E01022719,E01022720,E01022721,E01022722,E01022723,E01022724,E01022725,E01022726,E01022727,E01022728,E01022729,E01022730,E01022731,E01022732,E01022733,E01022734,E01022735,E01022736,E01022737,E01022738,E01022739,E01022740,E01022741,E01022742,E01022743,E01022744,E01022745,E01022746,E01022747,E01022748,E01022749,E01022750,E01022751,E01022752,E01022753,E01022754,E01022755,E01022756,E01022757,E01022758,E01022759,E01022760,E01022761,E01022762,E01022763,E01022764,E01022765,E01022766,E01022767,E01022768,E01022769,E01022770,E01022771,E01022772,E01022773,E01022774,E01022776,E01022777,E01022778,E01022780,E01022781,E01022782,E01022783,E01022784,E01022785,E01022786,E01022787,E01022788,E01022789,E01022790,E01022791,E01022792,E01022793,E01022794,E01022795,E01022796,E01022797,E01022798,E01022799,E01022800,E01022801,E01022802,E01022803,E01022804,E01022805,E01022806,E01022807,E01022810,E01022812,E01022813,E01022814,E01022815,E01022816,E01022817,E01022818,E01022819,E01022820,E01022823,E01022825,E01022826,E01022828,E01022829,E01022830,E01022831,E01022832,E01022833,E01022834,E01022835,E01022836,E01022837,E01022838,E01022839,E01022840,E01022841,E01022842,E01022843,E01022845,E01022846,E01022847,E01022848,E01022849,E01022850,E01022851,E01022852,E01022853,E01022854,E01022855,E01022856,E01022858,E01022859,E01022860,E01022861,E01022862,E01022863,E01022864,E01022865,E01022867,E01022868,E01022869,E01022870,E01022871,E01022872,E01022874,E01022875,E01022876,E01022877,E01022878,E01022879,E01022880,E01022881,E01022882,E01022883,E01022884,E01022885,E01022887,E01022888,E01022889,E01022890,E01022891,E01022892,E01022893,E01022894,E01022895,E01022896,E01022897,E01022898,E01022899,E01022900,E01022901,E01022902,E01022903,E01022904,E01022905,E01022906,E01022907,E01022908,E01022909,E01022910,E01022911,E01022913,E01022914,E01022915,E01022916,E01022917,E01022918,E01022919,E01022920,E01022921,E01022922,E01022924,E01022925,E01022926,E01022927,E01022928,E01022929,E01022930,E01022931,E01022933,E01022934,E01022935,E01022936,E01022937,E01022938,E01022939,E01022940,E01022941,E01022942,E01022943,E01022944,E01022945,E01022946,E01022947,E01022948,E01022949,E01022950,E01022951,E01022952,E01022954,E01022955,E01022956,E01022957,E01022958,E01022959,E01022960,E01022961,E01022962,E01022963,E01022964,E01022965,E01022966,E01022967,E01022968,E01022969,E01022970,E01022971,E01022972,E01022973,E01022974,E01022975,E01022976,E01022977,E01022978,E01022979,E01022980,E01022981,E01022982,E01022983,E01022984,E01022985,E01022986,E01022987,E01022988,E01022989,E01022990,E01022991,E01022992,E01022993,E01022994,E01022995,E01022996,E01022997,E01022998,E01022999,E01023000,E01023001,E01023002,E01023003,E01023004,E01023005,E01023006,E01023007,E01023008,E01023009,E01023010,E01023011,E01023012,E01023013,E01023014,E01023015,E01023016,E01023017,E01023018,E01023019,E01023020,E01023021,E01023022,E01023023,E01023024,E01023025,E01023026,E01023027,E01023028,E01023029,E01023030,E01023031,E01023032,E01023033,E01023034,E01023035,E01023036,E01023037,E01023038,E01023039,E01023040,E01023041,E01023042,E01023043,E01023044,E01023045,E01023046,E01023047,E01023048,E01023049,E01023050,E01023051,E01023052,E01023053,E01023054,E01023055,E01023056,E01023057,E01023058,E01023059,E01023060,E01023061,E01023062,E01023063,E01023064,E01023065,E01023066,E01023067,E01023068,E01023069,E01023070,E01023071,E01023072,E01023073,E01023074,E01023075,E01023076,E01023077,E01023078,E01023079,E01023080,E01023081,E01023082,E01023083,E01023084,E01023085,E01023086,E01023087,E01023088,E01023089,E01023090,E01023091,E01023092,E01023093,E01023094,E01023095,E01023096,E01023097,E01023098,E01023099,E01023100,E01023101,E01023102,E01023103,E01023104,E01023105,E01023106,E01023107,E01023108,E01023109,E01023110,E01023111,E01023112,E01023113,E01023114,E01023115,E01023116,E01023117,E01023118,E01023119,E01023120,E01023121,E01023122,E01023123,E01023124,E01023125,E01023128,E01023129,E01023130,E01023131,E01023132,E01023133,E01023134,E01023135,E01023136,E01023137,E01023138,E01023139,E01023140,E01023141,E01023144,E01023145,E01023146,E01023147,E01023148,E01023149,E01023150,E01023151,E01023152,E01023153,E01023154,E01023156,E01023157,E01023158,E01023159,E01023160,E01023162,E01023163,E01023164,E01023165,E01023166,E01023167,E01023168,E01023169,E01023170,E01023171,E01023172,E01023173,E01023174,E01023175&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_044.json.gz + content_sha256: 6a1620b87e87a985f55b14d2b4a15aaf0c09e2502c486279209a757dfa95a2af +- id: census_lsoa_tenure5a_bedrooms_045 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 045 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01023176,E01023177,E01023178,E01023179,E01023180,E01023181,E01023182,E01023183,E01023184,E01023185,E01023186,E01023187,E01023188,E01023189,E01023190,E01023191,E01023192,E01023193,E01023194,E01023195,E01023196,E01023197,E01023198,E01023200,E01023202,E01023203,E01023204,E01023205,E01023206,E01023207,E01023208,E01023209,E01023210,E01023211,E01023212,E01023213,E01023214,E01023215,E01023216,E01023217,E01023218,E01023219,E01023220,E01023221,E01023222,E01023223,E01023225,E01023226,E01023227,E01023228,E01023229,E01023230,E01023231,E01023232,E01023233,E01023234,E01023235,E01023236,E01023237,E01023238,E01023239,E01023240,E01023241,E01023242,E01023243,E01023244,E01023245,E01023246,E01023247,E01023248,E01023249,E01023250,E01023251,E01023252,E01023253,E01023254,E01023255,E01023256,E01023257,E01023258,E01023259,E01023260,E01023261,E01023262,E01023263,E01023264,E01023265,E01023266,E01023267,E01023268,E01023269,E01023270,E01023271,E01023272,E01023273,E01023274,E01023275,E01023276,E01023277,E01023278,E01023279,E01023280,E01023281,E01023282,E01023285,E01023286,E01023287,E01023288,E01023289,E01023290,E01023291,E01023292,E01023293,E01023294,E01023295,E01023296,E01023297,E01023298,E01023299,E01023300,E01023301,E01023302,E01023303,E01023304,E01023305,E01023306,E01023307,E01023308,E01023309,E01023310,E01023311,E01023312,E01023313,E01023314,E01023315,E01023316,E01023317,E01023318,E01023319,E01023320,E01023321,E01023322,E01023323,E01023324,E01023325,E01023326,E01023327,E01023328,E01023329,E01023330,E01023331,E01023332,E01023333,E01023334,E01023335,E01023336,E01023337,E01023339,E01023340,E01023341,E01023343,E01023344,E01023345,E01023346,E01023347,E01023348,E01023349,E01023350,E01023351,E01023352,E01023354,E01023355,E01023356,E01023357,E01023358,E01023359,E01023360,E01023361,E01023362,E01023363,E01023364,E01023365,E01023366,E01023367,E01023368,E01023369,E01023370,E01023371,E01023372,E01023373,E01023374,E01023375,E01023376,E01023377,E01023378,E01023379,E01023380,E01023381,E01023382,E01023383,E01023384,E01023385,E01023386,E01023387,E01023388,E01023389,E01023390,E01023391,E01023392,E01023393,E01023394,E01023395,E01023396,E01023397,E01023399,E01023400,E01023401,E01023402,E01023403,E01023404,E01023405,E01023406,E01023407,E01023408,E01023409,E01023410,E01023411,E01023412,E01023413,E01023414,E01023415,E01023416,E01023417,E01023418,E01023419,E01023420,E01023421,E01023422,E01023423,E01023424,E01023425,E01023426,E01023427,E01023428,E01023429,E01023430,E01023431,E01023432,E01023433,E01023434,E01023435,E01023436,E01023437,E01023438,E01023439,E01023440,E01023441,E01023442,E01023443,E01023444,E01023445,E01023447,E01023448,E01023449,E01023451,E01023452,E01023453,E01023454,E01023455,E01023456,E01023457,E01023458,E01023459,E01023460,E01023461,E01023462,E01023463,E01023465,E01023466,E01023467,E01023468,E01023469,E01023470,E01023471,E01023472,E01023473,E01023474,E01023475,E01023476,E01023477,E01023479,E01023480,E01023481,E01023482,E01023483,E01023484,E01023485,E01023486,E01023487,E01023488,E01023489,E01023490,E01023491,E01023492,E01023493,E01023494,E01023495,E01023496,E01023497,E01023499,E01023500,E01023501,E01023502,E01023503,E01023504,E01023505,E01023506,E01023507,E01023509,E01023510,E01023511,E01023512,E01023513,E01023514,E01023515,E01023516,E01023517,E01023518,E01023519,E01023520,E01023523,E01023524,E01023525,E01023526,E01023527,E01023528,E01023529,E01023530,E01023531,E01023532,E01023533,E01023534,E01023535,E01023536,E01023537,E01023538,E01023540,E01023541,E01023542,E01023543,E01023544,E01023545,E01023546,E01023547,E01023548,E01023549,E01023550,E01023551,E01023552,E01023553,E01023554,E01023555,E01023556,E01023557,E01023558,E01023559,E01023560,E01023561,E01023562,E01023563,E01023564,E01023565,E01023566,E01023567,E01023568,E01023569,E01023570,E01023571,E01023572,E01023573,E01023574,E01023575,E01023576,E01023577,E01023578,E01023579,E01023580,E01023581,E01023582,E01023583,E01023584,E01023585,E01023586,E01023587,E01023588,E01023589,E01023590,E01023591,E01023592,E01023593,E01023594,E01023595,E01023596,E01023597,E01023598,E01023600,E01023601,E01023602,E01023603,E01023604,E01023605,E01023606,E01023607,E01023608,E01023609,E01023610,E01023611,E01023612,E01023613,E01023614,E01023615,E01023616,E01023617,E01023618,E01023619,E01023620,E01023621,E01023622,E01023623,E01023624,E01023625,E01023626,E01023627,E01023628,E01023629,E01023630,E01023631,E01023632,E01023633,E01023634,E01023635,E01023636,E01023637,E01023638,E01023639,E01023640,E01023641,E01023642,E01023643,E01023644,E01023645,E01023646,E01023647,E01023648,E01023649,E01023650,E01023651,E01023652,E01023653,E01023654,E01023655,E01023656,E01023657,E01023658,E01023659,E01023660,E01023661,E01023662,E01023663,E01023664,E01023665,E01023666,E01023667,E01023668,E01023669,E01023670,E01023671,E01023672,E01023673,E01023674,E01023675,E01023676,E01023677,E01023678,E01023680,E01023681,E01023682,E01023683,E01023684,E01023685,E01023686,E01023687,E01023688,E01023689,E01023690,E01023691,E01023692,E01023693,E01023694,E01023695&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_045.json.gz + content_sha256: ae2697c10b9bef138d1169a822dce47cdc34fa55cd679147379cfbc622364226 +- id: census_lsoa_tenure5a_bedrooms_046 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 046 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01023696,E01023697,E01023698,E01023699,E01023700,E01023701,E01023702,E01023703,E01023704,E01023705,E01023706,E01023708,E01023709,E01023710,E01023711,E01023712,E01023713,E01023714,E01023715,E01023716,E01023717,E01023718,E01023719,E01023720,E01023721,E01023722,E01023723,E01023724,E01023725,E01023726,E01023727,E01023728,E01023730,E01023731,E01023732,E01023733,E01023734,E01023735,E01023736,E01023737,E01023738,E01023739,E01023740,E01023741,E01023742,E01023743,E01023744,E01023745,E01023746,E01023747,E01023748,E01023749,E01023750,E01023751,E01023752,E01023753,E01023754,E01023755,E01023756,E01023757,E01023759,E01023760,E01023761,E01023762,E01023763,E01023764,E01023765,E01023766,E01023767,E01023769,E01023770,E01023771,E01023772,E01023773,E01023774,E01023775,E01023776,E01023777,E01023778,E01023779,E01023780,E01023781,E01023782,E01023783,E01023784,E01023785,E01023786,E01023787,E01023788,E01023790,E01023791,E01023792,E01023793,E01023794,E01023795,E01023796,E01023797,E01023798,E01023799,E01023800,E01023801,E01023802,E01023803,E01023804,E01023805,E01023806,E01023807,E01023809,E01023810,E01023811,E01023812,E01023813,E01023814,E01023815,E01023816,E01023817,E01023818,E01023819,E01023820,E01023821,E01023822,E01023823,E01023824,E01023825,E01023826,E01023827,E01023828,E01023829,E01023830,E01023831,E01023832,E01023834,E01023835,E01023836,E01023837,E01023838,E01023839,E01023840,E01023841,E01023842,E01023843,E01023844,E01023845,E01023846,E01023847,E01023848,E01023849,E01023850,E01023851,E01023852,E01023853,E01023854,E01023855,E01023856,E01023857,E01023858,E01023860,E01023862,E01023863,E01023864,E01023865,E01023866,E01023868,E01023869,E01023870,E01023871,E01023872,E01023873,E01023874,E01023875,E01023876,E01023877,E01023878,E01023880,E01023881,E01023882,E01023883,E01023884,E01023885,E01023886,E01023887,E01023888,E01023889,E01023890,E01023891,E01023892,E01023893,E01023894,E01023895,E01023896,E01023897,E01023898,E01023899,E01023900,E01023901,E01023902,E01023903,E01023904,E01023905,E01023906,E01023907,E01023908,E01023909,E01023910,E01023911,E01023912,E01023913,E01023914,E01023915,E01023916,E01023917,E01023918,E01023919,E01023920,E01023921,E01023922,E01023923,E01023924,E01023925,E01023926,E01023927,E01023929,E01023930,E01023932,E01023933,E01023934,E01023935,E01023936,E01023937,E01023938,E01023939,E01023940,E01023942,E01023943,E01023944,E01023945,E01023946,E01023947,E01023948,E01023949,E01023950,E01023951,E01023952,E01023953,E01023955,E01023956,E01023957,E01023958,E01023959,E01023960,E01023961,E01023962,E01023963,E01023965,E01023966,E01023967,E01023968,E01023969,E01023970,E01023971,E01023972,E01023973,E01023974,E01023975,E01023976,E01023977,E01023978,E01023979,E01023980,E01023981,E01023983,E01023984,E01023985,E01023986,E01023987,E01023988,E01023989,E01023990,E01023992,E01023993,E01023995,E01023996,E01023997,E01023998,E01023999,E01024001,E01024002,E01024003,E01024004,E01024005,E01024006,E01024008,E01024009,E01024010,E01024011,E01024012,E01024013,E01024014,E01024015,E01024016,E01024017,E01024019,E01024020,E01024021,E01024022,E01024023,E01024024,E01024025,E01024026,E01024028,E01024029,E01024031,E01024032,E01024033,E01024035,E01024036,E01024037,E01024038,E01024039,E01024040,E01024041,E01024042,E01024043,E01024044,E01024045,E01024046,E01024047,E01024048,E01024049,E01024052,E01024053,E01024054,E01024055,E01024056,E01024057,E01024058,E01024059,E01024060,E01024061,E01024062,E01024063,E01024064,E01024065,E01024066,E01024067,E01024068,E01024069,E01024070,E01024071,E01024072,E01024073,E01024074,E01024075,E01024076,E01024077,E01024078,E01024079,E01024080,E01024081,E01024082,E01024083,E01024084,E01024085,E01024086,E01024087,E01024088,E01024089,E01024090,E01024091,E01024092,E01024093,E01024094,E01024095,E01024096,E01024097,E01024098,E01024099,E01024100,E01024101,E01024102,E01024103,E01024104,E01024105,E01024106,E01024107,E01024108,E01024109,E01024110,E01024111,E01024112,E01024113,E01024114,E01024115,E01024116,E01024117,E01024118,E01024119,E01024120,E01024121,E01024122,E01024123,E01024124,E01024126,E01024127,E01024128,E01024129,E01024131,E01024132,E01024133,E01024134,E01024135,E01024136,E01024137,E01024138,E01024139,E01024143,E01024144,E01024145,E01024146,E01024147,E01024150,E01024151,E01024152,E01024153,E01024154,E01024155,E01024156,E01024157,E01024158,E01024159,E01024160,E01024161,E01024162,E01024163,E01024164,E01024165,E01024166,E01024167,E01024168,E01024169,E01024170,E01024171,E01024172,E01024173,E01024174,E01024175,E01024176,E01024177,E01024178,E01024179,E01024180,E01024181,E01024182,E01024183,E01024184,E01024185,E01024186,E01024187,E01024188,E01024189,E01024190,E01024192,E01024193,E01024194,E01024195,E01024196,E01024197,E01024198,E01024200,E01024201,E01024202,E01024203,E01024204,E01024205,E01024206,E01024207,E01024208,E01024209,E01024212,E01024213,E01024214,E01024215,E01024216,E01024218,E01024219,E01024220,E01024221,E01024222,E01024223,E01024224,E01024225,E01024226,E01024227,E01024228,E01024229,E01024230,E01024231,E01024232,E01024233,E01024234&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_046.json.gz + content_sha256: aeddd24572cd5b140484bc355f3b36113914e9c8fb67248e54a31373159b35d9 +- id: census_lsoa_tenure5a_bedrooms_047 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 047 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01024235,E01024236,E01024237,E01024238,E01024239,E01024240,E01024241,E01024242,E01024243,E01024244,E01024245,E01024246,E01024247,E01024248,E01024249,E01024250,E01024251,E01024252,E01024253,E01024254,E01024255,E01024256,E01024257,E01024258,E01024259,E01024260,E01024261,E01024262,E01024263,E01024264,E01024265,E01024266,E01024267,E01024268,E01024269,E01024270,E01024271,E01024272,E01024273,E01024274,E01024275,E01024276,E01024277,E01024278,E01024279,E01024281,E01024282,E01024283,E01024284,E01024285,E01024286,E01024287,E01024288,E01024290,E01024291,E01024292,E01024293,E01024294,E01024295,E01024296,E01024297,E01024298,E01024299,E01024300,E01024301,E01024302,E01024303,E01024304,E01024305,E01024306,E01024307,E01024308,E01024309,E01024310,E01024311,E01024312,E01024313,E01024314,E01024315,E01024316,E01024317,E01024318,E01024319,E01024320,E01024321,E01024322,E01024323,E01024324,E01024325,E01024326,E01024327,E01024328,E01024329,E01024330,E01024331,E01024332,E01024333,E01024335,E01024336,E01024337,E01024339,E01024340,E01024341,E01024342,E01024343,E01024344,E01024345,E01024346,E01024347,E01024348,E01024349,E01024350,E01024351,E01024352,E01024353,E01024354,E01024355,E01024356,E01024357,E01024358,E01024360,E01024361,E01024362,E01024364,E01024365,E01024366,E01024367,E01024369,E01024371,E01024372,E01024373,E01024374,E01024375,E01024376,E01024377,E01024378,E01024379,E01024380,E01024381,E01024382,E01024383,E01024384,E01024385,E01024386,E01024387,E01024390,E01024391,E01024392,E01024393,E01024394,E01024395,E01024396,E01024397,E01024398,E01024399,E01024400,E01024401,E01024402,E01024403,E01024404,E01024405,E01024406,E01024407,E01024408,E01024409,E01024410,E01024411,E01024413,E01024414,E01024415,E01024416,E01024417,E01024418,E01024419,E01024420,E01024421,E01024423,E01024424,E01024425,E01024426,E01024427,E01024428,E01024429,E01024430,E01024431,E01024432,E01024433,E01024434,E01024435,E01024436,E01024437,E01024438,E01024439,E01024440,E01024441,E01024442,E01024443,E01024444,E01024445,E01024446,E01024447,E01024448,E01024449,E01024450,E01024451,E01024452,E01024453,E01024454,E01024455,E01024456,E01024457,E01024458,E01024459,E01024460,E01024461,E01024462,E01024463,E01024464,E01024465,E01024466,E01024467,E01024468,E01024469,E01024470,E01024471,E01024472,E01024473,E01024474,E01024475,E01024476,E01024477,E01024478,E01024479,E01024480,E01024481,E01024482,E01024483,E01024484,E01024485,E01024486,E01024487,E01024488,E01024489,E01024490,E01024491,E01024492,E01024493,E01024494,E01024495,E01024496,E01024497,E01024498,E01024499,E01024500,E01024501,E01024502,E01024503,E01024504,E01024505,E01024507,E01024508,E01024509,E01024510,E01024511,E01024512,E01024513,E01024514,E01024515,E01024516,E01024517,E01024518,E01024519,E01024520,E01024521,E01024522,E01024523,E01024524,E01024525,E01024526,E01024527,E01024528,E01024529,E01024530,E01024531,E01024532,E01024533,E01024534,E01024535,E01024536,E01024537,E01024538,E01024539,E01024540,E01024541,E01024542,E01024544,E01024545,E01024546,E01024547,E01024548,E01024549,E01024550,E01024551,E01024552,E01024553,E01024554,E01024555,E01024556,E01024557,E01024558,E01024559,E01024560,E01024561,E01024562,E01024563,E01024564,E01024565,E01024566,E01024569,E01024570,E01024571,E01024572,E01024573,E01024575,E01024576,E01024577,E01024578,E01024580,E01024581,E01024582,E01024583,E01024584,E01024585,E01024586,E01024587,E01024588,E01024589,E01024590,E01024591,E01024592,E01024593,E01024594,E01024595,E01024596,E01024597,E01024598,E01024599,E01024600,E01024601,E01024602,E01024603,E01024604,E01024605,E01024606,E01024608,E01024609,E01024610,E01024611,E01024612,E01024613,E01024614,E01024615,E01024616,E01024619,E01024620,E01024621,E01024622,E01024623,E01024624,E01024625,E01024626,E01024627,E01024628,E01024629,E01024630,E01024631,E01024632,E01024633,E01024634,E01024635,E01024636,E01024637,E01024638,E01024639,E01024640,E01024641,E01024642,E01024643,E01024644,E01024645,E01024646,E01024647,E01024648,E01024649,E01024650,E01024651,E01024652,E01024653,E01024654,E01024655,E01024656,E01024657,E01024658,E01024659,E01024660,E01024661,E01024662,E01024663,E01024664,E01024665,E01024666,E01024667,E01024668,E01024669,E01024670,E01024671,E01024672,E01024673,E01024674,E01024675,E01024676,E01024677,E01024679,E01024680,E01024681,E01024682,E01024683,E01024684,E01024685,E01024686,E01024687,E01024688,E01024689,E01024690,E01024691,E01024692,E01024693,E01024694,E01024695,E01024696,E01024697,E01024698,E01024699,E01024700,E01024702,E01024703,E01024704,E01024705,E01024706,E01024707,E01024708,E01024709,E01024710,E01024711,E01024712,E01024713,E01024714,E01024715,E01024716,E01024717,E01024718,E01024719,E01024720,E01024721,E01024722,E01024723,E01024724,E01024725,E01024726,E01024728,E01024729,E01024730,E01024731,E01024732,E01024733,E01024734,E01024735,E01024736,E01024737,E01024740,E01024741,E01024742,E01024743,E01024744,E01024745,E01024746,E01024747,E01024748,E01024749,E01024750,E01024751,E01024752,E01024753,E01024754,E01024755,E01024756,E01024757,E01024758,E01024760,E01024761&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_047.json.gz + content_sha256: 84e44e35a054632b76da278ce914de7a547d7582090a22fb1ab803e36a35d22c +- id: census_lsoa_tenure5a_bedrooms_048 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 048 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01024762,E01024763,E01024764,E01024765,E01024767,E01024768,E01024771,E01024772,E01024773,E01024775,E01024776,E01024777,E01024778,E01024779,E01024780,E01024781,E01024782,E01024783,E01024785,E01024786,E01024787,E01024788,E01024789,E01024790,E01024791,E01024792,E01024793,E01024794,E01024795,E01024796,E01024797,E01024798,E01024799,E01024800,E01024802,E01024803,E01024804,E01024805,E01024806,E01024807,E01024808,E01024809,E01024810,E01024811,E01024812,E01024813,E01024814,E01024815,E01024816,E01024817,E01024818,E01024819,E01024820,E01024821,E01024822,E01024823,E01024824,E01024825,E01024826,E01024827,E01024828,E01024829,E01024830,E01024831,E01024832,E01024833,E01024834,E01024835,E01024836,E01024837,E01024838,E01024839,E01024840,E01024841,E01024842,E01024843,E01024844,E01024845,E01024846,E01024847,E01024848,E01024849,E01024850,E01024851,E01024852,E01024853,E01024854,E01024855,E01024856,E01024857,E01024858,E01024859,E01024860,E01024861,E01024862,E01024863,E01024864,E01024865,E01024866,E01024867,E01024868,E01024869,E01024870,E01024871,E01024872,E01024873,E01024874,E01024875,E01024876,E01024877,E01024878,E01024879,E01024880,E01024881,E01024882,E01024883,E01024884,E01024885,E01024886,E01024887,E01024888,E01024889,E01024890,E01024891,E01024892,E01024893,E01024894,E01024895,E01024896,E01024897,E01024898,E01024899,E01024900,E01024901,E01024902,E01024903,E01024904,E01024905,E01024906,E01024907,E01024908,E01024909,E01024910,E01024911,E01024912,E01024913,E01024914,E01024915,E01024916,E01024917,E01024918,E01024920,E01024921,E01024922,E01024923,E01024924,E01024925,E01024926,E01024927,E01024928,E01024930,E01024931,E01024933,E01024934,E01024935,E01024936,E01024937,E01024938,E01024939,E01024940,E01024941,E01024942,E01024943,E01024944,E01024946,E01024947,E01024948,E01024950,E01024951,E01024952,E01024953,E01024954,E01024955,E01024956,E01024957,E01024958,E01024959,E01024960,E01024961,E01024962,E01024963,E01024964,E01024965,E01024966,E01024967,E01024968,E01024969,E01024970,E01024971,E01024972,E01024973,E01024974,E01024975,E01024976,E01024977,E01024978,E01024979,E01024980,E01024981,E01024982,E01024983,E01024984,E01024985,E01024986,E01024987,E01024988,E01024989,E01024990,E01024991,E01024992,E01024993,E01024994,E01024995,E01024996,E01024997,E01024998,E01024999,E01025000,E01025001,E01025002,E01025003,E01025004,E01025005,E01025006,E01025007,E01025008,E01025009,E01025010,E01025011,E01025012,E01025013,E01025014,E01025015,E01025016,E01025017,E01025018,E01025019,E01025020,E01025021,E01025022,E01025023,E01025024,E01025025,E01025026,E01025027,E01025028,E01025029,E01025030,E01025031,E01025032,E01025033,E01025034,E01025035,E01025036,E01025037,E01025038,E01025039,E01025040,E01025041,E01025042,E01025044,E01025045,E01025046,E01025048,E01025049,E01025050,E01025051,E01025052,E01025053,E01025054,E01025055,E01025056,E01025057,E01025058,E01025059,E01025060,E01025061,E01025062,E01025063,E01025064,E01025065,E01025066,E01025067,E01025068,E01025069,E01025070,E01025071,E01025072,E01025073,E01025074,E01025075,E01025076,E01025077,E01025078,E01025079,E01025080,E01025081,E01025082,E01025083,E01025084,E01025085,E01025086,E01025087,E01025088,E01025089,E01025090,E01025091,E01025092,E01025093,E01025094,E01025095,E01025096,E01025097,E01025098,E01025099,E01025100,E01025101,E01025102,E01025103,E01025105,E01025106,E01025107,E01025108,E01025109,E01025110,E01025111,E01025112,E01025113,E01025114,E01025115,E01025116,E01025117,E01025118,E01025119,E01025120,E01025121,E01025122,E01025123,E01025124,E01025126,E01025127,E01025128,E01025130,E01025131,E01025132,E01025133,E01025136,E01025137,E01025138,E01025139,E01025140,E01025141,E01025142,E01025143,E01025144,E01025145,E01025146,E01025147,E01025148,E01025149,E01025150,E01025151,E01025152,E01025153,E01025154,E01025155,E01025156,E01025157,E01025158,E01025159,E01025160,E01025161,E01025162,E01025163,E01025164,E01025165,E01025166,E01025167,E01025168,E01025169,E01025170,E01025171,E01025172,E01025173,E01025174,E01025175,E01025176,E01025177,E01025178,E01025179,E01025180,E01025181,E01025182,E01025183,E01025184,E01025185,E01025186,E01025187,E01025188,E01025189,E01025190,E01025191,E01025192,E01025193,E01025194,E01025195,E01025196,E01025197,E01025198,E01025199,E01025200,E01025201,E01025202,E01025203,E01025204,E01025205,E01025206,E01025207,E01025208,E01025209,E01025210,E01025211,E01025212,E01025213,E01025214,E01025215,E01025216,E01025217,E01025218,E01025219,E01025220,E01025221,E01025222,E01025223,E01025224,E01025225,E01025226,E01025227,E01025228,E01025229,E01025230,E01025231,E01025232,E01025233,E01025234,E01025235,E01025236,E01025237,E01025238,E01025239,E01025240,E01025241,E01025242,E01025243,E01025244,E01025245,E01025246,E01025247,E01025248,E01025249,E01025250,E01025251,E01025252,E01025253,E01025254,E01025255,E01025256,E01025257,E01025258,E01025259,E01025260,E01025261,E01025262,E01025263,E01025264,E01025265,E01025266,E01025267,E01025268,E01025269,E01025270,E01025271,E01025272,E01025273,E01025274,E01025275,E01025276,E01025277,E01025278,E01025279&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_048.json.gz + content_sha256: 2105cbf28b18162f48d8c578c2446e3973a7b693294e6c4b45cdc231432162df +- id: census_lsoa_tenure5a_bedrooms_049 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 049 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01025281,E01025282,E01025283,E01025284,E01025285,E01025286,E01025287,E01025288,E01025289,E01025290,E01025291,E01025292,E01025293,E01025294,E01025295,E01025296,E01025297,E01025298,E01025299,E01025300,E01025301,E01025302,E01025303,E01025304,E01025305,E01025306,E01025308,E01025309,E01025310,E01025311,E01025312,E01025313,E01025314,E01025315,E01025316,E01025317,E01025318,E01025319,E01025320,E01025321,E01025322,E01025323,E01025324,E01025325,E01025326,E01025327,E01025328,E01025329,E01025330,E01025331,E01025333,E01025334,E01025335,E01025336,E01025337,E01025338,E01025339,E01025340,E01025341,E01025342,E01025343,E01025344,E01025345,E01025346,E01025347,E01025348,E01025349,E01025350,E01025351,E01025352,E01025354,E01025355,E01025356,E01025357,E01025358,E01025359,E01025360,E01025361,E01025362,E01025363,E01025364,E01025365,E01025366,E01025367,E01025368,E01025369,E01025370,E01025371,E01025373,E01025374,E01025375,E01025376,E01025377,E01025378,E01025379,E01025380,E01025381,E01025382,E01025383,E01025384,E01025385,E01025387,E01025388,E01025389,E01025390,E01025391,E01025392,E01025393,E01025394,E01025395,E01025396,E01025397,E01025398,E01025399,E01025400,E01025401,E01025402,E01025403,E01025404,E01025405,E01025406,E01025407,E01025408,E01025409,E01025410,E01025411,E01025412,E01025413,E01025414,E01025415,E01025416,E01025417,E01025418,E01025419,E01025421,E01025422,E01025423,E01025424,E01025425,E01025426,E01025427,E01025428,E01025429,E01025431,E01025433,E01025434,E01025435,E01025436,E01025437,E01025438,E01025439,E01025440,E01025441,E01025442,E01025443,E01025444,E01025445,E01025446,E01025447,E01025448,E01025451,E01025452,E01025453,E01025454,E01025455,E01025456,E01025457,E01025458,E01025459,E01025460,E01025461,E01025462,E01025463,E01025464,E01025465,E01025466,E01025467,E01025468,E01025469,E01025470,E01025471,E01025472,E01025473,E01025474,E01025475,E01025476,E01025477,E01025478,E01025479,E01025480,E01025481,E01025482,E01025483,E01025484,E01025485,E01025486,E01025487,E01025488,E01025489,E01025490,E01025491,E01025492,E01025493,E01025494,E01025495,E01025496,E01025497,E01025498,E01025499,E01025500,E01025501,E01025502,E01025503,E01025504,E01025505,E01025506,E01025507,E01025508,E01025509,E01025510,E01025511,E01025512,E01025513,E01025514,E01025515,E01025516,E01025517,E01025518,E01025519,E01025520,E01025521,E01025522,E01025523,E01025524,E01025525,E01025526,E01025527,E01025528,E01025529,E01025530,E01025531,E01025532,E01025533,E01025534,E01025535,E01025536,E01025537,E01025538,E01025539,E01025540,E01025541,E01025542,E01025543,E01025544,E01025545,E01025546,E01025547,E01025548,E01025549,E01025550,E01025551,E01025552,E01025553,E01025554,E01025555,E01025556,E01025557,E01025558,E01025559,E01025560,E01025561,E01025562,E01025563,E01025564,E01025565,E01025566,E01025567,E01025568,E01025569,E01025570,E01025571,E01025572,E01025573,E01025574,E01025575,E01025576,E01025577,E01025578,E01025579,E01025580,E01025581,E01025582,E01025583,E01025584,E01025585,E01025586,E01025587,E01025588,E01025589,E01025590,E01025591,E01025592,E01025593,E01025594,E01025595,E01025596,E01025597,E01025598,E01025599,E01025600,E01025601,E01025602,E01025603,E01025604,E01025605,E01025606,E01025607,E01025608,E01025609,E01025610,E01025611,E01025612,E01025613,E01025614,E01025615,E01025616,E01025617,E01025618,E01025619,E01025620,E01025621,E01025622,E01025623,E01025624,E01025625,E01025626,E01025627,E01025629,E01025630,E01025631,E01025632,E01025633,E01025634,E01025635,E01025636,E01025637,E01025638,E01025639,E01025640,E01025641,E01025642,E01025643,E01025644,E01025645,E01025646,E01025647,E01025648,E01025649,E01025650,E01025651,E01025652,E01025653,E01025654,E01025655,E01025656,E01025657,E01025658,E01025659,E01025660,E01025661,E01025662,E01025663,E01025664,E01025665,E01025666,E01025667,E01025668,E01025669,E01025670,E01025671,E01025672,E01025673,E01025674,E01025675,E01025676,E01025677,E01025678,E01025680,E01025681,E01025682,E01025683,E01025684,E01025685,E01025686,E01025687,E01025688,E01025689,E01025690,E01025692,E01025693,E01025695,E01025696,E01025697,E01025698,E01025699,E01025700,E01025701,E01025702,E01025703,E01025704,E01025705,E01025706,E01025707,E01025708,E01025709,E01025710,E01025711,E01025712,E01025713,E01025715,E01025716,E01025718,E01025719,E01025720,E01025721,E01025722,E01025723,E01025724,E01025725,E01025726,E01025727,E01025728,E01025729,E01025730,E01025731,E01025732,E01025733,E01025734,E01025735,E01025736,E01025737,E01025738,E01025739,E01025740,E01025741,E01025742,E01025743,E01025744,E01025745,E01025746,E01025747,E01025748,E01025749,E01025750,E01025751,E01025752,E01025753,E01025754,E01025755,E01025756,E01025757,E01025758,E01025759,E01025760,E01025761,E01025762,E01025763,E01025764,E01025765,E01025766,E01025767,E01025768,E01025769,E01025770,E01025772,E01025773,E01025774,E01025775,E01025776,E01025777,E01025778,E01025779,E01025780,E01025781,E01025782,E01025783,E01025784,E01025786,E01025787,E01025788,E01025789,E01025790,E01025791,E01025792,E01025794,E01025795,E01025796,E01025797,E01025798,E01025799&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_049.json.gz + content_sha256: 8046130b8fbca58764e2693e7109bebf89fa821a94b88d2a77afbb250a47dae7 +- id: census_lsoa_tenure5a_bedrooms_050 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 050 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01025800,E01025801,E01025802,E01025803,E01025804,E01025806,E01025807,E01025808,E01025809,E01025810,E01025812,E01025813,E01025814,E01025815,E01025816,E01025817,E01025818,E01025819,E01025820,E01025821,E01025822,E01025823,E01025824,E01025825,E01025826,E01025827,E01025828,E01025829,E01025830,E01025831,E01025832,E01025833,E01025834,E01025835,E01025836,E01025837,E01025838,E01025839,E01025840,E01025841,E01025842,E01025843,E01025844,E01025845,E01025846,E01025847,E01025848,E01025849,E01025850,E01025851,E01025852,E01025854,E01025855,E01025856,E01025857,E01025858,E01025859,E01025860,E01025861,E01025862,E01025863,E01025864,E01025865,E01025866,E01025867,E01025868,E01025869,E01025870,E01025871,E01025872,E01025873,E01025874,E01025875,E01025876,E01025877,E01025879,E01025880,E01025881,E01025882,E01025883,E01025884,E01025885,E01025886,E01025887,E01025888,E01025889,E01025890,E01025891,E01025892,E01025893,E01025894,E01025895,E01025896,E01025897,E01025898,E01025899,E01025900,E01025901,E01025902,E01025903,E01025904,E01025905,E01025906,E01025907,E01025908,E01025909,E01025910,E01025911,E01025912,E01025913,E01025914,E01025915,E01025916,E01025918,E01025919,E01025920,E01025921,E01025923,E01025924,E01025926,E01025927,E01025928,E01025929,E01025930,E01025931,E01025932,E01025933,E01025934,E01025935,E01025936,E01025937,E01025938,E01025939,E01025940,E01025941,E01025942,E01025943,E01025944,E01025945,E01025946,E01025947,E01025948,E01025949,E01025950,E01025951,E01025952,E01025953,E01025954,E01025955,E01025956,E01025957,E01025958,E01025959,E01025960,E01025961,E01025962,E01025963,E01025964,E01025965,E01025966,E01025967,E01025968,E01025969,E01025970,E01025971,E01025972,E01025973,E01025974,E01025975,E01025976,E01025977,E01025978,E01025979,E01025980,E01025981,E01025982,E01025983,E01025984,E01025985,E01025986,E01025987,E01025988,E01025989,E01025990,E01025991,E01025992,E01025993,E01025994,E01025995,E01025997,E01025998,E01025999,E01026000,E01026001,E01026003,E01026004,E01026005,E01026007,E01026008,E01026009,E01026010,E01026012,E01026013,E01026014,E01026015,E01026016,E01026017,E01026018,E01026019,E01026020,E01026021,E01026022,E01026023,E01026024,E01026025,E01026026,E01026027,E01026028,E01026030,E01026031,E01026032,E01026033,E01026034,E01026035,E01026036,E01026037,E01026038,E01026039,E01026040,E01026041,E01026042,E01026043,E01026044,E01026045,E01026046,E01026047,E01026048,E01026049,E01026050,E01026051,E01026052,E01026053,E01026054,E01026055,E01026056,E01026057,E01026058,E01026059,E01026060,E01026061,E01026062,E01026063,E01026064,E01026065,E01026066,E01026067,E01026068,E01026069,E01026070,E01026071,E01026072,E01026073,E01026074,E01026075,E01026076,E01026077,E01026078,E01026079,E01026080,E01026081,E01026082,E01026083,E01026084,E01026087,E01026088,E01026089,E01026090,E01026091,E01026092,E01026093,E01026094,E01026095,E01026096,E01026097,E01026098,E01026099,E01026100,E01026101,E01026102,E01026103,E01026104,E01026105,E01026106,E01026107,E01026108,E01026109,E01026110,E01026111,E01026112,E01026113,E01026114,E01026115,E01026116,E01026117,E01026118,E01026119,E01026120,E01026121,E01026122,E01026123,E01026124,E01026125,E01026126,E01026127,E01026129,E01026130,E01026131,E01026132,E01026134,E01026135,E01026136,E01026137,E01026138,E01026139,E01026140,E01026141,E01026142,E01026144,E01026145,E01026146,E01026147,E01026148,E01026149,E01026150,E01026151,E01026152,E01026153,E01026154,E01026155,E01026156,E01026157,E01026158,E01026159,E01026160,E01026161,E01026162,E01026163,E01026164,E01026165,E01026166,E01026167,E01026168,E01026169,E01026170,E01026171,E01026172,E01026173,E01026174,E01026175,E01026176,E01026177,E01026178,E01026179,E01026180,E01026182,E01026183,E01026184,E01026185,E01026186,E01026187,E01026188,E01026189,E01026190,E01026191,E01026192,E01026193,E01026194,E01026195,E01026196,E01026197,E01026198,E01026199,E01026200,E01026201,E01026202,E01026203,E01026204,E01026205,E01026206,E01026207,E01026208,E01026209,E01026210,E01026211,E01026212,E01026213,E01026214,E01026215,E01026218,E01026219,E01026220,E01026221,E01026222,E01026223,E01026224,E01026225,E01026226,E01026227,E01026228,E01026229,E01026230,E01026231,E01026232,E01026233,E01026235,E01026236,E01026237,E01026238,E01026239,E01026240,E01026241,E01026242,E01026243,E01026244,E01026245,E01026246,E01026247,E01026248,E01026249,E01026250,E01026251,E01026252,E01026253,E01026254,E01026255,E01026256,E01026257,E01026258,E01026259,E01026260,E01026262,E01026263,E01026264,E01026265,E01026266,E01026268,E01026269,E01026270,E01026271,E01026272,E01026273,E01026275,E01026276,E01026277,E01026278,E01026279,E01026280,E01026281,E01026282,E01026283,E01026284,E01026285,E01026286,E01026287,E01026288,E01026289,E01026290,E01026291,E01026292,E01026293,E01026294,E01026296,E01026297,E01026298,E01026299,E01026300,E01026302,E01026303,E01026304,E01026305,E01026306,E01026307,E01026308,E01026309,E01026310,E01026311,E01026312,E01026313,E01026314,E01026315,E01026316,E01026317,E01026318,E01026319,E01026321,E01026322,E01026323,E01026324,E01026325,E01026326&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_050.json.gz + content_sha256: 0bdafc5faa2a2e07bad09b4933fb479969611399206e352942760d1a4382a5f7 +- id: census_lsoa_tenure5a_bedrooms_051 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 051 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01026327,E01026328,E01026329,E01026330,E01026331,E01026332,E01026333,E01026334,E01026335,E01026336,E01026337,E01026338,E01026339,E01026340,E01026341,E01026342,E01026343,E01026344,E01026345,E01026346,E01026347,E01026348,E01026349,E01026350,E01026351,E01026352,E01026353,E01026354,E01026355,E01026356,E01026357,E01026358,E01026359,E01026360,E01026361,E01026362,E01026363,E01026364,E01026365,E01026366,E01026367,E01026368,E01026369,E01026370,E01026371,E01026372,E01026373,E01026374,E01026375,E01026376,E01026377,E01026378,E01026379,E01026380,E01026381,E01026382,E01026383,E01026384,E01026385,E01026386,E01026387,E01026388,E01026389,E01026390,E01026391,E01026392,E01026393,E01026394,E01026395,E01026396,E01026398,E01026399,E01026400,E01026401,E01026402,E01026403,E01026404,E01026405,E01026406,E01026407,E01026408,E01026409,E01026410,E01026411,E01026412,E01026413,E01026414,E01026415,E01026416,E01026419,E01026420,E01026421,E01026423,E01026424,E01026425,E01026426,E01026427,E01026428,E01026429,E01026430,E01026431,E01026432,E01026433,E01026434,E01026435,E01026436,E01026437,E01026438,E01026439,E01026440,E01026441,E01026442,E01026443,E01026444,E01026445,E01026446,E01026447,E01026448,E01026449,E01026450,E01026451,E01026452,E01026453,E01026454,E01026456,E01026457,E01026458,E01026459,E01026461,E01026462,E01026464,E01026465,E01026466,E01026467,E01026468,E01026469,E01026470,E01026471,E01026472,E01026473,E01026474,E01026475,E01026476,E01026477,E01026478,E01026479,E01026480,E01026481,E01026483,E01026484,E01026485,E01026486,E01026487,E01026488,E01026489,E01026490,E01026491,E01026492,E01026493,E01026494,E01026495,E01026496,E01026497,E01026498,E01026499,E01026500,E01026501,E01026502,E01026503,E01026504,E01026505,E01026506,E01026507,E01026508,E01026509,E01026510,E01026511,E01026512,E01026513,E01026514,E01026515,E01026516,E01026517,E01026518,E01026519,E01026520,E01026521,E01026522,E01026523,E01026524,E01026525,E01026526,E01026527,E01026528,E01026529,E01026530,E01026531,E01026532,E01026533,E01026534,E01026535,E01026536,E01026537,E01026538,E01026539,E01026540,E01026541,E01026542,E01026543,E01026544,E01026545,E01026546,E01026547,E01026548,E01026549,E01026550,E01026551,E01026552,E01026553,E01026554,E01026555,E01026556,E01026557,E01026558,E01026559,E01026560,E01026562,E01026563,E01026564,E01026565,E01026566,E01026567,E01026568,E01026569,E01026570,E01026571,E01026572,E01026573,E01026574,E01026575,E01026576,E01026577,E01026578,E01026579,E01026580,E01026581,E01026582,E01026583,E01026584,E01026587,E01026588,E01026589,E01026590,E01026591,E01026592,E01026593,E01026594,E01026595,E01026596,E01026597,E01026598,E01026599,E01026600,E01026601,E01026602,E01026603,E01026604,E01026605,E01026606,E01026607,E01026608,E01026609,E01026610,E01026611,E01026612,E01026613,E01026614,E01026615,E01026616,E01026617,E01026618,E01026619,E01026620,E01026621,E01026622,E01026623,E01026624,E01026625,E01026626,E01026627,E01026628,E01026629,E01026630,E01026631,E01026632,E01026633,E01026634,E01026635,E01026636,E01026637,E01026638,E01026639,E01026640,E01026641,E01026642,E01026643,E01026644,E01026645,E01026646,E01026647,E01026648,E01026649,E01026650,E01026651,E01026652,E01026653,E01026654,E01026655,E01026656,E01026659,E01026660,E01026661,E01026662,E01026663,E01026664,E01026665,E01026666,E01026667,E01026668,E01026669,E01026670,E01026671,E01026672,E01026673,E01026674,E01026675,E01026676,E01026677,E01026678,E01026679,E01026680,E01026681,E01026682,E01026683,E01026684,E01026685,E01026687,E01026688,E01026689,E01026690,E01026691,E01026692,E01026693,E01026694,E01026695,E01026696,E01026697,E01026699,E01026700,E01026701,E01026702,E01026703,E01026704,E01026705,E01026706,E01026708,E01026709,E01026710,E01026711,E01026712,E01026713,E01026714,E01026715,E01026716,E01026717,E01026718,E01026719,E01026720,E01026721,E01026722,E01026723,E01026724,E01026725,E01026726,E01026727,E01026728,E01026729,E01026730,E01026733,E01026734,E01026735,E01026736,E01026737,E01026738,E01026739,E01026740,E01026741,E01026742,E01026743,E01026744,E01026745,E01026746,E01026747,E01026748,E01026749,E01026750,E01026751,E01026752,E01026753,E01026754,E01026755,E01026756,E01026757,E01026758,E01026759,E01026760,E01026761,E01026763,E01026764,E01026765,E01026766,E01026767,E01026768,E01026769,E01026770,E01026771,E01026772,E01026773,E01026774,E01026775,E01026776,E01026777,E01026778,E01026779,E01026780,E01026781,E01026782,E01026783,E01026784,E01026785,E01026786,E01026787,E01026788,E01026789,E01026790,E01026791,E01026792,E01026793,E01026794,E01026796,E01026797,E01026798,E01026799,E01026800,E01026801,E01026802,E01026803,E01026804,E01026805,E01026806,E01026807,E01026808,E01026809,E01026810,E01026811,E01026812,E01026813,E01026814,E01026815,E01026816,E01026817,E01026818,E01026819,E01026820,E01026821,E01026823,E01026824,E01026825,E01026826,E01026827,E01026828,E01026829,E01026830,E01026831,E01026832,E01026833,E01026834,E01026835,E01026836,E01026837,E01026838,E01026839,E01026840,E01026841,E01026842,E01026843,E01026844,E01026845,E01026846,E01026848&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_051.json.gz + content_sha256: c30d69d31a1679d417c5f3ce6c8d29e2d90ea0bf2a54e95888bca22514785a8b +- id: census_lsoa_tenure5a_bedrooms_052 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 052 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01026849,E01026850,E01026852,E01026853,E01026855,E01026856,E01026857,E01026858,E01026859,E01026860,E01026861,E01026862,E01026863,E01026864,E01026865,E01026866,E01026867,E01026868,E01026869,E01026870,E01026871,E01026872,E01026873,E01026874,E01026875,E01026876,E01026877,E01026878,E01026879,E01026880,E01026881,E01026883,E01026884,E01026885,E01026886,E01026887,E01026888,E01026889,E01026891,E01026892,E01026893,E01026894,E01026895,E01026896,E01026897,E01026898,E01026899,E01026900,E01026901,E01026902,E01026903,E01026904,E01026905,E01026906,E01026907,E01026908,E01026909,E01026910,E01026911,E01026912,E01026913,E01026914,E01026915,E01026916,E01026917,E01026918,E01026919,E01026920,E01026921,E01026922,E01026924,E01026925,E01026927,E01026928,E01026929,E01026930,E01026931,E01026932,E01026934,E01026935,E01026936,E01026937,E01026938,E01026939,E01026940,E01026941,E01026942,E01026943,E01026944,E01026947,E01026948,E01026949,E01026950,E01026951,E01026952,E01026953,E01026954,E01026955,E01026956,E01026957,E01026958,E01026959,E01026960,E01026961,E01026962,E01026963,E01026965,E01026966,E01026967,E01026968,E01026969,E01026970,E01026971,E01026972,E01026973,E01026974,E01026975,E01026976,E01026979,E01026980,E01026981,E01026982,E01026983,E01026984,E01026985,E01026986,E01026987,E01026988,E01026989,E01026991,E01026992,E01026993,E01026994,E01026998,E01026999,E01027000,E01027001,E01027002,E01027003,E01027004,E01027005,E01027006,E01027007,E01027008,E01027009,E01027010,E01027011,E01027012,E01027013,E01027015,E01027016,E01027017,E01027018,E01027019,E01027020,E01027021,E01027022,E01027023,E01027024,E01027025,E01027026,E01027027,E01027028,E01027029,E01027030,E01027031,E01027032,E01027033,E01027034,E01027035,E01027037,E01027038,E01027039,E01027040,E01027041,E01027042,E01027043,E01027044,E01027045,E01027046,E01027047,E01027048,E01027049,E01027050,E01027051,E01027052,E01027053,E01027054,E01027055,E01027056,E01027057,E01027058,E01027059,E01027060,E01027061,E01027062,E01027063,E01027064,E01027065,E01027066,E01027067,E01027069,E01027070,E01027071,E01027072,E01027073,E01027074,E01027075,E01027076,E01027078,E01027079,E01027080,E01027081,E01027082,E01027083,E01027084,E01027085,E01027086,E01027087,E01027088,E01027089,E01027090,E01027091,E01027092,E01027094,E01027095,E01027096,E01027097,E01027098,E01027099,E01027100,E01027102,E01027103,E01027104,E01027105,E01027106,E01027107,E01027108,E01027109,E01027111,E01027112,E01027113,E01027114,E01027115,E01027116,E01027117,E01027119,E01027120,E01027121,E01027122,E01027123,E01027125,E01027126,E01027127,E01027128,E01027129,E01027130,E01027131,E01027132,E01027133,E01027134,E01027135,E01027136,E01027137,E01027138,E01027139,E01027140,E01027141,E01027142,E01027143,E01027144,E01027145,E01027146,E01027147,E01027148,E01027149,E01027151,E01027152,E01027153,E01027154,E01027155,E01027156,E01027158,E01027159,E01027160,E01027161,E01027162,E01027163,E01027164,E01027165,E01027166,E01027167,E01027168,E01027169,E01027170,E01027171,E01027172,E01027173,E01027174,E01027175,E01027176,E01027177,E01027178,E01027179,E01027180,E01027181,E01027182,E01027183,E01027184,E01027185,E01027186,E01027187,E01027188,E01027189,E01027190,E01027191,E01027192,E01027193,E01027194,E01027195,E01027196,E01027197,E01027198,E01027199,E01027200,E01027201,E01027202,E01027203,E01027204,E01027205,E01027207,E01027208,E01027209,E01027210,E01027211,E01027212,E01027213,E01027214,E01027215,E01027216,E01027217,E01027218,E01027219,E01027220,E01027221,E01027222,E01027224,E01027225,E01027226,E01027227,E01027228,E01027229,E01027230,E01027231,E01027232,E01027233,E01027234,E01027235,E01027236,E01027237,E01027238,E01027239,E01027240,E01027241,E01027242,E01027243,E01027244,E01027245,E01027246,E01027247,E01027248,E01027249,E01027250,E01027252,E01027253,E01027254,E01027255,E01027256,E01027257,E01027258,E01027259,E01027260,E01027261,E01027262,E01027263,E01027264,E01027265,E01027267,E01027268,E01027269,E01027270,E01027271,E01027272,E01027273,E01027274,E01027275,E01027277,E01027279,E01027280,E01027281,E01027282,E01027283,E01027284,E01027285,E01027286,E01027287,E01027288,E01027289,E01027290,E01027291,E01027292,E01027293,E01027294,E01027295,E01027296,E01027297,E01027298,E01027299,E01027300,E01027301,E01027302,E01027303,E01027304,E01027305,E01027306,E01027307,E01027308,E01027309,E01027310,E01027311,E01027312,E01027313,E01027314,E01027316,E01027317,E01027318,E01027319,E01027320,E01027321,E01027322,E01027323,E01027324,E01027325,E01027326,E01027327,E01027328,E01027329,E01027330,E01027331,E01027332,E01027333,E01027334,E01027335,E01027336,E01027337,E01027338,E01027339,E01027340,E01027341,E01027342,E01027343,E01027344,E01027345,E01027346,E01027347,E01027348,E01027350,E01027351,E01027352,E01027353,E01027354,E01027355,E01027356,E01027357,E01027358,E01027359,E01027361,E01027362,E01027363,E01027364,E01027365,E01027366,E01027367,E01027368,E01027369,E01027370,E01027371,E01027372,E01027373,E01027374,E01027375,E01027376,E01027377,E01027378,E01027379,E01027380,E01027381,E01027382,E01027383,E01027384&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_052.json.gz + content_sha256: 3e3b6dffbb6028dbf780880e432917fb80b9c6d7ce91e22828512e696573541a +- id: census_lsoa_tenure5a_bedrooms_053 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 053 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01027385,E01027386,E01027387,E01027388,E01027389,E01027390,E01027391,E01027392,E01027393,E01027394,E01027395,E01027396,E01027397,E01027398,E01027399,E01027400,E01027401,E01027402,E01027403,E01027404,E01027405,E01027406,E01027407,E01027408,E01027409,E01027410,E01027411,E01027412,E01027413,E01027414,E01027415,E01027416,E01027417,E01027418,E01027419,E01027420,E01027421,E01027422,E01027423,E01027424,E01027426,E01027427,E01027428,E01027429,E01027430,E01027431,E01027432,E01027433,E01027434,E01027435,E01027436,E01027437,E01027438,E01027439,E01027440,E01027442,E01027443,E01027444,E01027445,E01027446,E01027447,E01027449,E01027450,E01027451,E01027452,E01027453,E01027455,E01027456,E01027457,E01027458,E01027460,E01027461,E01027462,E01027463,E01027464,E01027465,E01027466,E01027468,E01027469,E01027471,E01027472,E01027473,E01027474,E01027475,E01027476,E01027477,E01027478,E01027479,E01027480,E01027481,E01027482,E01027483,E01027484,E01027485,E01027486,E01027487,E01027488,E01027489,E01027490,E01027491,E01027492,E01027493,E01027494,E01027495,E01027496,E01027497,E01027498,E01027499,E01027500,E01027503,E01027504,E01027505,E01027507,E01027508,E01027509,E01027510,E01027511,E01027512,E01027513,E01027514,E01027515,E01027516,E01027517,E01027518,E01027519,E01027520,E01027521,E01027522,E01027523,E01027524,E01027525,E01027526,E01027527,E01027528,E01027529,E01027530,E01027531,E01027532,E01027533,E01027534,E01027535,E01027536,E01027537,E01027538,E01027539,E01027540,E01027541,E01027542,E01027543,E01027544,E01027545,E01027546,E01027549,E01027550,E01027551,E01027552,E01027553,E01027554,E01027555,E01027556,E01027557,E01027558,E01027559,E01027560,E01027561,E01027562,E01027563,E01027564,E01027565,E01027566,E01027567,E01027568,E01027569,E01027570,E01027571,E01027572,E01027573,E01027574,E01027575,E01027576,E01027577,E01027578,E01027579,E01027580,E01027582,E01027583,E01027584,E01027585,E01027586,E01027587,E01027588,E01027589,E01027590,E01027591,E01027592,E01027593,E01027594,E01027595,E01027596,E01027597,E01027598,E01027599,E01027600,E01027601,E01027603,E01027604,E01027605,E01027606,E01027607,E01027608,E01027609,E01027610,E01027612,E01027613,E01027614,E01027615,E01027616,E01027617,E01027618,E01027619,E01027620,E01027622,E01027623,E01027624,E01027625,E01027626,E01027627,E01027628,E01027629,E01027630,E01027631,E01027632,E01027633,E01027634,E01027635,E01027636,E01027637,E01027638,E01027639,E01027640,E01027641,E01027642,E01027643,E01027644,E01027645,E01027646,E01027647,E01027648,E01027649,E01027650,E01027651,E01027652,E01027653,E01027654,E01027655,E01027656,E01027657,E01027658,E01027659,E01027660,E01027661,E01027662,E01027663,E01027665,E01027666,E01027667,E01027668,E01027669,E01027670,E01027671,E01027672,E01027673,E01027674,E01027675,E01027676,E01027677,E01027678,E01027679,E01027680,E01027681,E01027682,E01027683,E01027684,E01027685,E01027686,E01027687,E01027688,E01027689,E01027690,E01027691,E01027692,E01027693,E01027694,E01027695,E01027696,E01027697,E01027698,E01027699,E01027700,E01027701,E01027702,E01027703,E01027704,E01027705,E01027706,E01027707,E01027708,E01027709,E01027710,E01027711,E01027712,E01027713,E01027714,E01027715,E01027716,E01027717,E01027718,E01027719,E01027720,E01027721,E01027722,E01027723,E01027724,E01027725,E01027726,E01027727,E01027728,E01027729,E01027730,E01027731,E01027732,E01027733,E01027734,E01027735,E01027736,E01027737,E01027738,E01027739,E01027740,E01027741,E01027742,E01027743,E01027744,E01027745,E01027746,E01027747,E01027748,E01027749,E01027751,E01027752,E01027753,E01027756,E01027758,E01027759,E01027760,E01027761,E01027762,E01027763,E01027764,E01027765,E01027766,E01027767,E01027768,E01027769,E01027770,E01027771,E01027772,E01027773,E01027774,E01027775,E01027776,E01027777,E01027778,E01027779,E01027780,E01027781,E01027782,E01027783,E01027784,E01027786,E01027787,E01027788,E01027790,E01027791,E01027792,E01027793,E01027794,E01027795,E01027796,E01027797,E01027798,E01027799,E01027800,E01027801,E01027802,E01027803,E01027804,E01027805,E01027806,E01027807,E01027808,E01027809,E01027810,E01027811,E01027812,E01027813,E01027814,E01027815,E01027816,E01027817,E01027818,E01027819,E01027820,E01027821,E01027822,E01027823,E01027824,E01027825,E01027826,E01027827,E01027828,E01027829,E01027830,E01027833,E01027834,E01027835,E01027836,E01027837,E01027838,E01027839,E01027840,E01027841,E01027842,E01027843,E01027844,E01027845,E01027847,E01027849,E01027850,E01027851,E01027852,E01027853,E01027854,E01027855,E01027856,E01027857,E01027858,E01027859,E01027860,E01027861,E01027862,E01027863,E01027864,E01027865,E01027866,E01027867,E01027868,E01027871,E01027872,E01027873,E01027874,E01027875,E01027876,E01027877,E01027878,E01027879,E01027880,E01027881,E01027882,E01027883,E01027884,E01027885,E01027886,E01027887,E01027888,E01027889,E01027890,E01027891,E01027892,E01027893,E01027894,E01027895,E01027896,E01027897,E01027899,E01027900,E01027901,E01027902,E01027903,E01027904,E01027906,E01027907,E01027908,E01027909,E01027913,E01027914,E01027915,E01027916,E01027918,E01027919&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_053.json.gz + content_sha256: 8a0b4ab76408fdd610d421ba253a08aa53b6c3743ef9d49f9161aabf3a296815 +- id: census_lsoa_tenure5a_bedrooms_054 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 054 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01027920,E01027921,E01027922,E01027923,E01027924,E01027925,E01027926,E01027927,E01027928,E01027930,E01027932,E01027933,E01027934,E01027935,E01027936,E01027937,E01027938,E01027939,E01027940,E01027941,E01027942,E01027943,E01027944,E01027945,E01027946,E01027947,E01027948,E01027949,E01027950,E01027951,E01027952,E01027953,E01027954,E01027955,E01027956,E01027957,E01027958,E01027959,E01027960,E01027961,E01027962,E01027963,E01027964,E01027965,E01027966,E01027967,E01027968,E01027969,E01027970,E01027971,E01027972,E01027973,E01027974,E01027975,E01027976,E01027977,E01027978,E01027979,E01027980,E01027981,E01027982,E01027983,E01027984,E01027985,E01027986,E01027987,E01027988,E01027989,E01027990,E01027991,E01027992,E01027993,E01027994,E01027995,E01027996,E01027997,E01027998,E01027999,E01028000,E01028001,E01028002,E01028003,E01028004,E01028005,E01028006,E01028007,E01028008,E01028009,E01028010,E01028011,E01028012,E01028013,E01028014,E01028015,E01028016,E01028017,E01028018,E01028019,E01028020,E01028021,E01028022,E01028023,E01028024,E01028025,E01028026,E01028027,E01028028,E01028029,E01028030,E01028031,E01028032,E01028033,E01028034,E01028035,E01028036,E01028037,E01028038,E01028039,E01028042,E01028043,E01028044,E01028045,E01028046,E01028047,E01028048,E01028049,E01028050,E01028051,E01028052,E01028053,E01028054,E01028055,E01028056,E01028057,E01028058,E01028059,E01028060,E01028061,E01028062,E01028063,E01028064,E01028065,E01028066,E01028067,E01028068,E01028069,E01028070,E01028073,E01028074,E01028075,E01028076,E01028077,E01028078,E01028079,E01028080,E01028081,E01028082,E01028083,E01028084,E01028085,E01028086,E01028087,E01028088,E01028089,E01028090,E01028091,E01028092,E01028093,E01028094,E01028095,E01028096,E01028097,E01028098,E01028099,E01028100,E01028101,E01028102,E01028103,E01028104,E01028105,E01028106,E01028107,E01028108,E01028109,E01028110,E01028111,E01028112,E01028113,E01028114,E01028115,E01028116,E01028117,E01028118,E01028119,E01028120,E01028121,E01028122,E01028123,E01028124,E01028125,E01028126,E01028127,E01028128,E01028129,E01028130,E01028131,E01028132,E01028133,E01028134,E01028135,E01028136,E01028137,E01028138,E01028139,E01028142,E01028143,E01028144,E01028146,E01028147,E01028148,E01028149,E01028150,E01028151,E01028152,E01028153,E01028154,E01028155,E01028156,E01028157,E01028158,E01028159,E01028160,E01028161,E01028162,E01028163,E01028164,E01028165,E01028166,E01028167,E01028168,E01028169,E01028170,E01028171,E01028172,E01028173,E01028174,E01028175,E01028176,E01028177,E01028178,E01028179,E01028180,E01028181,E01028182,E01028183,E01028184,E01028185,E01028186,E01028187,E01028188,E01028189,E01028190,E01028191,E01028192,E01028193,E01028194,E01028195,E01028196,E01028197,E01028198,E01028199,E01028200,E01028201,E01028202,E01028203,E01028204,E01028205,E01028206,E01028207,E01028208,E01028209,E01028210,E01028211,E01028212,E01028213,E01028214,E01028215,E01028216,E01028217,E01028218,E01028219,E01028220,E01028222,E01028223,E01028224,E01028225,E01028226,E01028227,E01028228,E01028229,E01028230,E01028231,E01028232,E01028233,E01028234,E01028235,E01028236,E01028237,E01028238,E01028239,E01028240,E01028242,E01028243,E01028244,E01028245,E01028246,E01028247,E01028248,E01028249,E01028250,E01028251,E01028252,E01028253,E01028254,E01028255,E01028256,E01028257,E01028258,E01028259,E01028260,E01028261,E01028262,E01028263,E01028264,E01028267,E01028268,E01028269,E01028270,E01028271,E01028272,E01028273,E01028274,E01028275,E01028276,E01028277,E01028278,E01028279,E01028280,E01028281,E01028282,E01028283,E01028284,E01028285,E01028286,E01028287,E01028288,E01028289,E01028290,E01028291,E01028292,E01028293,E01028294,E01028295,E01028296,E01028297,E01028298,E01028299,E01028300,E01028301,E01028302,E01028303,E01028304,E01028305,E01028306,E01028307,E01028308,E01028309,E01028310,E01028311,E01028312,E01028313,E01028315,E01028316,E01028317,E01028318,E01028319,E01028320,E01028321,E01028322,E01028323,E01028325,E01028326,E01028327,E01028328,E01028329,E01028330,E01028331,E01028332,E01028333,E01028334,E01028335,E01028336,E01028337,E01028338,E01028339,E01028340,E01028342,E01028343,E01028344,E01028345,E01028346,E01028347,E01028348,E01028349,E01028350,E01028351,E01028352,E01028353,E01028354,E01028355,E01028356,E01028357,E01028358,E01028359,E01028360,E01028361,E01028362,E01028363,E01028364,E01028365,E01028366,E01028367,E01028368,E01028369,E01028370,E01028371,E01028372,E01028374,E01028375,E01028376,E01028377,E01028378,E01028379,E01028380,E01028381,E01028382,E01028383,E01028384,E01028385,E01028386,E01028387,E01028388,E01028389,E01028390,E01028391,E01028392,E01028393,E01028394,E01028395,E01028396,E01028397,E01028398,E01028399,E01028400,E01028401,E01028402,E01028403,E01028404,E01028405,E01028406,E01028407,E01028408,E01028409,E01028410,E01028411,E01028412,E01028413,E01028414,E01028415,E01028416,E01028417,E01028418,E01028419,E01028420,E01028421,E01028422,E01028423,E01028426,E01028427,E01028428,E01028429,E01028430,E01028431,E01028432,E01028433,E01028434,E01028435,E01028437,E01028439,E01028440&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_054.json.gz + content_sha256: 55cb7ef91e0b9e410ad79019018f032db44bef6a5a27055ed8f902c0ef4d8710 +- id: census_lsoa_tenure5a_bedrooms_055 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 055 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01028441,E01028443,E01028444,E01028445,E01028446,E01028447,E01028448,E01028449,E01028450,E01028451,E01028452,E01028453,E01028454,E01028455,E01028456,E01028457,E01028458,E01028459,E01028460,E01028461,E01028462,E01028463,E01028464,E01028465,E01028466,E01028467,E01028469,E01028470,E01028471,E01028472,E01028473,E01028474,E01028476,E01028477,E01028478,E01028479,E01028480,E01028481,E01028482,E01028483,E01028484,E01028485,E01028486,E01028487,E01028488,E01028489,E01028490,E01028491,E01028492,E01028493,E01028494,E01028495,E01028496,E01028497,E01028498,E01028499,E01028500,E01028501,E01028502,E01028503,E01028504,E01028506,E01028507,E01028508,E01028510,E01028511,E01028512,E01028513,E01028514,E01028515,E01028516,E01028517,E01028518,E01028519,E01028520,E01028521,E01028522,E01028523,E01028524,E01028525,E01028526,E01028527,E01028528,E01028529,E01028530,E01028531,E01028532,E01028534,E01028535,E01028536,E01028537,E01028538,E01028539,E01028540,E01028541,E01028542,E01028543,E01028544,E01028545,E01028546,E01028547,E01028548,E01028549,E01028550,E01028551,E01028552,E01028553,E01028554,E01028555,E01028556,E01028557,E01028558,E01028559,E01028560,E01028561,E01028562,E01028563,E01028565,E01028567,E01028568,E01028569,E01028570,E01028571,E01028572,E01028573,E01028574,E01028575,E01028576,E01028577,E01028578,E01028579,E01028580,E01028581,E01028582,E01028584,E01028586,E01028587,E01028588,E01028591,E01028592,E01028594,E01028595,E01028596,E01028597,E01028598,E01028599,E01028600,E01028601,E01028602,E01028603,E01028604,E01028605,E01028606,E01028607,E01028608,E01028609,E01028610,E01028611,E01028612,E01028613,E01028614,E01028615,E01028616,E01028617,E01028619,E01028622,E01028623,E01028624,E01028625,E01028626,E01028627,E01028628,E01028629,E01028630,E01028631,E01028632,E01028633,E01028634,E01028635,E01028636,E01028637,E01028638,E01028639,E01028640,E01028641,E01028642,E01028643,E01028644,E01028645,E01028646,E01028647,E01028649,E01028650,E01028651,E01028652,E01028653,E01028654,E01028655,E01028656,E01028657,E01028658,E01028659,E01028660,E01028661,E01028662,E01028663,E01028664,E01028665,E01028666,E01028667,E01028668,E01028669,E01028670,E01028671,E01028672,E01028673,E01028674,E01028675,E01028676,E01028677,E01028678,E01028679,E01028680,E01028681,E01028682,E01028683,E01028684,E01028685,E01028686,E01028688,E01028689,E01028690,E01028691,E01028692,E01028693,E01028694,E01028695,E01028696,E01028697,E01028698,E01028699,E01028700,E01028701,E01028702,E01028703,E01028704,E01028705,E01028706,E01028707,E01028708,E01028709,E01028710,E01028711,E01028712,E01028713,E01028715,E01028716,E01028717,E01028718,E01028719,E01028720,E01028721,E01028722,E01028723,E01028724,E01028725,E01028726,E01028728,E01028729,E01028730,E01028731,E01028732,E01028733,E01028734,E01028735,E01028737,E01028738,E01028739,E01028740,E01028741,E01028742,E01028743,E01028744,E01028745,E01028746,E01028747,E01028748,E01028749,E01028750,E01028751,E01028752,E01028753,E01028754,E01028755,E01028757,E01028758,E01028759,E01028760,E01028761,E01028762,E01028763,E01028764,E01028765,E01028767,E01028771,E01028772,E01028773,E01028774,E01028775,E01028776,E01028777,E01028778,E01028779,E01028780,E01028781,E01028782,E01028783,E01028784,E01028785,E01028786,E01028787,E01028788,E01028789,E01028790,E01028791,E01028792,E01028793,E01028794,E01028795,E01028796,E01028797,E01028798,E01028799,E01028800,E01028801,E01028802,E01028804,E01028806,E01028807,E01028808,E01028809,E01028810,E01028811,E01028813,E01028814,E01028815,E01028816,E01028817,E01028818,E01028819,E01028820,E01028821,E01028822,E01028823,E01028824,E01028825,E01028826,E01028827,E01028828,E01028829,E01028830,E01028831,E01028832,E01028833,E01028834,E01028835,E01028836,E01028837,E01028838,E01028839,E01028840,E01028841,E01028842,E01028843,E01028844,E01028845,E01028846,E01028847,E01028848,E01028849,E01028850,E01028851,E01028852,E01028853,E01028854,E01028855,E01028856,E01028858,E01028859,E01028860,E01028861,E01028862,E01028863,E01028864,E01028865,E01028866,E01028867,E01028868,E01028869,E01028870,E01028871,E01028872,E01028873,E01028874,E01028875,E01028876,E01028877,E01028878,E01028879,E01028880,E01028881,E01028882,E01028883,E01028884,E01028886,E01028887,E01028888,E01028889,E01028890,E01028891,E01028892,E01028893,E01028894,E01028895,E01028896,E01028897,E01028898,E01028899,E01028902,E01028903,E01028904,E01028905,E01028906,E01028907,E01028908,E01028909,E01028910,E01028911,E01028912,E01028913,E01028914,E01028915,E01028916,E01028917,E01028918,E01028919,E01028920,E01028921,E01028922,E01028923,E01028924,E01028925,E01028926,E01028927,E01028928,E01028929,E01028930,E01028931,E01028933,E01028934,E01028935,E01028936,E01028937,E01028938,E01028939,E01028940,E01028942,E01028943,E01028944,E01028945,E01028947,E01028948,E01028949,E01028950,E01028951,E01028952,E01028953,E01028954,E01028955,E01028956,E01028957,E01028958,E01028960,E01028961,E01028962,E01028963,E01028964,E01028965,E01028966,E01028967,E01028968,E01028969,E01028970,E01028971,E01028972,E01028973,E01028974,E01028975,E01028976,E01028977&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_055.json.gz + content_sha256: e06e7c217d366a8217e216a1a4babcb925bcdb3704112c25ab9d16c0be223b7f +- id: census_lsoa_tenure5a_bedrooms_056 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 056 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01028978,E01028979,E01028980,E01028981,E01028982,E01028983,E01028984,E01028985,E01028986,E01028988,E01028989,E01028990,E01028992,E01028993,E01028994,E01028995,E01028996,E01028997,E01028998,E01028999,E01029000,E01029001,E01029002,E01029003,E01029005,E01029006,E01029007,E01029009,E01029010,E01029011,E01029012,E01029013,E01029014,E01029016,E01029018,E01029019,E01029020,E01029021,E01029022,E01029023,E01029024,E01029025,E01029027,E01029028,E01029029,E01029030,E01029031,E01029032,E01029033,E01029034,E01029036,E01029037,E01029038,E01029039,E01029040,E01029041,E01029042,E01029043,E01029044,E01029045,E01029046,E01029047,E01029050,E01029051,E01029052,E01029053,E01029054,E01029055,E01029056,E01029057,E01029058,E01029059,E01029060,E01029062,E01029063,E01029064,E01029065,E01029066,E01029067,E01029068,E01029070,E01029071,E01029072,E01029073,E01029074,E01029075,E01029076,E01029077,E01029078,E01029079,E01029080,E01029081,E01029082,E01029083,E01029084,E01029085,E01029086,E01029089,E01029091,E01029092,E01029093,E01029094,E01029095,E01029096,E01029097,E01029098,E01029099,E01029100,E01029101,E01029102,E01029103,E01029104,E01029105,E01029106,E01029107,E01029108,E01029109,E01029110,E01029111,E01029112,E01029113,E01029114,E01029115,E01029116,E01029117,E01029118,E01029119,E01029120,E01029121,E01029122,E01029123,E01029124,E01029125,E01029126,E01029127,E01029128,E01029129,E01029130,E01029131,E01029132,E01029133,E01029134,E01029135,E01029136,E01029137,E01029138,E01029139,E01029142,E01029143,E01029144,E01029145,E01029147,E01029148,E01029149,E01029150,E01029151,E01029152,E01029153,E01029154,E01029155,E01029156,E01029157,E01029158,E01029159,E01029161,E01029162,E01029163,E01029164,E01029165,E01029166,E01029167,E01029168,E01029169,E01029170,E01029171,E01029172,E01029173,E01029174,E01029175,E01029176,E01029177,E01029178,E01029179,E01029180,E01029181,E01029182,E01029183,E01029184,E01029185,E01029186,E01029187,E01029188,E01029189,E01029190,E01029191,E01029192,E01029193,E01029194,E01029195,E01029196,E01029197,E01029198,E01029199,E01029200,E01029201,E01029202,E01029203,E01029204,E01029205,E01029206,E01029207,E01029208,E01029209,E01029210,E01029211,E01029212,E01029213,E01029214,E01029215,E01029216,E01029217,E01029218,E01029219,E01029220,E01029221,E01029222,E01029223,E01029224,E01029225,E01029226,E01029227,E01029228,E01029229,E01029230,E01029231,E01029232,E01029233,E01029235,E01029236,E01029237,E01029238,E01029239,E01029240,E01029241,E01029242,E01029243,E01029244,E01029245,E01029246,E01029247,E01029248,E01029249,E01029250,E01029251,E01029252,E01029254,E01029255,E01029256,E01029257,E01029258,E01029259,E01029260,E01029261,E01029262,E01029263,E01029264,E01029265,E01029266,E01029267,E01029268,E01029269,E01029270,E01029271,E01029273,E01029274,E01029275,E01029276,E01029277,E01029278,E01029279,E01029280,E01029281,E01029282,E01029283,E01029284,E01029285,E01029286,E01029287,E01029288,E01029289,E01029290,E01029291,E01029292,E01029293,E01029294,E01029295,E01029296,E01029297,E01029298,E01029299,E01029300,E01029301,E01029302,E01029303,E01029304,E01029305,E01029306,E01029307,E01029308,E01029310,E01029311,E01029312,E01029313,E01029315,E01029316,E01029317,E01029319,E01029320,E01029321,E01029322,E01029324,E01029325,E01029326,E01029327,E01029328,E01029329,E01029330,E01029331,E01029333,E01029334,E01029335,E01029336,E01029337,E01029338,E01029339,E01029341,E01029342,E01029343,E01029345,E01029346,E01029347,E01029348,E01029349,E01029350,E01029351,E01029352,E01029353,E01029354,E01029355,E01029356,E01029357,E01029358,E01029359,E01029360,E01029361,E01029362,E01029363,E01029364,E01029365,E01029366,E01029367,E01029368,E01029369,E01029370,E01029371,E01029372,E01029373,E01029374,E01029375,E01029376,E01029377,E01029378,E01029379,E01029380,E01029381,E01029382,E01029383,E01029385,E01029386,E01029387,E01029388,E01029389,E01029390,E01029391,E01029392,E01029393,E01029394,E01029395,E01029396,E01029397,E01029398,E01029399,E01029400,E01029401,E01029402,E01029403,E01029404,E01029405,E01029406,E01029407,E01029408,E01029409,E01029410,E01029411,E01029412,E01029413,E01029414,E01029415,E01029416,E01029417,E01029418,E01029419,E01029420,E01029422,E01029423,E01029424,E01029425,E01029426,E01029427,E01029428,E01029429,E01029430,E01029431,E01029432,E01029433,E01029434,E01029435,E01029436,E01029437,E01029438,E01029439,E01029440,E01029441,E01029442,E01029443,E01029444,E01029445,E01029446,E01029447,E01029448,E01029449,E01029450,E01029451,E01029452,E01029453,E01029454,E01029455,E01029456,E01029457,E01029458,E01029459,E01029460,E01029461,E01029462,E01029463,E01029464,E01029465,E01029466,E01029467,E01029468,E01029469,E01029470,E01029471,E01029472,E01029473,E01029474,E01029475,E01029476,E01029477,E01029478,E01029480,E01029482,E01029483,E01029484,E01029485,E01029486,E01029487,E01029488,E01029489,E01029490,E01029491,E01029492,E01029493,E01029494,E01029495,E01029496,E01029498,E01029499,E01029500,E01029501,E01029502,E01029503,E01029504,E01029505,E01029506,E01029507,E01029508,E01029509,E01029511,E01029512&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_056.json.gz + content_sha256: 5676d1faffca42028dab24e147ab1cbb356de1e239b814dc692e11fdbe78ce01 +- id: census_lsoa_tenure5a_bedrooms_057 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 057 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01029513,E01029514,E01029515,E01029516,E01029517,E01029518,E01029519,E01029520,E01029521,E01029522,E01029523,E01029524,E01029525,E01029526,E01029527,E01029528,E01029529,E01029531,E01029532,E01029533,E01029534,E01029535,E01029536,E01029537,E01029538,E01029539,E01029540,E01029541,E01029542,E01029543,E01029544,E01029545,E01029546,E01029547,E01029548,E01029549,E01029551,E01029552,E01029553,E01029554,E01029555,E01029556,E01029557,E01029558,E01029559,E01029560,E01029561,E01029562,E01029563,E01029564,E01029565,E01029566,E01029567,E01029568,E01029569,E01029570,E01029571,E01029572,E01029573,E01029574,E01029575,E01029576,E01029577,E01029578,E01029579,E01029580,E01029581,E01029582,E01029583,E01029584,E01029585,E01029586,E01029587,E01029588,E01029589,E01029590,E01029591,E01029593,E01029594,E01029595,E01029596,E01029597,E01029598,E01029599,E01029600,E01029601,E01029602,E01029603,E01029604,E01029605,E01029606,E01029607,E01029608,E01029609,E01029610,E01029611,E01029612,E01029613,E01029614,E01029615,E01029616,E01029617,E01029618,E01029619,E01029620,E01029621,E01029622,E01029623,E01029624,E01029625,E01029626,E01029627,E01029628,E01029629,E01029630,E01029631,E01029632,E01029633,E01029634,E01029635,E01029636,E01029637,E01029638,E01029639,E01029640,E01029641,E01029642,E01029643,E01029644,E01029645,E01029646,E01029647,E01029648,E01029649,E01029650,E01029651,E01029652,E01029653,E01029654,E01029655,E01029656,E01029657,E01029658,E01029659,E01029660,E01029661,E01029662,E01029663,E01029664,E01029665,E01029666,E01029667,E01029668,E01029669,E01029670,E01029671,E01029672,E01029673,E01029674,E01029675,E01029676,E01029677,E01029678,E01029679,E01029680,E01029681,E01029682,E01029683,E01029684,E01029685,E01029686,E01029687,E01029688,E01029689,E01029690,E01029691,E01029692,E01029693,E01029694,E01029695,E01029696,E01029697,E01029698,E01029699,E01029700,E01029702,E01029703,E01029704,E01029705,E01029706,E01029707,E01029708,E01029709,E01029710,E01029711,E01029712,E01029713,E01029714,E01029715,E01029716,E01029717,E01029718,E01029719,E01029720,E01029721,E01029722,E01029723,E01029724,E01029725,E01029726,E01029727,E01029728,E01029729,E01029733,E01029734,E01029735,E01029736,E01029737,E01029738,E01029739,E01029740,E01029741,E01029742,E01029743,E01029744,E01029745,E01029746,E01029747,E01029748,E01029749,E01029750,E01029751,E01029752,E01029753,E01029754,E01029755,E01029756,E01029757,E01029758,E01029759,E01029760,E01029761,E01029762,E01029763,E01029764,E01029765,E01029766,E01029767,E01029768,E01029769,E01029770,E01029771,E01029772,E01029773,E01029774,E01029775,E01029776,E01029777,E01029778,E01029779,E01029780,E01029781,E01029782,E01029783,E01029784,E01029785,E01029786,E01029787,E01029788,E01029789,E01029790,E01029791,E01029792,E01029793,E01029794,E01029795,E01029796,E01029797,E01029798,E01029799,E01029800,E01029801,E01029802,E01029803,E01029804,E01029805,E01029806,E01029807,E01029808,E01029809,E01029810,E01029811,E01029812,E01029813,E01029814,E01029815,E01029816,E01029817,E01029818,E01029819,E01029820,E01029821,E01029822,E01029823,E01029824,E01029825,E01029826,E01029827,E01029828,E01029829,E01029830,E01029831,E01029832,E01029833,E01029834,E01029835,E01029836,E01029837,E01029838,E01029839,E01029840,E01029841,E01029842,E01029843,E01029844,E01029845,E01029846,E01029847,E01029848,E01029849,E01029850,E01029851,E01029852,E01029853,E01029854,E01029855,E01029856,E01029857,E01029858,E01029859,E01029860,E01029861,E01029862,E01029863,E01029864,E01029865,E01029867,E01029868,E01029869,E01029870,E01029871,E01029872,E01029873,E01029874,E01029875,E01029876,E01029877,E01029878,E01029879,E01029880,E01029881,E01029882,E01029883,E01029884,E01029885,E01029886,E01029887,E01029888,E01029889,E01029890,E01029891,E01029892,E01029894,E01029895,E01029896,E01029897,E01029898,E01029899,E01029900,E01029901,E01029902,E01029903,E01029904,E01029905,E01029906,E01029907,E01029908,E01029909,E01029910,E01029911,E01029912,E01029913,E01029914,E01029915,E01029916,E01029917,E01029918,E01029919,E01029920,E01029921,E01029922,E01029923,E01029925,E01029926,E01029927,E01029928,E01029929,E01029930,E01029937,E01029938,E01029939,E01029940,E01029941,E01029942,E01029944,E01029945,E01029946,E01029947,E01029949,E01029950,E01029951,E01029952,E01029953,E01029955,E01029956,E01029957,E01029958,E01029960,E01029961,E01029962,E01029963,E01029964,E01029965,E01029966,E01029967,E01029968,E01029969,E01029970,E01029971,E01029973,E01029974,E01029975,E01029976,E01029977,E01029978,E01029979,E01029980,E01029981,E01029982,E01029983,E01029984,E01029985,E01029986,E01029987,E01029988,E01029990,E01029991,E01029992,E01029994,E01029995,E01029996,E01029997,E01029998,E01029999,E01030000,E01030001,E01030002,E01030003,E01030004,E01030005,E01030006,E01030007,E01030008,E01030009,E01030010,E01030011,E01030012,E01030013,E01030014,E01030015,E01030016,E01030017,E01030018,E01030019,E01030020,E01030021,E01030022,E01030023,E01030024,E01030025,E01030026,E01030027,E01030028,E01030029,E01030030,E01030031,E01030032,E01030033,E01030034,E01030035&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_057.json.gz + content_sha256: a56380236b3ba115265d14756944f7e58b8d086c7b80300ffb55bceafca3dcef +- id: census_lsoa_tenure5a_bedrooms_058 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 058 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01030036,E01030037,E01030038,E01030039,E01030040,E01030041,E01030042,E01030043,E01030044,E01030045,E01030046,E01030047,E01030048,E01030049,E01030050,E01030051,E01030052,E01030053,E01030054,E01030055,E01030056,E01030057,E01030058,E01030059,E01030060,E01030061,E01030062,E01030063,E01030064,E01030065,E01030066,E01030067,E01030068,E01030069,E01030070,E01030071,E01030072,E01030073,E01030075,E01030076,E01030077,E01030078,E01030079,E01030080,E01030081,E01030083,E01030084,E01030085,E01030086,E01030087,E01030088,E01030089,E01030090,E01030091,E01030092,E01030093,E01030094,E01030095,E01030096,E01030097,E01030098,E01030099,E01030100,E01030101,E01030104,E01030105,E01030106,E01030107,E01030108,E01030109,E01030110,E01030111,E01030112,E01030113,E01030114,E01030115,E01030116,E01030117,E01030118,E01030119,E01030121,E01030122,E01030123,E01030124,E01030125,E01030127,E01030128,E01030129,E01030130,E01030131,E01030132,E01030133,E01030134,E01030135,E01030137,E01030138,E01030139,E01030140,E01030141,E01030142,E01030143,E01030144,E01030145,E01030146,E01030147,E01030148,E01030149,E01030150,E01030151,E01030152,E01030153,E01030154,E01030155,E01030158,E01030159,E01030160,E01030161,E01030162,E01030163,E01030164,E01030165,E01030166,E01030167,E01030168,E01030169,E01030170,E01030171,E01030172,E01030173,E01030174,E01030175,E01030176,E01030179,E01030180,E01030181,E01030183,E01030184,E01030185,E01030186,E01030187,E01030188,E01030189,E01030190,E01030191,E01030192,E01030193,E01030194,E01030195,E01030196,E01030197,E01030198,E01030199,E01030200,E01030201,E01030203,E01030204,E01030205,E01030206,E01030207,E01030208,E01030209,E01030210,E01030211,E01030212,E01030214,E01030215,E01030216,E01030217,E01030218,E01030219,E01030220,E01030221,E01030222,E01030223,E01030224,E01030225,E01030226,E01030227,E01030228,E01030229,E01030230,E01030231,E01030232,E01030233,E01030234,E01030235,E01030236,E01030237,E01030238,E01030239,E01030240,E01030241,E01030242,E01030243,E01030244,E01030245,E01030246,E01030247,E01030248,E01030249,E01030250,E01030251,E01030252,E01030253,E01030254,E01030255,E01030256,E01030257,E01030258,E01030259,E01030260,E01030261,E01030262,E01030263,E01030264,E01030265,E01030266,E01030267,E01030268,E01030269,E01030270,E01030271,E01030272,E01030273,E01030274,E01030275,E01030276,E01030277,E01030278,E01030279,E01030280,E01030281,E01030283,E01030285,E01030286,E01030287,E01030288,E01030289,E01030290,E01030291,E01030292,E01030293,E01030294,E01030295,E01030296,E01030297,E01030298,E01030299,E01030300,E01030301,E01030302,E01030303,E01030304,E01030305,E01030306,E01030307,E01030308,E01030309,E01030310,E01030311,E01030312,E01030313,E01030314,E01030315,E01030316,E01030317,E01030318,E01030319,E01030320,E01030321,E01030322,E01030323,E01030324,E01030325,E01030326,E01030327,E01030328,E01030329,E01030330,E01030331,E01030332,E01030333,E01030334,E01030335,E01030336,E01030337,E01030338,E01030339,E01030340,E01030341,E01030342,E01030343,E01030344,E01030345,E01030346,E01030347,E01030348,E01030349,E01030350,E01030351,E01030352,E01030353,E01030354,E01030355,E01030356,E01030357,E01030358,E01030360,E01030361,E01030362,E01030363,E01030364,E01030365,E01030366,E01030367,E01030368,E01030369,E01030370,E01030371,E01030372,E01030373,E01030374,E01030375,E01030376,E01030377,E01030378,E01030379,E01030380,E01030381,E01030382,E01030384,E01030385,E01030386,E01030387,E01030388,E01030389,E01030390,E01030391,E01030392,E01030393,E01030394,E01030395,E01030396,E01030398,E01030399,E01030400,E01030401,E01030402,E01030403,E01030404,E01030405,E01030406,E01030407,E01030408,E01030409,E01030410,E01030412,E01030413,E01030414,E01030415,E01030416,E01030417,E01030418,E01030419,E01030420,E01030421,E01030422,E01030423,E01030424,E01030425,E01030426,E01030427,E01030428,E01030429,E01030430,E01030431,E01030432,E01030433,E01030434,E01030435,E01030436,E01030437,E01030438,E01030439,E01030440,E01030441,E01030442,E01030443,E01030444,E01030445,E01030446,E01030447,E01030448,E01030449,E01030450,E01030451,E01030452,E01030453,E01030454,E01030455,E01030456,E01030457,E01030458,E01030459,E01030460,E01030461,E01030462,E01030463,E01030464,E01030465,E01030466,E01030467,E01030468,E01030469,E01030470,E01030471,E01030472,E01030473,E01030474,E01030475,E01030476,E01030477,E01030478,E01030479,E01030480,E01030481,E01030482,E01030483,E01030484,E01030485,E01030486,E01030487,E01030488,E01030489,E01030490,E01030491,E01030492,E01030493,E01030494,E01030495,E01030496,E01030497,E01030498,E01030499,E01030500,E01030501,E01030502,E01030503,E01030504,E01030505,E01030506,E01030507,E01030508,E01030509,E01030510,E01030511,E01030512,E01030513,E01030514,E01030515,E01030516,E01030517,E01030518,E01030519,E01030520,E01030521,E01030522,E01030523,E01030524,E01030525,E01030526,E01030527,E01030528,E01030529,E01030530,E01030531,E01030532,E01030533,E01030534,E01030535,E01030536,E01030537,E01030538,E01030539,E01030540,E01030541,E01030542,E01030543,E01030544,E01030545,E01030546,E01030547,E01030548,E01030549,E01030550,E01030551,E01030552,E01030553,E01030554,E01030555&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_058.json.gz + content_sha256: 8112e503327781f9fbabb6acde99a6bb7729b380ca9e384d356da3cb33924f70 +- id: census_lsoa_tenure5a_bedrooms_059 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 059 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01030556,E01030557,E01030558,E01030559,E01030560,E01030561,E01030562,E01030563,E01030564,E01030565,E01030566,E01030567,E01030568,E01030569,E01030570,E01030571,E01030572,E01030573,E01030574,E01030576,E01030577,E01030578,E01030579,E01030580,E01030581,E01030582,E01030583,E01030585,E01030586,E01030587,E01030588,E01030589,E01030590,E01030591,E01030592,E01030593,E01030594,E01030595,E01030596,E01030597,E01030598,E01030599,E01030600,E01030601,E01030602,E01030603,E01030604,E01030605,E01030606,E01030607,E01030608,E01030609,E01030610,E01030613,E01030614,E01030615,E01030616,E01030617,E01030618,E01030619,E01030620,E01030621,E01030622,E01030623,E01030624,E01030625,E01030626,E01030627,E01030628,E01030629,E01030630,E01030631,E01030632,E01030633,E01030634,E01030635,E01030636,E01030637,E01030638,E01030639,E01030640,E01030641,E01030642,E01030643,E01030644,E01030645,E01030646,E01030647,E01030648,E01030649,E01030650,E01030651,E01030652,E01030653,E01030654,E01030655,E01030656,E01030657,E01030658,E01030659,E01030660,E01030661,E01030662,E01030663,E01030664,E01030665,E01030666,E01030667,E01030668,E01030669,E01030670,E01030671,E01030672,E01030673,E01030674,E01030675,E01030676,E01030677,E01030678,E01030679,E01030680,E01030681,E01030682,E01030683,E01030684,E01030685,E01030686,E01030687,E01030688,E01030689,E01030690,E01030691,E01030692,E01030693,E01030694,E01030695,E01030696,E01030697,E01030698,E01030699,E01030700,E01030701,E01030702,E01030703,E01030704,E01030705,E01030706,E01030707,E01030708,E01030709,E01030710,E01030711,E01030712,E01030713,E01030714,E01030715,E01030716,E01030717,E01030718,E01030719,E01030720,E01030721,E01030722,E01030723,E01030724,E01030725,E01030726,E01030727,E01030728,E01030729,E01030730,E01030731,E01030732,E01030733,E01030734,E01030735,E01030737,E01030738,E01030739,E01030740,E01030741,E01030742,E01030743,E01030744,E01030745,E01030746,E01030747,E01030748,E01030749,E01030750,E01030751,E01030752,E01030753,E01030754,E01030755,E01030756,E01030757,E01030758,E01030759,E01030760,E01030761,E01030762,E01030763,E01030764,E01030765,E01030766,E01030767,E01030768,E01030769,E01030770,E01030771,E01030772,E01030773,E01030774,E01030775,E01030776,E01030777,E01030778,E01030779,E01030780,E01030781,E01030782,E01030783,E01030784,E01030785,E01030786,E01030787,E01030788,E01030789,E01030790,E01030791,E01030792,E01030793,E01030794,E01030795,E01030796,E01030797,E01030798,E01030799,E01030800,E01030801,E01030802,E01030803,E01030804,E01030805,E01030806,E01030807,E01030808,E01030809,E01030810,E01030811,E01030812,E01030813,E01030814,E01030815,E01030816,E01030817,E01030818,E01030819,E01030820,E01030821,E01030822,E01030823,E01030824,E01030825,E01030826,E01030827,E01030828,E01030829,E01030830,E01030831,E01030832,E01030833,E01030834,E01030835,E01030836,E01030837,E01030838,E01030839,E01030840,E01030841,E01030842,E01030843,E01030844,E01030845,E01030846,E01030847,E01030848,E01030850,E01030851,E01030852,E01030853,E01030854,E01030855,E01030856,E01030857,E01030858,E01030859,E01030861,E01030862,E01030863,E01030864,E01030865,E01030866,E01030867,E01030868,E01030869,E01030870,E01030871,E01030872,E01030873,E01030874,E01030875,E01030876,E01030877,E01030878,E01030879,E01030880,E01030881,E01030882,E01030883,E01030884,E01030885,E01030886,E01030887,E01030888,E01030889,E01030890,E01030891,E01030892,E01030893,E01030894,E01030895,E01030896,E01030897,E01030898,E01030899,E01030900,E01030901,E01030902,E01030903,E01030904,E01030905,E01030906,E01030907,E01030908,E01030909,E01030910,E01030911,E01030912,E01030913,E01030914,E01030915,E01030916,E01030917,E01030918,E01030919,E01030920,E01030921,E01030922,E01030923,E01030924,E01030925,E01030926,E01030927,E01030928,E01030929,E01030930,E01030931,E01030932,E01030933,E01030934,E01030935,E01030937,E01030938,E01030939,E01030940,E01030941,E01030942,E01030943,E01030944,E01030945,E01030946,E01030947,E01030948,E01030949,E01030950,E01030951,E01030952,E01030953,E01030954,E01030955,E01030956,E01030957,E01030958,E01030959,E01030960,E01030961,E01030962,E01030963,E01030964,E01030965,E01030966,E01030967,E01030968,E01030969,E01030970,E01030971,E01030972,E01030974,E01030975,E01030976,E01030977,E01030978,E01030979,E01030980,E01030981,E01030983,E01030984,E01030986,E01030987,E01030988,E01030989,E01030990,E01030991,E01030992,E01030994,E01030995,E01030996,E01030997,E01030998,E01030999,E01031000,E01031001,E01031002,E01031003,E01031004,E01031005,E01031006,E01031007,E01031008,E01031009,E01031010,E01031011,E01031012,E01031013,E01031014,E01031015,E01031016,E01031017,E01031018,E01031019,E01031020,E01031021,E01031022,E01031023,E01031024,E01031025,E01031026,E01031027,E01031028,E01031029,E01031030,E01031031,E01031032,E01031033,E01031034,E01031035,E01031036,E01031037,E01031038,E01031039,E01031040,E01031041,E01031042,E01031043,E01031044,E01031045,E01031046,E01031047,E01031048,E01031049,E01031050,E01031051,E01031052,E01031053,E01031054,E01031055,E01031056,E01031057,E01031058,E01031059,E01031060,E01031061,E01031062,E01031063,E01031064,E01031065,E01031066,E01031067&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_059.json.gz + content_sha256: 9261bad8eaaf0c057542f5d1ba0ccd54045fb9f7c53dc91c7adc87381710d64e +- id: census_lsoa_tenure5a_bedrooms_060 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 060 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01031068,E01031069,E01031070,E01031073,E01031074,E01031075,E01031077,E01031078,E01031079,E01031080,E01031081,E01031082,E01031083,E01031084,E01031085,E01031086,E01031087,E01031088,E01031089,E01031090,E01031091,E01031092,E01031093,E01031094,E01031095,E01031096,E01031097,E01031098,E01031099,E01031100,E01031101,E01031102,E01031104,E01031105,E01031106,E01031107,E01031108,E01031109,E01031110,E01031111,E01031112,E01031114,E01031115,E01031116,E01031117,E01031118,E01031119,E01031120,E01031121,E01031122,E01031123,E01031124,E01031125,E01031127,E01031129,E01031130,E01031131,E01031132,E01031133,E01031134,E01031135,E01031136,E01031137,E01031138,E01031139,E01031140,E01031142,E01031143,E01031144,E01031145,E01031146,E01031148,E01031149,E01031150,E01031151,E01031152,E01031153,E01031154,E01031155,E01031156,E01031157,E01031158,E01031159,E01031160,E01031161,E01031162,E01031163,E01031165,E01031166,E01031167,E01031168,E01031169,E01031170,E01031171,E01031172,E01031173,E01031174,E01031175,E01031176,E01031177,E01031178,E01031179,E01031180,E01031181,E01031182,E01031183,E01031184,E01031185,E01031186,E01031187,E01031188,E01031189,E01031190,E01031191,E01031192,E01031193,E01031194,E01031195,E01031196,E01031197,E01031198,E01031199,E01031200,E01031201,E01031202,E01031203,E01031204,E01031205,E01031206,E01031207,E01031208,E01031209,E01031210,E01031211,E01031213,E01031214,E01031215,E01031216,E01031217,E01031218,E01031219,E01031220,E01031221,E01031222,E01031223,E01031224,E01031225,E01031227,E01031228,E01031229,E01031231,E01031232,E01031233,E01031234,E01031235,E01031236,E01031237,E01031238,E01031239,E01031240,E01031241,E01031242,E01031243,E01031244,E01031245,E01031246,E01031247,E01031248,E01031249,E01031251,E01031252,E01031253,E01031254,E01031255,E01031256,E01031257,E01031258,E01031259,E01031260,E01031261,E01031262,E01031263,E01031264,E01031265,E01031266,E01031267,E01031268,E01031269,E01031271,E01031272,E01031273,E01031274,E01031275,E01031276,E01031277,E01031278,E01031279,E01031280,E01031281,E01031282,E01031283,E01031284,E01031285,E01031286,E01031287,E01031288,E01031289,E01031290,E01031291,E01031292,E01031293,E01031294,E01031295,E01031296,E01031297,E01031298,E01031299,E01031300,E01031301,E01031302,E01031303,E01031304,E01031305,E01031306,E01031307,E01031308,E01031309,E01031310,E01031311,E01031312,E01031313,E01031314,E01031315,E01031316,E01031317,E01031319,E01031320,E01031321,E01031322,E01031323,E01031324,E01031325,E01031327,E01031328,E01031329,E01031330,E01031331,E01031332,E01031333,E01031334,E01031335,E01031336,E01031337,E01031338,E01031339,E01031340,E01031341,E01031342,E01031343,E01031344,E01031345,E01031346,E01031347,E01031348,E01031349,E01031350,E01031351,E01031352,E01031353,E01031354,E01031355,E01031356,E01031357,E01031358,E01031359,E01031360,E01031361,E01031362,E01031363,E01031364,E01031365,E01031366,E01031367,E01031368,E01031369,E01031370,E01031371,E01031372,E01031373,E01031374,E01031375,E01031376,E01031377,E01031378,E01031379,E01031380,E01031381,E01031382,E01031383,E01031384,E01031385,E01031386,E01031387,E01031389,E01031390,E01031391,E01031392,E01031393,E01031394,E01031395,E01031396,E01031397,E01031398,E01031399,E01031400,E01031402,E01031403,E01031404,E01031405,E01031406,E01031407,E01031408,E01031409,E01031410,E01031411,E01031412,E01031413,E01031414,E01031415,E01031416,E01031417,E01031418,E01031419,E01031420,E01031422,E01031423,E01031424,E01031425,E01031426,E01031427,E01031428,E01031429,E01031430,E01031431,E01031432,E01031433,E01031434,E01031435,E01031436,E01031437,E01031438,E01031439,E01031440,E01031441,E01031442,E01031443,E01031444,E01031445,E01031446,E01031447,E01031448,E01031449,E01031450,E01031451,E01031452,E01031453,E01031454,E01031455,E01031456,E01031457,E01031458,E01031459,E01031460,E01031461,E01031462,E01031463,E01031464,E01031465,E01031466,E01031469,E01031470,E01031471,E01031472,E01031473,E01031474,E01031475,E01031476,E01031477,E01031478,E01031479,E01031480,E01031481,E01031482,E01031483,E01031484,E01031485,E01031486,E01031487,E01031489,E01031490,E01031491,E01031492,E01031493,E01031494,E01031495,E01031496,E01031497,E01031498,E01031499,E01031500,E01031501,E01031502,E01031503,E01031504,E01031505,E01031506,E01031507,E01031508,E01031511,E01031512,E01031514,E01031515,E01031516,E01031517,E01031518,E01031519,E01031520,E01031521,E01031522,E01031523,E01031524,E01031525,E01031526,E01031527,E01031528,E01031529,E01031530,E01031531,E01031532,E01031533,E01031534,E01031535,E01031536,E01031538,E01031539,E01031540,E01031541,E01031542,E01031543,E01031544,E01031545,E01031546,E01031547,E01031548,E01031549,E01031550,E01031551,E01031552,E01031553,E01031554,E01031555,E01031556,E01031557,E01031558,E01031559,E01031560,E01031561,E01031562,E01031563,E01031564,E01031565,E01031566,E01031567,E01031568,E01031569,E01031570,E01031571,E01031572,E01031573,E01031574,E01031575,E01031576,E01031577,E01031578,E01031579,E01031580,E01031581,E01031582,E01031583,E01031584,E01031585,E01031586,E01031588,E01031589,E01031590,E01031591,E01031592,E01031593,E01031594,E01031595&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_060.json.gz + content_sha256: a929fdf15d96a4626b78cf5b877ef64b67e5674df6e82f1f0a6dcb848549499b +- id: census_lsoa_tenure5a_bedrooms_061 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 061 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01031596,E01031597,E01031598,E01031599,E01031600,E01031601,E01031603,E01031604,E01031605,E01031606,E01031607,E01031608,E01031609,E01031610,E01031611,E01031612,E01031614,E01031615,E01031616,E01031617,E01031618,E01031620,E01031621,E01031622,E01031623,E01031624,E01031625,E01031626,E01031627,E01031628,E01031629,E01031630,E01031631,E01031632,E01031633,E01031634,E01031635,E01031638,E01031639,E01031640,E01031641,E01031642,E01031643,E01031644,E01031645,E01031646,E01031647,E01031648,E01031649,E01031650,E01031651,E01031652,E01031653,E01031654,E01031655,E01031656,E01031657,E01031658,E01031659,E01031660,E01031661,E01031662,E01031663,E01031664,E01031665,E01031666,E01031667,E01031668,E01031669,E01031670,E01031671,E01031672,E01031673,E01031674,E01031675,E01031677,E01031678,E01031679,E01031680,E01031681,E01031682,E01031683,E01031684,E01031685,E01031686,E01031687,E01031688,E01031689,E01031690,E01031691,E01031693,E01031694,E01031695,E01031696,E01031697,E01031698,E01031699,E01031700,E01031701,E01031702,E01031703,E01031704,E01031705,E01031706,E01031707,E01031708,E01031709,E01031710,E01031712,E01031713,E01031714,E01031715,E01031716,E01031717,E01031718,E01031719,E01031720,E01031721,E01031722,E01031723,E01031724,E01031725,E01031726,E01031727,E01031729,E01031730,E01031731,E01031732,E01031733,E01031734,E01031735,E01031736,E01031737,E01031738,E01031739,E01031740,E01031741,E01031743,E01031744,E01031745,E01031746,E01031747,E01031748,E01031749,E01031750,E01031751,E01031752,E01031753,E01031754,E01031755,E01031757,E01031758,E01031759,E01031760,E01031763,E01031764,E01031765,E01031766,E01031767,E01031768,E01031769,E01031770,E01031771,E01031772,E01031773,E01031774,E01031775,E01031776,E01031777,E01031778,E01031779,E01031780,E01031781,E01031782,E01031783,E01031785,E01031786,E01031787,E01031788,E01031790,E01031791,E01031792,E01031793,E01031794,E01031795,E01031796,E01031797,E01031798,E01031799,E01031800,E01031801,E01031802,E01031803,E01031804,E01031805,E01031806,E01031807,E01031808,E01031809,E01031810,E01031811,E01031812,E01031813,E01031814,E01031815,E01031816,E01031817,E01031818,E01031820,E01031821,E01031822,E01031823,E01031824,E01031825,E01031826,E01031827,E01031828,E01031829,E01031830,E01031831,E01031832,E01031833,E01031834,E01031835,E01031836,E01031837,E01031838,E01031839,E01031840,E01031841,E01031842,E01031843,E01031845,E01031846,E01031847,E01031848,E01031849,E01031850,E01031851,E01031852,E01031853,E01031854,E01031855,E01031856,E01031857,E01031858,E01031859,E01031860,E01031861,E01031862,E01031863,E01031864,E01031865,E01031866,E01031867,E01031868,E01031869,E01031870,E01031871,E01031872,E01031873,E01031874,E01031876,E01031877,E01031879,E01031882,E01031883,E01031884,E01031885,E01031886,E01031887,E01031888,E01031889,E01031890,E01031892,E01031893,E01031894,E01031895,E01031896,E01031897,E01031898,E01031901,E01031902,E01031903,E01031904,E01031905,E01031906,E01031907,E01031908,E01031909,E01031910,E01031911,E01031912,E01031913,E01031914,E01031915,E01031916,E01031917,E01031918,E01031919,E01031920,E01031921,E01031922,E01031923,E01031924,E01031925,E01031926,E01031927,E01031928,E01031929,E01031930,E01031931,E01031932,E01031933,E01031934,E01031935,E01031936,E01031937,E01031938,E01031939,E01031940,E01031941,E01031942,E01031944,E01031946,E01031947,E01031948,E01031949,E01031950,E01031951,E01031952,E01031955,E01031956,E01031957,E01031958,E01031959,E01031960,E01031961,E01031962,E01031963,E01031964,E01031965,E01031966,E01031967,E01031968,E01031969,E01031970,E01031971,E01031972,E01031973,E01031974,E01031975,E01031976,E01031978,E01031979,E01031980,E01031981,E01031982,E01031983,E01031984,E01031985,E01031986,E01031988,E01031989,E01031990,E01031991,E01031992,E01031993,E01031994,E01031995,E01031996,E01031997,E01031999,E01032000,E01032001,E01032002,E01032003,E01032004,E01032005,E01032006,E01032007,E01032008,E01032009,E01032010,E01032012,E01032013,E01032014,E01032015,E01032016,E01032017,E01032018,E01032019,E01032020,E01032021,E01032022,E01032023,E01032024,E01032025,E01032026,E01032027,E01032029,E01032030,E01032031,E01032032,E01032033,E01032034,E01032035,E01032036,E01032037,E01032038,E01032039,E01032040,E01032041,E01032042,E01032043,E01032044,E01032045,E01032046,E01032047,E01032048,E01032049,E01032051,E01032052,E01032053,E01032054,E01032055,E01032056,E01032057,E01032058,E01032059,E01032060,E01032061,E01032062,E01032063,E01032064,E01032065,E01032066,E01032067,E01032068,E01032069,E01032070,E01032072,E01032073,E01032074,E01032075,E01032076,E01032077,E01032078,E01032079,E01032080,E01032081,E01032082,E01032083,E01032085,E01032087,E01032088,E01032089,E01032090,E01032091,E01032092,E01032093,E01032094,E01032095,E01032096,E01032097,E01032098,E01032100,E01032101,E01032102,E01032103,E01032104,E01032105,E01032106,E01032107,E01032108,E01032109,E01032110,E01032111,E01032112,E01032113,E01032114,E01032115,E01032117,E01032118,E01032119,E01032121,E01032122,E01032123,E01032124,E01032125,E01032126,E01032127,E01032128,E01032129,E01032131,E01032132,E01032133,E01032134,E01032135,E01032136&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_061.json.gz + content_sha256: 224c3d13d6aedfdbbdc03f3d27d6c2f0c499387a5e2fc3e501ac4d29e6951613 +- id: census_lsoa_tenure5a_bedrooms_062 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 062 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01032137,E01032138,E01032139,E01032140,E01032141,E01032142,E01032143,E01032144,E01032145,E01032146,E01032147,E01032148,E01032149,E01032150,E01032151,E01032152,E01032153,E01032154,E01032155,E01032156,E01032157,E01032158,E01032159,E01032160,E01032161,E01032162,E01032168,E01032169,E01032170,E01032171,E01032172,E01032173,E01032174,E01032175,E01032176,E01032177,E01032178,E01032179,E01032180,E01032181,E01032182,E01032183,E01032184,E01032185,E01032186,E01032187,E01032188,E01032189,E01032190,E01032191,E01032192,E01032193,E01032194,E01032195,E01032196,E01032197,E01032198,E01032199,E01032200,E01032201,E01032202,E01032203,E01032204,E01032205,E01032206,E01032207,E01032208,E01032209,E01032210,E01032211,E01032212,E01032213,E01032214,E01032215,E01032216,E01032217,E01032218,E01032219,E01032220,E01032221,E01032222,E01032223,E01032224,E01032225,E01032226,E01032227,E01032228,E01032229,E01032230,E01032231,E01032232,E01032234,E01032235,E01032236,E01032237,E01032238,E01032239,E01032240,E01032241,E01032242,E01032243,E01032244,E01032245,E01032246,E01032247,E01032248,E01032249,E01032250,E01032251,E01032252,E01032253,E01032254,E01032255,E01032256,E01032257,E01032258,E01032259,E01032260,E01032261,E01032262,E01032263,E01032264,E01032265,E01032266,E01032267,E01032268,E01032269,E01032270,E01032271,E01032272,E01032273,E01032274,E01032275,E01032276,E01032277,E01032278,E01032279,E01032280,E01032281,E01032282,E01032283,E01032284,E01032285,E01032286,E01032287,E01032288,E01032289,E01032290,E01032292,E01032294,E01032296,E01032297,E01032298,E01032299,E01032300,E01032301,E01032302,E01032303,E01032304,E01032305,E01032306,E01032307,E01032308,E01032309,E01032310,E01032311,E01032312,E01032313,E01032314,E01032315,E01032316,E01032317,E01032318,E01032319,E01032320,E01032321,E01032322,E01032323,E01032324,E01032325,E01032326,E01032327,E01032328,E01032329,E01032330,E01032331,E01032332,E01032333,E01032334,E01032335,E01032336,E01032337,E01032338,E01032339,E01032340,E01032341,E01032342,E01032344,E01032345,E01032346,E01032347,E01032348,E01032349,E01032350,E01032351,E01032352,E01032353,E01032355,E01032356,E01032357,E01032358,E01032359,E01032360,E01032361,E01032363,E01032364,E01032365,E01032366,E01032367,E01032368,E01032369,E01032370,E01032371,E01032372,E01032373,E01032374,E01032375,E01032376,E01032377,E01032378,E01032379,E01032380,E01032381,E01032382,E01032383,E01032384,E01032385,E01032386,E01032387,E01032388,E01032389,E01032390,E01032391,E01032392,E01032393,E01032394,E01032395,E01032396,E01032397,E01032398,E01032399,E01032400,E01032401,E01032402,E01032403,E01032404,E01032405,E01032406,E01032407,E01032408,E01032409,E01032410,E01032411,E01032412,E01032413,E01032414,E01032415,E01032416,E01032417,E01032418,E01032419,E01032420,E01032421,E01032422,E01032423,E01032424,E01032425,E01032426,E01032427,E01032428,E01032429,E01032430,E01032431,E01032432,E01032433,E01032434,E01032435,E01032436,E01032437,E01032438,E01032439,E01032440,E01032441,E01032442,E01032443,E01032444,E01032445,E01032446,E01032447,E01032448,E01032449,E01032451,E01032452,E01032453,E01032454,E01032455,E01032456,E01032457,E01032458,E01032459,E01032460,E01032461,E01032462,E01032463,E01032464,E01032465,E01032466,E01032467,E01032468,E01032469,E01032471,E01032472,E01032473,E01032474,E01032475,E01032476,E01032477,E01032478,E01032479,E01032480,E01032481,E01032482,E01032483,E01032484,E01032485,E01032486,E01032487,E01032488,E01032489,E01032490,E01032491,E01032492,E01032493,E01032494,E01032496,E01032497,E01032498,E01032499,E01032500,E01032501,E01032502,E01032503,E01032504,E01032505,E01032506,E01032507,E01032508,E01032509,E01032510,E01032511,E01032512,E01032513,E01032514,E01032515,E01032516,E01032517,E01032518,E01032519,E01032520,E01032522,E01032523,E01032524,E01032526,E01032527,E01032528,E01032529,E01032530,E01032532,E01032533,E01032534,E01032535,E01032536,E01032537,E01032538,E01032540,E01032541,E01032542,E01032543,E01032544,E01032545,E01032546,E01032548,E01032549,E01032551,E01032552,E01032554,E01032555,E01032556,E01032557,E01032558,E01032559,E01032560,E01032561,E01032562,E01032563,E01032564,E01032565,E01032566,E01032567,E01032568,E01032569,E01032570,E01032571,E01032572,E01032573,E01032574,E01032575,E01032576,E01032577,E01032579,E01032580,E01032581,E01032582,E01032583,E01032584,E01032585,E01032586,E01032587,E01032588,E01032589,E01032590,E01032591,E01032592,E01032593,E01032594,E01032595,E01032596,E01032597,E01032599,E01032600,E01032601,E01032602,E01032603,E01032604,E01032605,E01032606,E01032607,E01032608,E01032609,E01032610,E01032611,E01032612,E01032613,E01032614,E01032615,E01032616,E01032617,E01032618,E01032619,E01032620,E01032621,E01032622,E01032623,E01032624,E01032625,E01032626,E01032627,E01032628,E01032631,E01032632,E01032633,E01032634,E01032635,E01032636,E01032637,E01032638,E01032639,E01032640,E01032643,E01032644,E01032645,E01032646,E01032647,E01032648,E01032649,E01032650,E01032651,E01032652,E01032653,E01032655,E01032656,E01032657,E01032658,E01032659,E01032660,E01032661,E01032662,E01032664,E01032665,E01032666&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_062.json.gz + content_sha256: 43fd1e175a047cab541de95c0bc759a0b2da9f654aa497688f2a66c41036bbaf +- id: census_lsoa_tenure5a_bedrooms_063 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 063 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01032667,E01032668,E01032669,E01032670,E01032671,E01032672,E01032673,E01032675,E01032676,E01032677,E01032678,E01032679,E01032680,E01032681,E01032682,E01032683,E01032684,E01032688,E01032689,E01032690,E01032691,E01032692,E01032693,E01032694,E01032695,E01032696,E01032697,E01032699,E01032700,E01032701,E01032702,E01032703,E01032704,E01032705,E01032706,E01032707,E01032708,E01032709,E01032710,E01032711,E01032712,E01032713,E01032714,E01032715,E01032716,E01032719,E01032724,E01032726,E01032728,E01032729,E01032730,E01032731,E01032732,E01032733,E01032734,E01032735,E01032737,E01032738,E01032739,E01032740,E01032741,E01032742,E01032743,E01032744,E01032745,E01032746,E01032748,E01032750,E01032751,E01032753,E01032755,E01032759,E01032764,E01032765,E01032767,E01032768,E01032769,E01032770,E01032771,E01032772,E01032774,E01032775,E01032776,E01032777,E01032779,E01032780,E01032781,E01032782,E01032783,E01032784,E01032785,E01032786,E01032787,E01032788,E01032789,E01032790,E01032792,E01032794,E01032799,E01032800,E01032802,E01032803,E01032807,E01032808,E01032810,E01032811,E01032812,E01032813,E01032814,E01032815,E01032816,E01032817,E01032818,E01032819,E01032820,E01032821,E01032822,E01032823,E01032824,E01032825,E01032826,E01032827,E01032828,E01032829,E01032830,E01032831,E01032832,E01032833,E01032834,E01032835,E01032836,E01032837,E01032838,E01032839,E01032841,E01032842,E01032843,E01032844,E01032845,E01032846,E01032847,E01032848,E01032849,E01032850,E01032852,E01032853,E01032854,E01032855,E01032856,E01032857,E01032858,E01032859,E01032860,E01032862,E01032863,E01032865,E01032868,E01032870,E01032871,E01032872,E01032873,E01032876,E01032877,E01032878,E01032879,E01032880,E01032881,E01032883,E01032884,E01032885,E01032886,E01032888,E01032891,E01032893,E01032894,E01032895,E01032896,E01032897,E01032898,E01032899,E01032900,E01032901,E01032902,E01032904,E01032905,E01032906,E01032909,E01032912,E01032914,E01032915,E01032920,E01032922,E01032923,E01032924,E01032925,E01032926,E01032927,E01032930,E01032931,E01032932,E01032933,E01032935,E01032937,E01032938,E01032939,E01032940,E01032941,E01032942,E01032943,E01032944,E01032946,E01032947,E01032948,E01032949,E01032950,E01032951,E01032952,E01032953,E01032955,E01032956,E01032957,E01032958,E01032961,E01032962,E01032963,E01032964,E01032965,E01032966,E01032967,E01032969,E01032970,E01032971,E01032972,E01032973,E01032974,E01032976,E01032978,E01032979,E01032980,E01032981,E01032982,E01032983,E01032984,E01032985,E01032986,E01032987,E01032988,E01032989,E01032991,E01032992,E01032993,E01032994,E01032995,E01032996,E01032997,E01032998,E01032999,E01033000,E01033002,E01033003,E01033008,E01033010,E01033011,E01033013,E01033015,E01033016,E01033021,E01033022,E01033023,E01033024,E01033025,E01033026,E01033027,E01033028,E01033029,E01033030,E01033031,E01033033,E01033034,E01033035,E01033036,E01033037,E01033038,E01033039,E01033040,E01033041,E01033042,E01033043,E01033044,E01033045,E01033046,E01033047,E01033048,E01033049,E01033050,E01033051,E01033052,E01033053,E01033054,E01033055,E01033056,E01033058,E01033059,E01033060,E01033061,E01033062,E01033063,E01033064,E01033067,E01033068,E01033069,E01033070,E01033071,E01033072,E01033073,E01033076,E01033077,E01033078,E01033079,E01033080,E01033081,E01033082,E01033083,E01033084,E01033086,E01033087,E01033088,E01033089,E01033091,E01033092,E01033094,E01033095,E01033097,E01033098,E01033099,E01033100,E01033102,E01033103,E01033104,E01033105,E01033107,E01033109,E01033111,E01033112,E01033113,E01033114,E01033115,E01033116,E01033117,E01033118,E01033120,E01033121,E01033122,E01033123,E01033124,E01033125,E01033126,E01033127,E01033128,E01033129,E01033130,E01033131,E01033132,E01033133,E01033135,E01033137,E01033138,E01033139,E01033140,E01033142,E01033144,E01033145,E01033146,E01033148,E01033150,E01033151,E01033156,E01033158,E01033160,E01033161,E01033163,E01033166,E01033167,E01033168,E01033169,E01033170,E01033171,E01033172,E01033173,E01033174,E01033175,E01033177,E01033179,E01033180,E01033181,E01033182,E01033184,E01033185,E01033186,E01033188,E01033189,E01033190,E01033191,E01033192,E01033195,E01033197,E01033198,E01033200,E01033205,E01033206,E01033208,E01033209,E01033210,E01033211,E01033212,E01033213,E01033214,E01033215,E01033217,E01033218,E01033219,E01033220,E01033223,E01033224,E01033226,E01033228,E01033229,E01033232,E01033234,E01033235,E01033236,E01033237,E01033239,E01033241,E01033242,E01033243,E01033244,E01033245,E01033246,E01033247,E01033248,E01033249,E01033250,E01033251,E01033252,E01033253,E01033254,E01033255,E01033256,E01033257,E01033259,E01033260,E01033261,E01033262,E01033263,E01033264,E01033265,E01033268,E01033269,E01033270,E01033271,E01033272,E01033273,E01033274,E01033275,E01033276,E01033277,E01033279,E01033280,E01033281,E01033283,E01033285,E01033286,E01033288,E01033289,E01033291,E01033292,E01033294,E01033296,E01033297,E01033299,E01033300,E01033302,E01033303,E01033305,E01033306,E01033308,E01033310,E01033311,E01033313,E01033314,E01033316,E01033317,E01033319,E01033320,E01033322,E01033324,E01033325,E01033327,E01033328&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_063.json.gz + content_sha256: c6f2e01d108b961bbf608e60e0a774d2d5194e913f13ddd5806f9c89118c4a34 +- id: census_lsoa_tenure5a_bedrooms_064 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 064 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01033329,E01033330,E01033334,E01033336,E01033338,E01033339,E01033341,E01033342,E01033344,E01033347,E01033348,E01033350,E01033352,E01033353,E01033356,E01033358,E01033359,E01033361,E01033362,E01033364,E01033366,E01033367,E01033369,E01033370,E01033372,E01033373,E01033375,E01033376,E01033378,E01033380,E01033381,E01033383,E01033384,E01033386,E01033387,E01033388,E01033389,E01033390,E01033391,E01033392,E01033394,E01033395,E01033396,E01033397,E01033398,E01033399,E01033400,E01033402,E01033407,E01033409,E01033410,E01033411,E01033412,E01033413,E01033414,E01033415,E01033417,E01033418,E01033420,E01033421,E01033422,E01033423,E01033425,E01033426,E01033427,E01033429,E01033430,E01033431,E01033434,E01033435,E01033436,E01033437,E01033438,E01033440,E01033441,E01033442,E01033443,E01033444,E01033445,E01033446,E01033447,E01033448,E01033449,E01033450,E01033452,E01033454,E01033455,E01033456,E01033457,E01033458,E01033459,E01033460,E01033461,E01033462,E01033463,E01033465,E01033466,E01033467,E01033469,E01033470,E01033471,E01033472,E01033473,E01033474,E01033475,E01033476,E01033479,E01033480,E01033481,E01033482,E01033485,E01033486,E01033487,E01033488,E01033489,E01033490,E01033491,E01033493,E01033494,E01033495,E01033496,E01033497,E01033498,E01033499,E01033500,E01033501,E01033503,E01033506,E01033508,E01033511,E01033514,E01033515,E01033517,E01033519,E01033520,E01033522,E01033523,E01033526,E01033527,E01033528,E01033530,E01033532,E01033533,E01033534,E01033535,E01033536,E01033537,E01033539,E01033540,E01033541,E01033543,E01033545,E01033547,E01033548,E01033550,E01033551,E01033553,E01033556,E01033557,E01033559,E01033561,E01033562,E01033564,E01033565,E01033567,E01033569,E01033570,E01033571,E01033579,E01033580,E01033581,E01033582,E01033584,E01033586,E01033587,E01033589,E01033590,E01033591,E01033592,E01033593,E01033594,E01033597,E01033598,E01033599,E01033601,E01033602,E01033603,E01033604,E01033605,E01033609,E01033610,E01033611,E01033612,E01033613,E01033614,E01033615,E01033616,E01033617,E01033618,E01033620,E01033622,E01033623,E01033624,E01033625,E01033626,E01033627,E01033629,E01033630,E01033638,E01033640,E01033642,E01033643,E01033646,E01033648,E01033649,E01033650,E01033651,E01033653,E01033654,E01033655,E01033656,E01033658,E01033660,E01033661,E01033662,E01033664,E01033665,E01033667,E01033668,E01033669,E01033670,E01033671,E01033672,E01033673,E01033674,E01033675,E01033676,E01033677,E01033678,E01033679,E01033680,E01033681,E01033682,E01033684,E01033685,E01033686,E01033687,E01033688,E01033689,E01033690,E01033691,E01033692,E01033693,E01033694,E01033695,E01033696,E01033697,E01033699,E01033700,E01033702,E01033703,E01033704,E01033705,E01033706,E01033708,E01033709,E01033710,E01033711,E01033712,E01033713,E01033714,E01033715,E01033716,E01033717,E01033718,E01033719,E01033720,E01033721,E01033722,E01033727,E01033728,E01033729,E01033732,E01033734,E01033735,E01033736,E01033737,E01033738,E01033739,E01033740,E01033741,E01033742,E01033743,E01033744,E01033745,E01033746,E01033747,E01033748,E01033750,E01033751,E01033756,E01033757,E01033758,E01033760,E01033761,E01033762,E01033763,E01033765,E01033767,E01033768,E01033769,E01033770,E01033771,E01033772,E01033773,E01033774,E01033775,E01033776,E01033777,E01033778,E01033779,E01033780,E01033781,E01033782,E01033783,E01033784,E01033785,E01033786,E01033787,E01033788,E01033789,E01033790,E01033791,E01033792,E01033793,E01033794,E01033795,E01033796,E01033797,E01033798,E01033799,E01033800,E01033801,E01033802,E01033803,E01033804,E01033805,E01033806,E01033807,E01033808,E01033809,E01033810,E01033811,E01033812,E01033813,E01033814,E01033815,E01033816,E01033817,E01033818,E01033819,E01033820,E01033821,E01033822,E01033823,E01033824,E01033825,E01033826,E01033827,E01033828,E01033829,E01033830,E01033831,E01033832,E01033833,E01033834,E01033835,E01033836,E01033837,E01033838,E01033839,E01033840,E01033841,E01033842,E01033843,E01033844,E01033845,E01033846,E01033847,E01033848,E01033849,E01033850,E01033851,E01033852,E01033853,E01033854,E01033855,E01033856,E01033857,E01033858,E01033859,E01033860,E01033861,E01033862,E01033863,E01033864,E01033865,E01033866,E01033867,E01033868,E01033869,E01033870,E01033871,E01033872,E01033873,E01033874,E01033875,E01033876,E01033877,E01033878,E01033879,E01033880,E01033881,E01033882,E01033883,E01033884,E01033885,E01033886,E01033887,E01033888,E01033889,E01033890,E01033891,E01033892,E01033893,E01033894,E01033895,E01033896,E01033897,E01033898,E01033899,E01033900,E01033901,E01033902,E01033903,E01033904,E01033905,E01033906,E01033907,E01033908,E01033909,E01033910,E01033911,E01033912,E01033913,E01033914,E01033915,E01033916,E01033917,E01033918,E01033919,E01033920,E01033921,E01033922,E01033923,E01033924,E01033925,E01033926,E01033927,E01033928,E01033929,E01033930,E01033931,E01033932,E01033933,E01033934,E01033935,E01033936,E01033937,E01033938,E01033939,E01033940,E01033941,E01033942,E01033943,E01033944,E01033945,E01033946,E01033947,E01033948,E01033949,E01033950,E01033951,E01033952,E01033953,E01033954,E01033955,E01033956,E01033957,E01033958&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_064.json.gz + content_sha256: 85d74d6f7105f986cede580cdd5104f002ec7e2d74775bba5b55284a3b82ce63 +- id: census_lsoa_tenure5a_bedrooms_065 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 065 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01033959,E01033960,E01033961,E01033962,E01033963,E01033964,E01033965,E01033966,E01033967,E01033968,E01033969,E01033970,E01033971,E01033972,E01033973,E01033974,E01033975,E01033976,E01033977,E01033978,E01033979,E01033980,E01033981,E01033982,E01033983,E01033984,E01033985,E01033986,E01033987,E01033988,E01033989,E01033990,E01033991,E01033992,E01033993,E01033994,E01033995,E01033996,E01033997,E01033998,E01033999,E01034000,E01034001,E01034002,E01034003,E01034004,E01034005,E01034006,E01034007,E01034008,E01034009,E01034010,E01034011,E01034012,E01034013,E01034014,E01034015,E01034016,E01034017,E01034018,E01034019,E01034020,E01034021,E01034022,E01034023,E01034024,E01034025,E01034026,E01034027,E01034028,E01034029,E01034030,E01034031,E01034032,E01034033,E01034034,E01034035,E01034036,E01034037,E01034038,E01034039,E01034040,E01034041,E01034042,E01034043,E01034044,E01034045,E01034046,E01034047,E01034048,E01034049,E01034050,E01034051,E01034052,E01034053,E01034054,E01034055,E01034056,E01034057,E01034058,E01034059,E01034060,E01034061,E01034062,E01034063,E01034064,E01034065,E01034066,E01034067,E01034068,E01034069,E01034070,E01034071,E01034072,E01034073,E01034074,E01034075,E01034076,E01034077,E01034078,E01034079,E01034080,E01034081,E01034082,E01034083,E01034084,E01034085,E01034086,E01034087,E01034088,E01034089,E01034090,E01034091,E01034092,E01034093,E01034094,E01034095,E01034096,E01034097,E01034098,E01034099,E01034100,E01034101,E01034102,E01034103,E01034104,E01034105,E01034106,E01034107,E01034108,E01034109,E01034110,E01034111,E01034112,E01034113,E01034114,E01034115,E01034116,E01034117,E01034118,E01034119,E01034120,E01034121,E01034122,E01034123,E01034124,E01034125,E01034126,E01034127,E01034128,E01034129,E01034130,E01034131,E01034132,E01034133,E01034134,E01034135,E01034136,E01034137,E01034138,E01034139,E01034140,E01034141,E01034142,E01034143,E01034144,E01034145,E01034146,E01034147,E01034148,E01034149,E01034150,E01034151,E01034152,E01034153,E01034154,E01034155,E01034156,E01034157,E01034158,E01034159,E01034160,E01034161,E01034162,E01034165,E01034166,E01034167,E01034168,E01034169,E01034170,E01034171,E01034172,E01034173,E01034174,E01034175,E01034176,E01034177,E01034178,E01034179,E01034180,E01034181,E01034182,E01034183,E01034184,E01034185,E01034186,E01034187,E01034188,E01034189,E01034190,E01034191,E01034192,E01034193,E01034194,E01034195,E01034196,E01034198,E01034199,E01034200,E01034201,E01034202,E01034203,E01034204,E01034205,E01034206,E01034207,E01034208,E01034209,E01034210,E01034211,E01034212,E01034213,E01034214,E01034215,E01034216,E01034217,E01034218,E01034219,E01034220,E01034221,E01034222,E01034223,E01034224,E01034225,E01034226,E01034227,E01034228,E01034229,E01034230,E01034231,E01034232,E01034233,E01034234,E01034235,E01034236,E01034237,E01034238,E01034239,E01034240,E01034241,E01034242,E01034243,E01034244,E01034245,E01034246,E01034247,E01034248,E01034249,E01034250,E01034251,E01034252,E01034253,E01034254,E01034255,E01034256,E01034257,E01034258,E01034259,E01034260,E01034261,E01034262,E01034263,E01034264,E01034265,E01034266,E01034267,E01034268,E01034269,E01034270,E01034271,E01034272,E01034273,E01034274,E01034275,E01034276,E01034277,E01034278,E01034279,E01034280,E01034281,E01034282,E01034283,E01034284,E01034285,E01034286,E01034287,E01034288,E01034289,E01034290,E01034291,E01034292,E01034293,E01034294,E01034295,E01034296,E01034297,E01034298,E01034299,E01034300,E01034301,E01034302,E01034303,E01034304,E01034305,E01034306,E01034307,E01034308,E01034309,E01034310,E01034311,E01034312,E01034313,E01034314,E01034315,E01034316,E01034317,E01034318,E01034319,E01034320,E01034321,E01034322,E01034323,E01034324,E01034325,E01034326,E01034327,E01034328,E01034329,E01034330,E01034331,E01034332,E01034333,E01034334,E01034335,E01034336,E01034337,E01034338,E01034339,E01034340,E01034341,E01034342,E01034343,E01034344,E01034345,E01034346,E01034347,E01034348,E01034349,E01034350,E01034351,E01034352,E01034353,E01034354,E01034355,E01034356,E01034357,E01034358,E01034359,E01034360,E01034361,E01034362,E01034363,E01034364,E01034365,E01034366,E01034367,E01034368,E01034369,E01034370,E01034371,E01034372,E01034373,E01034374,E01034375,E01034376,E01034377,E01034378,E01034379,E01034380,E01034381,E01034382,E01034383,E01034384,E01034385,E01034386,E01034387,E01034388,E01034389,E01034390,E01034391,E01034392,E01034393,E01034394,E01034395,E01034396,E01034397,E01034398,E01034399,E01034400,E01034401,E01034402,E01034403,E01034404,E01034405,E01034406,E01034407,E01034408,E01034409,E01034410,E01034411,E01034412,E01034413,E01034414,E01034415,E01034416,E01034417,E01034418,E01034419,E01034420,E01034421,E01034422,E01034423,E01034424,E01034425,E01034426,E01034427,E01034428,E01034429,E01034430,E01034431,E01034432,E01034433,E01034434,E01034435,E01034436,E01034437,E01034438,E01034439,E01034440,E01034441,E01034442,E01034443,E01034444,E01034445,E01034446,E01034447,E01034448,E01034449,E01034450,E01034451,E01034452,E01034453,E01034454,E01034455,E01034456,E01034457,E01034458,E01034459,E01034460,E01034461&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_065.json.gz + content_sha256: 135531545463a7a2ed838c00370ff00ac7dcd18f9ec77be39c6e3770f6ac8569 +- id: census_lsoa_tenure5a_bedrooms_066 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 066 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01034462,E01034463,E01034464,E01034465,E01034466,E01034467,E01034468,E01034469,E01034470,E01034471,E01034472,E01034473,E01034474,E01034475,E01034476,E01034477,E01034478,E01034479,E01034480,E01034481,E01034482,E01034483,E01034484,E01034485,E01034486,E01034487,E01034488,E01034489,E01034490,E01034491,E01034492,E01034493,E01034494,E01034495,E01034496,E01034497,E01034498,E01034499,E01034500,E01034501,E01034502,E01034503,E01034504,E01034505,E01034506,E01034507,E01034508,E01034509,E01034510,E01034511,E01034512,E01034513,E01034514,E01034515,E01034516,E01034517,E01034518,E01034519,E01034520,E01034521,E01034522,E01034523,E01034524,E01034525,E01034526,E01034527,E01034528,E01034529,E01034530,E01034531,E01034532,E01034533,E01034534,E01034535,E01034536,E01034537,E01034538,E01034539,E01034540,E01034541,E01034542,E01034543,E01034544,E01034545,E01034546,E01034547,E01034548,E01034549,E01034550,E01034551,E01034552,E01034553,E01034554,E01034555,E01034556,E01034557,E01034558,E01034559,E01034560,E01034561,E01034562,E01034563,E01034564,E01034565,E01034566,E01034567,E01034568,E01034569,E01034570,E01034571,E01034572,E01034573,E01034574,E01034575,E01034576,E01034577,E01034578,E01034579,E01034580,E01034581,E01034582,E01034583,E01034584,E01034585,E01034586,E01034587,E01034588,E01034589,E01034590,E01034591,E01034592,E01034593,E01034594,E01034595,E01034596,E01034597,E01034598,E01034599,E01034600,E01034601,E01034602,E01034603,E01034604,E01034605,E01034606,E01034607,E01034608,E01034609,E01034610,E01034611,E01034612,E01034613,E01034614,E01034615,E01034616,E01034617,E01034618,E01034619,E01034620,E01034621,E01034622,E01034623,E01034624,E01034625,E01034626,E01034627,E01034628,E01034629,E01034630,E01034631,E01034632,E01034633,E01034634,E01034635,E01034636,E01034637,E01034638,E01034639,E01034640,E01034641,E01034642,E01034643,E01034644,E01034645,E01034646,E01034647,E01034648,E01034649,E01034650,E01034651,E01034652,E01034653,E01034654,E01034655,E01034656,E01034657,E01034658,E01034659,E01034660,E01034661,E01034662,E01034663,E01034664,E01034665,E01034666,E01034667,E01034668,E01034669,E01034670,E01034671,E01034672,E01034673,E01034674,E01034675,E01034676,E01034677,E01034678,E01034679,E01034680,E01034681,E01034682,E01034683,E01034684,E01034685,E01034686,E01034687,E01034688,E01034689,E01034690,E01034691,E01034692,E01034693,E01034694,E01034695,E01034696,E01034697,E01034698,E01034699,E01034700,E01034701,E01034702,E01034703,E01034704,E01034705,E01034706,E01034707,E01034708,E01034709,E01034710,E01034711,E01034712,E01034713,E01034714,E01034715,E01034716,E01034717,E01034718,E01034719,E01034720,E01034721,E01034722,E01034723,E01034724,E01034725,E01034726,E01034727,E01034728,E01034729,E01034730,E01034731,E01034732,E01034733,E01034734,E01034735,E01034736,E01034737,E01034738,E01034739,E01034740,E01034741,E01034742,E01034743,E01034744,E01034745,E01034746,E01034747,E01034748,E01034749,E01034750,E01034751,E01034752,E01034753,E01034754,E01034755,E01034756,E01034757,E01034758,E01034759,E01034760,E01034761,E01034762,E01034763,E01034764,E01034765,E01034766,E01034767,E01034768,E01034769,E01034770,E01034771,E01034772,E01034773,E01034774,E01034775,E01034776,E01034777,E01034778,E01034779,E01034780,E01034781,E01034782,E01034783,E01034784,E01034785,E01034786,E01034787,E01034788,E01034789,E01034790,E01034791,E01034792,E01034793,E01034794,E01034795,E01034796,E01034797,E01034798,E01034799,E01034800,E01034801,E01034802,E01034803,E01034804,E01034805,E01034806,E01034807,E01034808,E01034809,E01034810,E01034811,E01034812,E01034813,E01034814,E01034815,E01034816,E01034817,E01034818,E01034819,E01034820,E01034821,E01034822,E01034823,E01034824,E01034825,E01034826,E01034827,E01034828,E01034829,E01034830,E01034831,E01034832,E01034833,E01034834,E01034835,E01034836,E01034837,E01034838,E01034839,E01034840,E01034841,E01034842,E01034843,E01034844,E01034845,E01034846,E01034847,E01034848,E01034849,E01034850,E01034851,E01034852,E01034853,E01034854,E01034855,E01034856,E01034857,E01034858,E01034859,E01034860,E01034862,E01034863,E01034864,E01034865,E01034866,E01034867,E01034868,E01034869,E01034913,E01034914,E01034915,E01034916,E01034917,E01034918,E01034919,E01034920,E01034921,E01034922,E01034923,E01034924,E01034925,E01034926,E01034927,E01034928,E01034929,E01034930,E01034931,E01034932,E01034933,E01034934,E01034935,E01034936,E01034937,E01034938,E01034939,E01034940,E01034941,E01034942,E01034943,E01034944,E01034945,E01034946,E01034947,E01034948,E01034949,E01034950,E01034951,E01034952,E01034953,E01034954,E01034955,E01034956,E01034957,E01034958,E01034959,E01034960,E01034961,E01034962,E01034963,E01034964,E01034965,E01034966,E01034967,E01034968,E01034969,E01034970,E01034971,E01034972,E01034973,E01034974,E01034975,E01034976,E01034977,E01034978,E01034979,E01034980,E01034981,E01034982,E01034983,E01034984,E01034985,E01034986,E01034987,E01034988,E01034989,E01034990,E01034991,E01034992,E01034993,E01034994,E01034995,E01034996,E01034997,E01034998,E01034999,E01035000,E01035001,E01035002,E01035003,E01035004,E01035005&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_066.json.gz + content_sha256: 6bdd4223863583e57e40ff82659baf39ab5a7d0319a89c2bc90b1880c5d86c18 +- id: census_lsoa_tenure5a_bedrooms_067 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 067 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01035006,E01035007,E01035008,E01035009,E01035010,E01035011,E01035012,E01035013,E01035014,E01035015,E01035016,E01035017,E01035018,E01035019,E01035020,E01035021,E01035022,E01035023,E01035024,E01035025,E01035026,E01035027,E01035028,E01035029,E01035030,E01035031,E01035032,E01035033,E01035034,E01035035,E01035036,E01035037,E01035038,E01035039,E01035040,E01035041,E01035042,E01035043,E01035044,E01035045,E01035046,E01035047,E01035048,E01035049,E01035050,E01035051,E01035052,E01035053,E01035054,E01035055,E01035056,E01035057,E01035058,E01035059,E01035060,E01035061,E01035062,E01035063,E01035064,E01035065,E01035066,E01035067,E01035068,E01035069,E01035070,E01035071,E01035072,E01035073,E01035074,E01035075,E01035076,E01035077,E01035078,E01035079,E01035080,E01035081,E01035082,E01035083,E01035084,E01035085,E01035086,E01035087,E01035088,E01035089,E01035090,E01035091,E01035092,E01035093,E01035094,E01035095,E01035096,E01035097,E01035098,E01035099,E01035100,E01035101,E01035102,E01035103,E01035104,E01035105,E01035106,E01035107,E01035108,E01035109,E01035110,E01035111,E01035112,E01035113,E01035114,E01035115,E01035116,E01035117,E01035118,E01035119,E01035120,E01035121,E01035122,E01035123,E01035124,E01035125,E01035126,E01035127,E01035128,E01035129,E01035130,E01035131,E01035132,E01035133,E01035134,E01035135,E01035136,E01035137,E01035138,E01035139,E01035140,E01035141,E01035142,E01035143,E01035144,E01035145,E01035146,E01035147,E01035148,E01035149,E01035150,E01035151,E01035152,E01035153,E01035154,E01035155,E01035156,E01035157,E01035158,E01035159,E01035160,E01035161,E01035162,E01035163,E01035164,E01035165,E01035166,E01035167,E01035168,E01035169,E01035170,E01035171,E01035172,E01035173,E01035174,E01035175,E01035176,E01035177,E01035178,E01035179,E01035180,E01035181,E01035182,E01035183,E01035184,E01035185,E01035186,E01035187,E01035188,E01035189,E01035190,E01035191,E01035192,E01035193,E01035194,E01035195,E01035196,E01035197,E01035198,E01035199,E01035200,E01035201,E01035202,E01035203,E01035204,E01035205,E01035206,E01035207,E01035208,E01035209,E01035210,E01035211,E01035212,E01035213,E01035214,E01035215,E01035216,E01035217,E01035218,E01035219,E01035220,E01035221,E01035222,E01035223,E01035224,E01035225,E01035226,E01035227,E01035228,E01035229,E01035230,E01035231,E01035232,E01035233,E01035234,E01035235,E01035236,E01035237,E01035238,E01035239,E01035240,E01035241,E01035242,E01035243,E01035244,E01035245,E01035246,E01035247,E01035248,E01035249,E01035250,E01035251,E01035252,E01035253,E01035254,E01035255,E01035256,E01035257,E01035258,E01035259,E01035260,E01035261,E01035262,E01035263,E01035264,E01035265,E01035266,E01035267,E01035268,E01035269,E01035270,E01035271,E01035272,E01035273,E01035274,E01035275,E01035276,E01035277,E01035278,E01035279,E01035280,E01035281,E01035282,E01035283,E01035284,E01035285,E01035286,E01035287,E01035288,E01035289,E01035290,E01035291,E01035292,E01035293,E01035294,E01035295,E01035296,E01035297,E01035298,E01035299,E01035300,E01035301,E01035302,E01035303,E01035304,E01035305,E01035306,E01035307,E01035308,E01035309,E01035310,E01035311,E01035312,E01035313,E01035314,E01035315,E01035316,E01035317,E01035318,E01035319,E01035320,E01035321,E01035322,E01035323,E01035324,E01035325,E01035326,E01035327,E01035328,E01035329,E01035330,E01035331,E01035332,E01035333,E01035334,E01035335,E01035336,E01035337,E01035338,E01035339,E01035340,E01035341,E01035342,E01035343,E01035344,E01035345,E01035346,E01035347,E01035348,E01035349,E01035350,E01035351,E01035352,E01035353,E01035354,E01035355,E01035356,E01035357,E01035358,E01035359,E01035360,E01035361,E01035362,E01035363,E01035364,E01035365,E01035366,E01035367,E01035368,E01035369,E01035370,E01035371,E01035372,E01035373,E01035374,E01035375,E01035376,E01035377,E01035378,E01035379,E01035380,E01035381,E01035382,E01035383,E01035384,E01035385,E01035386,E01035387,E01035388,E01035389,E01035390,E01035391,E01035392,E01035393,E01035394,E01035395,E01035396,E01035397,E01035398,E01035399,E01035400,E01035401,E01035402,E01035403,E01035404,E01035405,E01035406,E01035407,E01035408,E01035409,E01035410,E01035411,E01035412,E01035413,E01035414,E01035415,E01035416,E01035417,E01035418,E01035419,E01035420,E01035421,E01035422,E01035423,E01035424,E01035425,E01035426,E01035427,E01035428,E01035429,E01035430,E01035431,E01035432,E01035433,E01035434,E01035435,E01035436,E01035437,E01035438,E01035439,E01035440,E01035441,E01035442,E01035443,E01035444,E01035445,E01035446,E01035447,E01035448,E01035449,E01035450,E01035451,E01035452,E01035453,E01035454,E01035455,E01035456,E01035457,E01035458,E01035459,E01035460,E01035461,E01035462,E01035463,E01035464,E01035465,E01035466,E01035467,E01035468,E01035469,E01035470,E01035471,E01035472,E01035473,E01035474,E01035475,E01035476,E01035477,E01035478,E01035479,E01035480,E01035481,E01035482,E01035483,E01035484,E01035485,E01035486,E01035487,E01035488,E01035489,E01035490,E01035491,E01035492,E01035493,E01035494,E01035495,E01035496,E01035497,E01035498,E01035499,E01035500,E01035501,E01035502,E01035503,E01035504,E01035505&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_067.json.gz + content_sha256: 5e778d7c2fe058e32b70bc5ed241f7e50ffdba4593dc28a8085389a43907cb96 +- id: census_lsoa_tenure5a_bedrooms_068 + publisher: Office for National Statistics + title: Census 2021 English LSOA21 households by 5a tenure and bedrooms, batch 068 + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,E01035506,E01035507,E01035508,E01035509,E01035510,E01035511,E01035512,E01035513,E01035514,E01035515,E01035516,E01035517,E01035518,E01035519,E01035520,E01035521,E01035522,E01035523,E01035524,E01035525,E01035526,E01035527,E01035528,E01035529,E01035530,E01035531,E01035532,E01035533,E01035534,E01035535,E01035536,E01035537,E01035538,E01035539,E01035540,E01035541,E01035542,E01035543,E01035544,E01035545,E01035546,E01035547,E01035548,E01035549,E01035551,E01035552,E01035553,E01035554,E01035555,E01035556,E01035557,E01035558,E01035559,E01035560,E01035561,E01035562,E01035563,E01035564,E01035565,E01035566,E01035567,E01035568,E01035569,E01035570,E01035571,E01035572,E01035573,E01035574,E01035575,E01035576,E01035577,E01035578,E01035579,E01035580,E01035581,E01035582,E01035583,E01035584,E01035585,E01035586,E01035587,E01035588,E01035589,E01035590,E01035591,E01035592,E01035593,E01035594,E01035595,E01035596,E01035597,E01035598,E01035599,E01035600,E01035601,E01035602,E01035603,E01035604,E01035605,E01035606,E01035607,E01035608,E01035609,E01035610,E01035611,E01035612,E01035613,E01035614,E01035615,E01035616,E01035617,E01035618,E01035619,E01035620,E01035621,E01035622,E01035623,E01035624,E01035625,E01035626,E01035627,E01035628,E01035629,E01035630,E01035631,E01035632,E01035633,E01035634,E01035635,E01035636,E01035637,E01035638,E01035639,E01035640,E01035641,E01035642,E01035643,E01035644,E01035645,E01035646,E01035647,E01035648,E01035649,E01035650,E01035651,E01035652,E01035653,E01035654,E01035655,E01035656,E01035657,E01035658,E01035659,E01035660,E01035661,E01035662,E01035663,E01035664,E01035665,E01035666,E01035667,E01035668,E01035669,E01035670,E01035671,E01035672,E01035673,E01035674,E01035675,E01035676,E01035677,E01035678,E01035679,E01035680,E01035681,E01035682,E01035683,E01035684,E01035685,E01035686,E01035687,E01035688,E01035689,E01035690,E01035691,E01035692,E01035693,E01035694,E01035695,E01035696,E01035697,E01035698,E01035699,E01035700,E01035701,E01035702,E01035703,E01035704,E01035705,E01035706,E01035707,E01035708,E01035709,E01035710,E01035711,E01035712,E01035713,E01035714,E01035715,E01035716,E01035717,E01035718,E01035719,E01035720,E01035721,E01035722,E01035723,E01035724,E01035725,E01035726,E01035727,E01035728,E01035729,E01035730,E01035731,E01035733,E01035734,E01035735,E01035736,E01035737,E01035738,E01035739,E01035740,E01035741,E01035742,E01035743,E01035744,E01035745,E01035746,E01035747,E01035748,E01035749,E01035750,E01035751,E01035752,E01035753,E01035754,E01035755,E01035756,E01035757,E01035758,E01035759,E01035760,E01035761,E01035762&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_lsoa_tenure5a_bedrooms_068.json.gz + content_sha256: 3c2d87d64a00c752c1ea0de6b14693fdae032b592992953c3a6d68ae0003cef3 +- id: ons_geo_lsoa21_pwc + publisher: Office for National Statistics + title: Lower layer Super Output Areas (December 2021) EW Population Weighted Centroids + url: https://open-geography-portalx-ons.hub.arcgis.com/api/download/v1/items/32729e42d05e4e23bc7e43a36aa4ae8b/shapefile?layers=0 + landing_page: https://geoportal.statistics.gov.uk/datasets/32729e42d05e4e23bc7e43a36aa4ae8b/about + sha256: 52ad61266629ca2a312c9840a93a12c4f76efa1162ac53d71cd942beaebf59c2 + licence: 'Open Government Licence v3.0; ONS Geography licence terms: https://www.ons.gov.uk/methodology/geography/licences' + notes: Official ONS V4 population-weighted centroids; source contains both ONS and Ordnance Survey IPR. + PWC for E01027305 was revised to reflect Boundary-Line topological improvements. + filename: ons_geo_lsoa21_pwc.zip +- id: ons_geo_oa21_lsoa21 + publisher: Office for National Statistics + title: Output Area (2021) to LSOA to MSOA to LAD (December 2021) Exact Fit Lookup in EW (V3) + url: https://open-geography-portalx-ons.hub.arcgis.com/api/download/v1/items/b9ca90c10aaa4b8d9791e9859a38ca67/csv?layers=0 + landing_page: https://geoportal.statistics.gov.uk/datasets/b9ca90c10aaa4b8d9791e9859a38ca67/about + sha256: 1b7e715936374c586efbe669d82e3f3d6d093dc5295c2d99ce6ea2349f85153a + licence: 'Open Government Licence v3.0; ONS Geography licence terms: https://www.ons.gov.uk/methodology/geography/licences' + notes: Official exact-fit OA21-to-LSOA21 hierarchy; join with ons_geo_oa21_region on OA21CD, require + all constituent OAs of each LSOA have one government office region. + filename: ons_geo_oa21_lsoa21.csv +- id: ons_geo_oa21_region + publisher: Office for National Statistics + title: Output Area (2021) to Region (December 2021) Exact Fit Lookup in EW + url: https://www.arcgis.com/sharing/rest/content/items/efda0d0e14da4badbd8bdf8ae31d2f00/data + landing_page: https://geoportal.statistics.gov.uk/datasets/efda0d0e14da4badbd8bdf8ae31d2f00/about + sha256: 87bc05c0f667e0578f052ab9f3a7f21628aa658c37a3bf3fba8e5482dd6b2f60 + licence: 'Open Government Licence v3.0; ONS Geography licence terms: https://www.ons.gov.uk/methodology/geography/licences' + notes: Official exact-fit OA21-to-government office region hierarchy; fields use RGN22 names despite + December 2021 title. + filename: ons_geo_oa21_region.csv +- id: voa_english_brma_may2020 + publisher: Valuation Office Agency + title: 'English BRMA GIS: Broad Rental Market Area boundary layer applicable May 2020' + url: https://assets.publishing.service.gov.uk/media/5ee0ef64d3bf7f1ec05388df/GML.zip + landing_page: https://www.gov.uk/government/publications/broad-rental-market-area-boundary-layer-for-geographical-information-system-gis-applicable-may-2020 + sha256: 66515b0a1403e4557e6501d2d5ada5e7e17b29deef2da9165ba515a66907f6c6 + licence: Open Government Licence v3.0 (GOV.UK publication footer; no separately stated file licence) + notes: Official VOA download published 4 June 2020 for May 2020. The ZIP contains GML and XSD, despite + the landing page calling it a shapefile. Direct XML parsing preserves all polygon members in nine + MultiPolygons incorrectly declared Polygon by the XSD, including 63 Kernow West components. Shapely + make_valid repairs self-intersections in Basingstoke, Exeter, Mid & West Dorset, Newbury, Oxford, + Reading and West Cheshire. All 152 BRMAs are valid and nonempty, EPSG:27700. + filename: brma_england_voa_may2020.zip +- id: ons_nomis_ts054 + publisher: Office for National Statistics + title: 'Census 2021 TS054 tenure of household: England and Wales bulk CSV release' + url: https://www.nomisweb.co.uk/output/census/2021/census2021-ts054.zip + landing_page: https://www.nomisweb.co.uk/output/census/2021/census2021-ts054.zip + sha256: aa07311021776a93fdd7e8f6b3ad261b919305ef821ae68174fb1a66b85304c2 + licence: Open Government Licence v3.0 + notes: 'Nomis Census 2021 TS054 bulk, metadata version 2. Exact private-rented totals per English and + Welsh LSOA21: private landlord/letting agency plus other private rented. Aggregate equals both components; + rent-free is excluded.' + filename: census_nomis_ts054.zip +- id: geo_lsoa21_wales_pwc + publisher: Office for National Statistics + title: 'Lower layer Super Output Areas (December 2021) EW Population Weighted Centroids: Welsh LSOAs' + url: https://services1.arcgis.com/ESMARspQHYMw9BZ9/arcgis/rest/services/LSOA_PopCentroids_EW_2021_V4/FeatureServer/0/query?where=LSOA21CD+LIKE+%27W%25%27&outFields=%2A&outSR=27700&returnGeometry=true&orderByFields=LSOA21CD&f=geojson + landing_page: https://www.data.gov.uk/dataset/e3e903a6-1864-4083-8837-017b6bdf8cc5/lower-layer-super-output-areas-december-2021-ew-population-weighted-centroids1 + sha256: 4a072579da4bc4913949d19e742009280ed8dbe97712e75dd9fc1a5c977291a3 + licence: Open Government Licence v3.0; contains Ordnance Survey and ONS intellectual property rights + notes: Unmodified ArcGIS GeoJSON response; 1,917 Welsh LSOAs selected by LSOA21CD LIKE W%; all fit below + 2,000 feature query limit. Spatial reference EPSG:27700. The V4 revision adjusts only an English centroid. + filename: geo_lsoa21_wales_pwc.geojson +- id: geo_brma_wales_2014 + publisher: Welsh Government / Rent Officers Wales + title: 'Broad Rental Market Areas (Wales): Rent Officers Wales release of 23 September 2014' + url: https://datashare.ed.ac.uk/bitstreams/c2fe4c38-fd3e-4498-a4f9-62ac32163311/download + landing_page: https://datashare.ed.ac.uk/handle/10283/2615?show=full + sha256: 49de861258ba945eb659648d2a040aef1e9089ac80374f0bf4e85fcb062edd6c + licence: Open Government Licence + notes: Third-party archived copy of an official Rent Officers Wales release, deposited at Edinburgh + DataShare by Owen Boswarva. ZIP encloses the official 23 September 2014 correspondence explicitly + granting OGL and stating that West Cheshire covers north-east Wales. The May 2012 ESRI shapefile is + used; depositor-created MapInfo files are excluded. Contains 22 Welsh BRMAs plus West Cheshire; make_valid + repairs three invalid geometries. + filename: geo_brma_wales_2014.zip +- id: census_wales_lsoa_tenure5a_bedrooms_001 + publisher: Office for National Statistics + title: 'Census 2021 Welsh LSOA households by tenure5a and bedrooms: batch 001' + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,W01000003,W01000004,W01000005,W01000006,W01000007,W01000008,W01000009,W01000010,W01000011,W01000012,W01000013,W01000014,W01000015,W01000016,W01000017,W01000018,W01000019,W01000021,W01000022,W01000023,W01000024,W01000025,W01000026,W01000027,W01000028,W01000029,W01000030,W01000031,W01000033,W01000034,W01000035,W01000036,W01000037,W01000038,W01000040,W01000041,W01000042,W01000044,W01000047,W01000050,W01000051,W01000052,W01000053,W01000054,W01000056,W01000057,W01000059,W01000060,W01000061,W01000062,W01000063,W01000064,W01000065,W01000066,W01000067,W01000068,W01000070,W01000071,W01000072,W01000073,W01000074,W01000075,W01000076,W01000077,W01000078,W01000080,W01000081,W01000082,W01000083,W01000084,W01000085,W01000086,W01000087,W01000088,W01000089,W01000090,W01000091,W01000092,W01000093,W01000094,W01000095,W01000096,W01000097,W01000098,W01000099,W01000100,W01000101,W01000102,W01000103,W01000104,W01000105,W01000106,W01000107,W01000108,W01000109,W01000110,W01000111,W01000112,W01000113,W01000114,W01000115,W01000116,W01000117,W01000118,W01000119,W01000121,W01000122,W01000123,W01000124,W01000125,W01000126,W01000127,W01000128,W01000129,W01000130,W01000131,W01000132,W01000133,W01000134,W01000135,W01000136,W01000137,W01000138,W01000139,W01000140,W01000141,W01000142,W01000143,W01000144,W01000145,W01000146,W01000147,W01000148,W01000149,W01000150,W01000151,W01000152,W01000153,W01000154,W01000155,W01000156,W01000157,W01000158,W01000159,W01000162,W01000163,W01000164,W01000165,W01000166,W01000167,W01000168,W01000169,W01000170,W01000171,W01000172,W01000173,W01000174,W01000175,W01000176,W01000177,W01000178,W01000179,W01000180,W01000181,W01000182,W01000183,W01000184,W01000185,W01000186,W01000187,W01000188,W01000189,W01000190,W01000191,W01000192,W01000193,W01000194,W01000195,W01000196,W01000197,W01000198,W01000199,W01000200,W01000201,W01000202,W01000203,W01000204,W01000205,W01000206,W01000207,W01000208,W01000209,W01000210,W01000211,W01000212,W01000213,W01000214,W01000215,W01000216,W01000217,W01000218,W01000219,W01000220,W01000221,W01000222,W01000223,W01000224,W01000225,W01000226,W01000227,W01000228,W01000229,W01000230,W01000231,W01000232,W01000233,W01000234,W01000235,W01000236,W01000237,W01000238,W01000239,W01000240,W01000241,W01000242,W01000243,W01000244,W01000245,W01000246,W01000247,W01000248,W01000249,W01000250,W01000251,W01000252,W01000253,W01000254,W01000255,W01000256,W01000257,W01000258,W01000259,W01000260,W01000261,W01000262,W01000263,W01000264,W01000265,W01000266,W01000267,W01000268,W01000269,W01000270,W01000271,W01000272,W01000273,W01000274,W01000275,W01000276,W01000277,W01000278,W01000279,W01000280,W01000281,W01000282,W01000283,W01000284,W01000285,W01000286,W01000287,W01000288,W01000289,W01000290,W01000291,W01000292,W01000293,W01000294,W01000295,W01000296,W01000297,W01000298,W01000299,W01000300,W01000301,W01000302,W01000303,W01000304,W01000305,W01000306,W01000307,W01000308,W01000309,W01000310,W01000311,W01000312,W01000313,W01000314,W01000315,W01000316,W01000317,W01000318,W01000319,W01000320,W01000321,W01000322,W01000323,W01000324,W01000325,W01000326,W01000327,W01000328,W01000329,W01000330,W01000331,W01000332,W01000333,W01000334,W01000335,W01000336,W01000337,W01000338,W01000339,W01000340,W01000341,W01000342,W01000343,W01000344,W01000345,W01000346,W01000347,W01000348,W01000349,W01000350,W01000351,W01000354,W01000355,W01000356,W01000359,W01000360,W01000361,W01000362,W01000363,W01000364,W01000365,W01000366,W01000367,W01000368,W01000369,W01000370,W01000371,W01000372,W01000373,W01000374,W01000375,W01000376,W01000377,W01000378,W01000379,W01000380,W01000381,W01000382,W01000384,W01000385,W01000386,W01000387,W01000388,W01000389,W01000390,W01000391,W01000392,W01000393,W01000394,W01000395,W01000398,W01000399,W01000400,W01000401,W01000402,W01000403,W01000404,W01000405,W01000406,W01000407,W01000408,W01000409,W01000410,W01000411,W01000412,W01000413,W01000414,W01000415,W01000416,W01000417,W01000418,W01000419,W01000420,W01000422,W01000423,W01000424,W01000425,W01000426,W01000427,W01000428,W01000429,W01000430,W01000431,W01000432,W01000433,W01000434,W01000438,W01000439,W01000440,W01000441,W01000442,W01000443,W01000444,W01000445,W01000446,W01000447,W01000448,W01000449,W01000450,W01000451,W01000452,W01000453,W01000454,W01000455,W01000456,W01000457,W01000458,W01000459,W01000460,W01000461,W01000464,W01000465,W01000466,W01000467,W01000470,W01000471,W01000472,W01000473,W01000474,W01000475,W01000476,W01000477,W01000478,W01000479,W01000480,W01000481,W01000482,W01000483,W01000484,W01000485,W01000486,W01000487,W01000488,W01000489,W01000492,W01000493,W01000494,W01000496,W01000497,W01000498,W01000499,W01000500,W01000501,W01000502,W01000503,W01000504,W01000505,W01000506,W01000507,W01000508,W01000509,W01000510,W01000511,W01000512,W01000513,W01000515,W01000516,W01000517,W01000518,W01000519,W01000522,W01000523,W01000524,W01000525,W01000527,W01000528,W01000531,W01000533,W01000534,W01000535,W01000536,W01000537,W01000538,W01000539,W01000540,W01000541,W01000542&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_wales_lsoa_tenure5a_bedrooms_001.json.gz + content_sha256: 349cc31457a195d673c7edf6ce46a3dfa5f02d344e783e3bcc099da800f7d861 +- id: census_wales_lsoa_tenure5a_bedrooms_002 + publisher: Office for National Statistics + title: 'Census 2021 Welsh LSOA households by tenure5a and bedrooms: batch 002' + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,W01000543,W01000545,W01000546,W01000547,W01000548,W01000549,W01000550,W01000551,W01000552,W01000553,W01000554,W01000555,W01000556,W01000557,W01000558,W01000559,W01000560,W01000561,W01000562,W01000563,W01000564,W01000565,W01000566,W01000567,W01000569,W01000570,W01000571,W01000572,W01000573,W01000574,W01000575,W01000576,W01000577,W01000578,W01000579,W01000580,W01000581,W01000582,W01000583,W01000584,W01000585,W01000586,W01000587,W01000588,W01000589,W01000590,W01000591,W01000592,W01000594,W01000595,W01000596,W01000597,W01000598,W01000599,W01000600,W01000601,W01000602,W01000603,W01000604,W01000605,W01000606,W01000607,W01000608,W01000609,W01000610,W01000611,W01000612,W01000613,W01000614,W01000617,W01000618,W01000619,W01000622,W01000623,W01000624,W01000625,W01000626,W01000627,W01000628,W01000629,W01000630,W01000631,W01000632,W01000633,W01000634,W01000635,W01000636,W01000637,W01000638,W01000639,W01000640,W01000641,W01000642,W01000643,W01000644,W01000645,W01000646,W01000647,W01000648,W01000649,W01000650,W01000651,W01000652,W01000653,W01000654,W01000655,W01000656,W01000657,W01000658,W01000659,W01000660,W01000662,W01000663,W01000664,W01000665,W01000666,W01000667,W01000670,W01000671,W01000672,W01000673,W01000674,W01000675,W01000676,W01000677,W01000678,W01000679,W01000680,W01000681,W01000682,W01000683,W01000684,W01000685,W01000686,W01000687,W01000688,W01000689,W01000690,W01000691,W01000692,W01000693,W01000694,W01000695,W01000696,W01000697,W01000698,W01000699,W01000700,W01000701,W01000702,W01000703,W01000704,W01000705,W01000706,W01000707,W01000708,W01000709,W01000710,W01000711,W01000712,W01000713,W01000714,W01000715,W01000716,W01000717,W01000718,W01000719,W01000720,W01000721,W01000722,W01000723,W01000724,W01000725,W01000728,W01000729,W01000730,W01000731,W01000732,W01000733,W01000734,W01000735,W01000736,W01000737,W01000738,W01000739,W01000740,W01000741,W01000742,W01000744,W01000745,W01000746,W01000747,W01000749,W01000750,W01000751,W01000752,W01000753,W01000754,W01000755,W01000756,W01000757,W01000758,W01000759,W01000760,W01000761,W01000762,W01000763,W01000764,W01000765,W01000766,W01000767,W01000768,W01000769,W01000770,W01000771,W01000772,W01000774,W01000775,W01000776,W01000777,W01000778,W01000779,W01000781,W01000782,W01000784,W01000785,W01000786,W01000787,W01000788,W01000789,W01000790,W01000791,W01000792,W01000793,W01000794,W01000795,W01000797,W01000798,W01000799,W01000800,W01000801,W01000802,W01000803,W01000804,W01000805,W01000806,W01000807,W01000808,W01000809,W01000810,W01000811,W01000812,W01000813,W01000814,W01000815,W01000816,W01000817,W01000818,W01000819,W01000820,W01000821,W01000822,W01000823,W01000824,W01000825,W01000826,W01000827,W01000828,W01000829,W01000830,W01000831,W01000832,W01000833,W01000834,W01000835,W01000836,W01000837,W01000838,W01000839,W01000840,W01000841,W01000842,W01000843,W01000844,W01000845,W01000846,W01000847,W01000848,W01000849,W01000851,W01000852,W01000853,W01000854,W01000855,W01000856,W01000857,W01000858,W01000859,W01000860,W01000861,W01000862,W01000863,W01000864,W01000865,W01000866,W01000867,W01000868,W01000869,W01000870,W01000871,W01000872,W01000873,W01000874,W01000875,W01000876,W01000877,W01000878,W01000879,W01000880,W01000881,W01000882,W01000883,W01000884,W01000885,W01000886,W01000887,W01000888,W01000889,W01000890,W01000891,W01000892,W01000893,W01000894,W01000895,W01000896,W01000897,W01000898,W01000899,W01000900,W01000901,W01000904,W01000905,W01000907,W01000908,W01000909,W01000910,W01000911,W01000912,W01000913,W01000914,W01000915,W01000916,W01000918,W01000919,W01000921,W01000922,W01000925,W01000926,W01000927,W01000928,W01000929,W01000930,W01000931,W01000932,W01000933,W01000934,W01000935,W01000936,W01000937,W01000938,W01000939,W01000940,W01000941,W01000942,W01000943,W01000944,W01000945,W01000946,W01000947,W01000948,W01000949,W01000950,W01000951,W01000952,W01000953,W01000954,W01000955,W01000956,W01000957,W01000958,W01000959,W01000960,W01000961,W01000962,W01000963,W01000964,W01000965,W01000966,W01000967,W01000968,W01000969,W01000970,W01000971,W01000973,W01000974,W01000975,W01000976,W01000977,W01000978,W01000979,W01000980,W01000981,W01000982,W01000983,W01000984,W01000985,W01000986,W01000987,W01000989,W01000990,W01000991,W01000992,W01000993,W01000994,W01000995,W01000996,W01000997,W01000999,W01001000,W01001001,W01001002,W01001003,W01001004,W01001005,W01001006,W01001007,W01001008,W01001009,W01001010,W01001011,W01001012,W01001013,W01001014,W01001015,W01001016,W01001017,W01001018,W01001019,W01001020,W01001021,W01001022,W01001023,W01001024,W01001025,W01001026,W01001027,W01001028,W01001029,W01001030,W01001031,W01001032,W01001033,W01001034,W01001035,W01001036,W01001037,W01001038,W01001039,W01001040,W01001041,W01001042,W01001043,W01001044,W01001045,W01001046,W01001047,W01001048,W01001049,W01001050,W01001051,W01001052,W01001053,W01001054,W01001055,W01001056,W01001057,W01001058,W01001059,W01001060,W01001062,W01001063,W01001064,W01001065,W01001066,W01001067,W01001068,W01001069,W01001070,W01001071,W01001072&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_wales_lsoa_tenure5a_bedrooms_002.json.gz + content_sha256: dab6a9b66947188d8e37bea9f4cbddfe7fc1ec1cdafdd643fe7d8b6afeabae92 +- id: census_wales_lsoa_tenure5a_bedrooms_003 + publisher: Office for National Statistics + title: 'Census 2021 Welsh LSOA households by tenure5a and bedrooms: batch 003' + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,W01001073,W01001075,W01001076,W01001077,W01001078,W01001079,W01001080,W01001081,W01001082,W01001083,W01001084,W01001085,W01001086,W01001087,W01001088,W01001089,W01001090,W01001091,W01001092,W01001093,W01001094,W01001095,W01001096,W01001097,W01001098,W01001099,W01001100,W01001101,W01001102,W01001103,W01001104,W01001105,W01001106,W01001107,W01001108,W01001109,W01001110,W01001111,W01001112,W01001113,W01001114,W01001115,W01001116,W01001117,W01001119,W01001120,W01001121,W01001122,W01001123,W01001124,W01001125,W01001127,W01001128,W01001129,W01001130,W01001131,W01001132,W01001133,W01001134,W01001135,W01001136,W01001137,W01001138,W01001139,W01001140,W01001141,W01001142,W01001143,W01001144,W01001145,W01001146,W01001147,W01001148,W01001149,W01001150,W01001151,W01001152,W01001153,W01001154,W01001155,W01001156,W01001157,W01001158,W01001159,W01001160,W01001162,W01001164,W01001165,W01001166,W01001167,W01001168,W01001169,W01001170,W01001171,W01001172,W01001173,W01001174,W01001175,W01001176,W01001177,W01001178,W01001179,W01001180,W01001181,W01001184,W01001185,W01001186,W01001187,W01001188,W01001189,W01001190,W01001192,W01001193,W01001194,W01001195,W01001196,W01001197,W01001198,W01001199,W01001202,W01001203,W01001204,W01001205,W01001206,W01001207,W01001208,W01001209,W01001210,W01001211,W01001212,W01001214,W01001216,W01001217,W01001218,W01001219,W01001220,W01001221,W01001222,W01001223,W01001224,W01001225,W01001226,W01001227,W01001228,W01001229,W01001230,W01001231,W01001232,W01001233,W01001234,W01001235,W01001236,W01001237,W01001238,W01001239,W01001240,W01001241,W01001242,W01001243,W01001244,W01001245,W01001246,W01001247,W01001248,W01001249,W01001250,W01001251,W01001252,W01001253,W01001255,W01001256,W01001257,W01001258,W01001259,W01001260,W01001261,W01001262,W01001263,W01001264,W01001265,W01001266,W01001267,W01001268,W01001269,W01001270,W01001271,W01001272,W01001273,W01001274,W01001275,W01001276,W01001277,W01001278,W01001279,W01001280,W01001281,W01001282,W01001283,W01001284,W01001285,W01001286,W01001287,W01001288,W01001289,W01001290,W01001291,W01001292,W01001293,W01001294,W01001295,W01001296,W01001297,W01001298,W01001299,W01001300,W01001301,W01001302,W01001305,W01001306,W01001307,W01001308,W01001309,W01001310,W01001311,W01001312,W01001313,W01001314,W01001315,W01001317,W01001318,W01001319,W01001320,W01001321,W01001322,W01001324,W01001325,W01001326,W01001327,W01001328,W01001329,W01001330,W01001331,W01001332,W01001333,W01001334,W01001335,W01001336,W01001337,W01001338,W01001339,W01001340,W01001341,W01001342,W01001343,W01001344,W01001345,W01001346,W01001347,W01001348,W01001349,W01001350,W01001351,W01001352,W01001353,W01001354,W01001355,W01001356,W01001357,W01001358,W01001359,W01001360,W01001361,W01001362,W01001363,W01001364,W01001365,W01001366,W01001367,W01001368,W01001369,W01001370,W01001371,W01001372,W01001373,W01001374,W01001375,W01001376,W01001377,W01001378,W01001379,W01001380,W01001381,W01001382,W01001383,W01001384,W01001385,W01001386,W01001387,W01001388,W01001389,W01001390,W01001391,W01001392,W01001393,W01001394,W01001395,W01001396,W01001397,W01001398,W01001399,W01001400,W01001401,W01001402,W01001403,W01001404,W01001405,W01001406,W01001407,W01001408,W01001409,W01001410,W01001411,W01001412,W01001413,W01001414,W01001415,W01001416,W01001417,W01001418,W01001419,W01001420,W01001421,W01001422,W01001423,W01001424,W01001425,W01001426,W01001427,W01001428,W01001429,W01001430,W01001431,W01001432,W01001433,W01001434,W01001435,W01001436,W01001437,W01001438,W01001439,W01001440,W01001441,W01001442,W01001443,W01001444,W01001445,W01001446,W01001447,W01001448,W01001449,W01001450,W01001451,W01001452,W01001453,W01001454,W01001455,W01001456,W01001457,W01001458,W01001459,W01001460,W01001461,W01001462,W01001463,W01001464,W01001465,W01001466,W01001467,W01001468,W01001469,W01001470,W01001471,W01001472,W01001473,W01001474,W01001475,W01001478,W01001479,W01001480,W01001481,W01001482,W01001483,W01001484,W01001485,W01001486,W01001487,W01001488,W01001489,W01001490,W01001491,W01001492,W01001493,W01001494,W01001495,W01001496,W01001497,W01001498,W01001499,W01001500,W01001501,W01001502,W01001503,W01001504,W01001505,W01001506,W01001507,W01001508,W01001509,W01001510,W01001511,W01001512,W01001513,W01001514,W01001515,W01001516,W01001517,W01001518,W01001519,W01001520,W01001521,W01001522,W01001523,W01001524,W01001525,W01001526,W01001527,W01001528,W01001529,W01001530,W01001531,W01001532,W01001533,W01001534,W01001535,W01001536,W01001537,W01001538,W01001539,W01001540,W01001541,W01001542,W01001543,W01001545,W01001546,W01001548,W01001549,W01001551,W01001552,W01001554,W01001555,W01001556,W01001557,W01001558,W01001559,W01001560,W01001561,W01001562,W01001563,W01001565,W01001568,W01001569,W01001571,W01001572,W01001573,W01001574,W01001575,W01001576,W01001577,W01001578,W01001579,W01001580,W01001581,W01001582,W01001583,W01001584,W01001585,W01001586,W01001587,W01001588,W01001589,W01001590,W01001591,W01001592,W01001593,W01001594,W01001595,W01001596,W01001597,W01001598,W01001599&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_wales_lsoa_tenure5a_bedrooms_003.json.gz + content_sha256: e5c35c196eca90ca87b32f79d91458c9be40655a56044b9bc1bea9a3877691c6 +- id: census_wales_lsoa_tenure5a_bedrooms_004 + publisher: Office for National Statistics + title: 'Census 2021 Welsh LSOA households by tenure5a and bedrooms: batch 004' + url: https://api.beta.ons.gov.uk/v1/population-types/HH/census-observations?area-type=lsoa,W01001600,W01001601,W01001602,W01001603,W01001604,W01001605,W01001606,W01001607,W01001608,W01001609,W01001610,W01001611,W01001612,W01001613,W01001614,W01001615,W01001616,W01001617,W01001618,W01001619,W01001620,W01001621,W01001622,W01001623,W01001624,W01001625,W01001626,W01001627,W01001628,W01001629,W01001630,W01001631,W01001632,W01001633,W01001634,W01001635,W01001636,W01001637,W01001638,W01001639,W01001640,W01001641,W01001642,W01001643,W01001645,W01001646,W01001648,W01001650,W01001651,W01001652,W01001653,W01001654,W01001655,W01001657,W01001658,W01001659,W01001660,W01001661,W01001662,W01001663,W01001664,W01001665,W01001666,W01001667,W01001668,W01001669,W01001670,W01001671,W01001673,W01001674,W01001675,W01001676,W01001677,W01001678,W01001679,W01001680,W01001681,W01001682,W01001683,W01001684,W01001685,W01001686,W01001687,W01001689,W01001690,W01001691,W01001692,W01001693,W01001694,W01001695,W01001696,W01001697,W01001698,W01001699,W01001702,W01001703,W01001704,W01001705,W01001706,W01001707,W01001708,W01001710,W01001711,W01001712,W01001713,W01001714,W01001715,W01001716,W01001717,W01001718,W01001719,W01001720,W01001721,W01001722,W01001724,W01001725,W01001726,W01001727,W01001728,W01001729,W01001730,W01001731,W01001732,W01001733,W01001734,W01001735,W01001736,W01001737,W01001738,W01001739,W01001740,W01001741,W01001742,W01001743,W01001744,W01001745,W01001746,W01001747,W01001748,W01001749,W01001750,W01001751,W01001752,W01001753,W01001754,W01001755,W01001756,W01001757,W01001758,W01001759,W01001760,W01001761,W01001762,W01001764,W01001765,W01001766,W01001767,W01001768,W01001769,W01001770,W01001771,W01001772,W01001773,W01001774,W01001775,W01001776,W01001777,W01001778,W01001779,W01001780,W01001781,W01001782,W01001783,W01001784,W01001785,W01001786,W01001787,W01001788,W01001789,W01001790,W01001791,W01001792,W01001793,W01001794,W01001795,W01001796,W01001797,W01001798,W01001799,W01001800,W01001802,W01001803,W01001804,W01001805,W01001806,W01001807,W01001808,W01001809,W01001810,W01001811,W01001812,W01001813,W01001814,W01001815,W01001816,W01001817,W01001818,W01001819,W01001820,W01001821,W01001822,W01001823,W01001824,W01001825,W01001826,W01001827,W01001828,W01001829,W01001830,W01001831,W01001832,W01001833,W01001834,W01001835,W01001836,W01001837,W01001838,W01001839,W01001840,W01001841,W01001842,W01001844,W01001846,W01001847,W01001848,W01001849,W01001850,W01001851,W01001852,W01001853,W01001854,W01001855,W01001856,W01001857,W01001858,W01001859,W01001860,W01001861,W01001862,W01001863,W01001864,W01001865,W01001866,W01001867,W01001868,W01001869,W01001870,W01001871,W01001872,W01001873,W01001874,W01001876,W01001877,W01001878,W01001879,W01001880,W01001881,W01001882,W01001883,W01001884,W01001885,W01001886,W01001887,W01001888,W01001889,W01001890,W01001891,W01001892,W01001893,W01001894,W01001895,W01001896,W01001897,W01001898,W01001899,W01001900,W01001901,W01001902,W01001903,W01001904,W01001905,W01001906,W01001907,W01001908,W01001910,W01001911,W01001912,W01001913,W01001914,W01001915,W01001916,W01001917,W01001918,W01001919,W01001920,W01001921,W01001922,W01001923,W01001924,W01001925,W01001926,W01001927,W01001928,W01001929,W01001930,W01001931,W01001932,W01001933,W01001935,W01001936,W01001937,W01001940,W01001942,W01001943,W01001944,W01001946,W01001947,W01001948,W01001949,W01001950,W01001951,W01001952,W01001953,W01001954,W01001955,W01001956,W01001958,W01001959,W01001960,W01001961,W01001962,W01001963,W01001964,W01001965,W01001966,W01001967,W01001968,W01001969,W01001970,W01001971,W01001972,W01001973,W01001974,W01001975,W01001976,W01001977,W01001978,W01001979,W01001980,W01001981,W01001982,W01001983,W01001984,W01001985,W01001986,W01001987,W01001988,W01001989,W01001990,W01001991,W01001992,W01001993,W01001994,W01001995,W01001996,W01001997,W01001998,W01001999,W01002000,W01002001,W01002002,W01002003,W01002004,W01002005,W01002007,W01002008,W01002009,W01002010,W01002011,W01002012,W01002013,W01002014,W01002016,W01002017,W01002018,W01002019,W01002020,W01002021,W01002022,W01002023,W01002024,W01002025,W01002026,W01002027,W01002028,W01002029,W01002030,W01002031,W01002032,W01002033,W01002034,W01002035,W01002036,W01002037,W01002038,W01002039,W01002040&dimensions=hh_tenure_5a,number_bedrooms_5a + landing_page: https://www.ons.gov.uk/datasets/create + licence: Open Government Licence v3.0 + notes: 'Census 2021 HH custom query: hh_tenure_5a × number_bedrooms_5a, explicit LSOA21 batch. Category + 3 (private rented or lives rent free) supplies bedroom proportions multiplied by exact TS054 private-rented + totals; no blocked areas. content_sha256 pins the decoded JSON bytes, without parsing or reserialising. + Cache may contain gzip entity bytes or the decoded body; transport compression is not pinned.' + filename: census_wales_lsoa_tenure5a_bedrooms_004.json.gz + content_sha256: 18f40e236f6d052fc8dc8095af2337abf0edead62fe8abfcb141e67ff63bf546 +- id: scot_brma_boundaries_2009 + publisher: Scottish Government + title: Broad Rental Market Areas (2009 boundaries, revised 28 July 2015) + url: https://maps.gov.scot/ATOM/shapefiles/SG_BroadRentalMarketAreas_2009.zip + landing_page: https://www.data.gov.uk/dataset/39d08296-874a-4604-9ca4-6fe807aee4a8/broad-rental-market-areas + sha256: 4d121236a35ce896edeb663a7c2c390053c0deebe0cc149c84696bbfd4bddec0 + licence: 'Open Government Licence v3.0; attribution: Copyright Scottish Government, contains Ordnance + Survey data © Crown copyright and database right 2026' + notes: Official 18 BRMA polygons. ZIP original unchanged; shapefile components dated 29 July 2015. Metadata + records revision 2015-07-28. + filename: scot_brma_boundaries_2009.zip +- id: scot_foi_postcodes_brma_2023 + publisher: Scottish Government + title: FOI 202300368850 Information Released - Postcodes + url: https://www.gov.scot/binaries/content/documents/govscot/publications/foi-eir-release/2023/11/foi-202300368850/documents/foi-202300368850---information-released---postcodes/foi-202300368850---information-released---postcodes/govscot%3Adocument/FOI%2B202300368850%2B-%2BInformation%2BReleased%2B-%2BPostcodes.xlsx + landing_page: https://www.gov.scot/publications/foi-202300368850/ + sha256: 50366fea2b87bd6b697e7aa8736fbc870838b5e327458213294f31c3de7ede5e + licence: Open Government Licence v3.0 + notes: Official exact postcode-to-BRMA candidate sets supply nine missing polygon assignments and correct + 59 unique Balloch OA assignments. Two further conflicting ambiguous candidate sets are resolved by + the documented West Dunbartonshire council overrides in build.py, established on 2 October 2026. + filename: scot_foi_postcodes_brma_2023.xlsx +- id: scot_oa2022_population_weighted_centroids + publisher: National Records of Scotland + title: 2022 Census Output Area Population Weighted Centroids + url: https://www.nrscotland.gov.uk/media/gm4dvdsv/output-area-2022-pwc.zip + landing_page: https://www.nrscotland.gov.uk/publications/2022-census-geography-products/ + sha256: 509d7d937c8cc1477259dcd8e4e0e61240f5cc782721a79e9c279bd589e0ec0c + licence: 'Open Government Licence v3.0; attribution: Copyright National Records of Scotland, contains + Ordnance Survey data © Crown copyright and database right 2026' + notes: 46,363 output area population-weighted centroids, selected from OA master postcodes; EPSG:27700. + Original ZIP unchanged. + filename: scot_oa2022_population_weighted_centroids.zip +- id: scot_census2022_geography_index + publisher: National Records of Scotland + title: '2022 Census Index: Postcode to OA and OA to higher areas' + url: https://www.nrscotland.gov.uk/media/utrbt5ze/census_2022_index.zip + landing_page: https://www.nrscotland.gov.uk/publications/2022-census-geography-products/ + sha256: 0e4096a321cba2318dc5d2e35c197bf815ac333aebdf4cb0ddce82112b57a261 + licence: Open Government Licence v3.0 + notes: OA_TO_HIGHER_AREAS.csv supplies OA-to-2022 electoral ward and 2019 council membership. Household + counts come from the centroid file; geography census counts are independently perturbed. + filename: scot_census2022_geography_index.zip +- id: nrs_census_2022_ward2022_tenure_bedrooms + publisher: National Records of Scotland + title: Scotland Census 2022 electoral-ward households by detailed tenure and bedrooms + url: https://www.scotlandscensus.gov.uk/webapi/downloadTable?type=csv + landing_page: https://www.scotlandscensus.gov.uk/webapi/home + sha256: 03d6157d34bd3df44e79ded1c03510954dee55e9e05d3c276b26ad463a565677 + licence: Crown copyright; Open Government Licence v3.0 + notes: 'Manual browser download: 1. Open https://www.scotlandscensus.gov.uk/webapi/home; continue as + a guest and read and accept the site terms yourself. No account is needed. 2. Select the Census 2022 + Household dataset for larger geographies (database household2022ftb). 3. Add all 355 Electoral Ward + 2022 values as rows, displaying ward codes rather than names (field SXV4__Census20223CHHv1__F_GEOGRAPHY__EW2022_FLD). + 4. Add Number of bedrooms as columns (field SXV4__Census20223CHHv1__F_HOUSEHOLD__BEDROOMS_CAT_H_FLD; + all five values, One to Five or more, and Total). 5. Add Household tenure as wafers/layers (field + SXV4__Census20223CHHv1__F_HOUSEHOLD__TENURE_LANDLORD_CAT_H_FLD; all eight values and Total); keep + Counting: Households. 6. Retrieve the complete table, select Download CSV, and save the unmodified + export in the cache as census_2022_ward2022_tenure_bedrooms.csv. The downloadTable URL is session-dependent; + this script never accepts terms. The pinned export includes independently perturbed cells and margins; + future exports must match its SHA256 exactly.' + filename: census_2022_ward2022_tenure_bedrooms.csv + manual: true +- id: nisra_tenure_7_data_zones + publisher: Northern Ireland Statistics and Research Agency (NISRA) + title: 'Census 2021 households by tenure: Data Zone 2021, seven categories' + url: https://build.nisra.gov.uk/en/custom/table.csv?d=HOUSEHOLD&v=DZ21&v=HH_TENURE_AGG7 + landing_page: https://build.nisra.gov.uk/ + sha256: 933303a4430b7b9075d55b7cace165a2b8114c9f1bde57e2895ed8a79a920c76 + licence: Open Government Licence v3.0; Crown copyright (NISRA crown-copyright page) + notes: Seven tenure categories for all 3,780 Data Zones. Codes 5 and 6 are private rented; no bedroom + question was asked in Northern Ireland. + filename: tenure_7_data_zones.csv +- id: nihe_lha_brma_membership_archive_2021 + publisher: Northern Ireland Housing Executive (NIHE) + title: 'NIHE Current LHA rent levels: official 80 postcode-district BRMA definition, 2021 archive' + url: https://web.archive.org/web/20210802181148id_/https://www.nihe.gov.uk/Housing-Help/Local-Housing-Allowance/Current-LHA-rent-levels + landing_page: https://www.nihe.gov.uk/Housing-Help/Local-Housing-Allowance/Current-LHA-rent-levels + sha256: ba71c9e5d63f11614b6c80284a63b8e0e0d2ae77ca07e107c1190c8331624a81 + licence: Copyright Northern Ireland Housing Executive; no express open reuse licence found in archived + page. + notes: Official-page archive on 2021-08-02. Geography membership only is used; no LHA rates extracted. + Live NIHE requests returned HTTP 403. Matches the 2024 membership list exactly. + filename: nihe_current_lha_archive_2021.html +- id: nisra_census2021_postcode_households + publisher: Northern Ireland Statistics and Research Agency (NISRA) + title: Census 2021 person and household estimates for postcodes in Northern Ireland + url: https://www.nisra.gov.uk/system/files/statistics/census-2021-person-and-household-estimates-for-postcodes-in-northern-ireland.xlsx + landing_page: https://www.nisra.gov.uk/publications/census-2021-person-and-household-estimates-postcodes-northern-ireland + sha256: 61a77375ccf91b6504c6b267fec13d4cac61d15c9a4db5cc3f946530ee796ee0 + licence: 'Open Government Licence v3.0; Source: NISRA, www.nisra.gov.uk. Excludes third-party LPS mapping + data.' + notes: Households as at 21 March 2021, published 29 February 2024. Only the "Postcode district" sheet + is used; district totals are not suppressed. Each NIHE BRMA's households are the sum of its districts, + scaled by Northern Ireland's private-rented share from the Data Zone tenure table. + filename: nisra_census2021_postcode_households.xlsx diff --git a/uv.lock b/uv.lock index a3e9f44d9..5ae821352 100644 --- a/uv.lock +++ b/uv.lock @@ -577,6 +577,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/f4/124858007ddf3c61e9b144107304c9152fa80b5b6c168da07d86fe583cc1/huggingface_hub-1.1.5-py3-none-any.whl", hash = "sha256:e88ecc129011f37b868586bbcfae6c56868cae80cd56a79d61575426a3aa0d7d", size = 516000, upload-time = "2025-11-20T15:49:30.926Z" }, ] +[[package]] +name = "hypothesis" +version = "6.168.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/a8/bd70d7c2966e561228b9fdc075ee77c0ba577dcbbfbf921edf614db14f6a/hypothesis-6.168.5.tar.gz", hash = "sha256:76b9226962fe11d40858253a967eda95bb65811365286317e0118f4ec8f808c7", size = 511115, upload-time = "2026-10-05T23:26:35.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/0c/7f04c8d277dfc828ba584b7d9d10dbac5e91fce673fa5328f7bd5bf64609/hypothesis-6.168.5-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ca43a751410a9c6685f029fd5126cc5507664cafaa76017922aa8ae2e17b6620", size = 791539, upload-time = "2026-10-05T23:24:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/11/5c/660906d83db74eb86feda715d0f2df14836205b14a183332116676733e6f/hypothesis-6.168.5-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b98707cbe9f430d100a945bbe17612fd3aa44eac1b0ac5299669fe3b8e4128", size = 787208, upload-time = "2026-10-05T23:25:14.028Z" }, + { url = "https://files.pythonhosted.org/packages/01/85/36e19492bc4ff354c2be9c8fa7c6ace0c65f9d2c7116656b741680c6ca55/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dde52a0b696c642e7f988a03026c7c29f90daf21e74507b6f865c3ccc9d536e", size = 1115200, upload-time = "2026-10-05T23:25:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/d4/82/3273fb0a3567c09b767bb8fe2824d65e16ae2abb92cf1f43762df723df94/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42f02e4541fe0c17a1320617effc0ab8a8aca2a9af15e3358d4150acf3bbdc00", size = 1145113, upload-time = "2026-10-05T23:25:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/74/59/5c5904555a0bbd4b2898d73ea90c6d03f5be0d8ff0756ac1d519ace6ae66/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf6dd7e537a12763c9afa017f7a6159e5cda608e98670621fa44596a1e8e9288", size = 1139564, upload-time = "2026-10-05T23:25:56.681Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/55654ff9575587a401e304f08ad1d43b7e6318f81c66bd866fdc5ab4665b/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df2c04cd30abf42c52580184216162a75b5508b214a472b86670f6dd50659a3b", size = 1192142, upload-time = "2026-10-05T23:26:06.565Z" }, + { url = "https://files.pythonhosted.org/packages/48/91/4cc9d6e8a950473e07e3ebf00cbb8ee0d76b14d193f94c3de20f1c09e2b1/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278662eb21aaec9eaae71ea4dabd4fe390c2af11ec58a6a0606687cf6d7689b0", size = 1157262, upload-time = "2026-10-05T23:24:59.229Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/4b8aa3be788ea81b9a7bc6b673ed89edd72fd0645c6aa691d4c159ff971a/hypothesis-6.168.5-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:6bcedc4ab8ab92dd0f3af0cfe24dce184d225751d7bc870a9cddb9a557de847f", size = 1113779, upload-time = "2026-10-05T23:24:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/f9/98/2eb4c79d1851195e6a083568b065235680ab984e984bbd472f2a7d02ba33/hypothesis-6.168.5-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b58097cc3b98d8616f635ac73888fc9f859311875f2adc043f1544c40c3c466", size = 1152639, upload-time = "2026-10-05T23:25:43.635Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9c/68f7e99b43c6f37c077669a4d3bd88f48c042444ced9e7cff0eaf44bc70a/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f8a387d9ee7f804e830b31f2e2e339ab5731665e922cfda4f6f6fbdb05e191b4", size = 1290068, upload-time = "2026-10-05T23:25:28.45Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/d4f87164a0d028ab102cea345b601d9dafb3196358df5448caa88ac3c1e2/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:326f6383fdf2e37ac69773589a8238a3bf396ca8ac8efacb0fb9ed42dd08e426", size = 1418551, upload-time = "2026-10-05T23:24:51.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/6b518a25514f0e643f95610c77e279bfbf0e0b3bd423aac0187d6f039b9a/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d33fc74e43bbd7c3a8f6f7161a8b93b676924286e97e70e828c6e0dcee5c01f", size = 1372130, upload-time = "2026-10-05T23:25:32.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/c2/32538e14e63193ca894ba584696805d1eb45cfc27e15fccd47acfb87531c/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:1994923cf5e5220ae6bf19645302504b27c0289d83e5d8690df71dcae63d8416", size = 1268950, upload-time = "2026-10-05T23:25:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/e50e7e98af9489aa05203c2ab38c95d891dd8d1ed08fad972dcdb6955332/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:501038fd24d3bc95239cfd093a23cf1151f29dd82382a3554dac5dfdab9729ae", size = 1283907, upload-time = "2026-10-05T23:24:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/41c460a7d2148a04b212b2d594d39992fb52e0b844e13bf6784573fc8dea/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2292ddc24fe6d04b7d30fa6a7e2c9e280ad5078fe671d0bf4aa6df6e143b5ac", size = 1323762, upload-time = "2026-10-05T23:24:18.984Z" }, + { url = "https://files.pythonhosted.org/packages/68/4f/37a7fc1fe445e3589e0f56ff4573c28de1d6e6a03009cba2f99f04e46ffa/hypothesis-6.168.5-cp310-abi3-win32.whl", hash = "sha256:925d67c69b719d416334aa961c0cdfc4a58a471af1ebd2d7101bd515a70f4e5f", size = 677903, upload-time = "2026-10-05T23:25:07.129Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/7b25ca7845a60522ebc5f8054f6bba68d47126fb5d940c784fc528a4be4a/hypothesis-6.168.5-cp310-abi3-win_amd64.whl", hash = "sha256:2311590eccba452de863dfe3466daa86a05c25f072ab31ed8bb4d3313ee68439", size = 684616, upload-time = "2026-10-05T23:25:04.028Z" }, + { url = "https://files.pythonhosted.org/packages/c3/00/40e7c36b46c8788eddc7a322ad324e6db53c8ab9a8b9a95d6535ee7bdaaf/hypothesis-6.168.5-cp310-abi3-win_arm64.whl", hash = "sha256:222a6d23a2a824b0f9f73761c2fb9cd2aca96cf3e5b441617625bce4f7eb4fd4", size = 683061, upload-time = "2026-10-05T23:25:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b3/1f7f72cd28d02a5ca99c432fbffe4b750a375df2284af9d916943dd3aa4f/hypothesis-6.168.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:714337b25ca9137bc359c570b868269462307e120999412ca1946f997f4b9db5", size = 793075, upload-time = "2026-10-05T23:25:15.905Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ba/5b0874828695c4d49e3858d0967254f783e563cd0e211a6db27d11d48a1f/hypothesis-6.168.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f1c3617155fcf5b5259a1f2e4c775d3eec7bfa80b162b2f6f145b08f871ab08", size = 784693, upload-time = "2026-10-05T23:24:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5f/ca777becba5251b0d778bb9d83d15524c559a07e4b5d4e6211473855bae2/hypothesis-6.168.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebee70b7a026210bb47c86c89e5bfb42effd5bd630080e76bc084f29c01c7f7a", size = 1113693, upload-time = "2026-10-05T23:24:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/5a74bf329e405db3edc5639a2595eccf33ad6f5aaa191019e9f824d630f4/hypothesis-6.168.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cfb06b31cca005345b8ad63f88986d21fd359a7dc3dba2965dd3515b720e5c9", size = 1156530, upload-time = "2026-10-05T23:24:47.153Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/e79cf67f03f212a1394abac21053bd6887aa70f557be1da3f9c9c73e58ae/hypothesis-6.168.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a4c244d7ab64963fb575f0ec2d813630e1d14cefc39e7c460d5d778e5af4118", size = 1288713, upload-time = "2026-10-05T23:25:22.763Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/df452159ac8d7b278071a3e81fafc69da833ec4302b8c85f5b6e530aea21/hypothesis-6.168.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e59d519f6fb38b3fa4fcde046767b03a24740fe827d261ee7ff9a721c06169b", size = 1323134, upload-time = "2026-10-05T23:26:02.485Z" }, + { url = "https://files.pythonhosted.org/packages/af/fb/f07d8d09fb57eb14555cad64dfbe29bfdcecff3806f1e01268258088e741/hypothesis-6.168.5-cp313-cp313-win_amd64.whl", hash = "sha256:c103f655644afa4ef6bf7efbf86e44b78ee475fd0691da2db86e2cfe72c07234", size = 682019, upload-time = "2026-10-05T23:24:22.888Z" }, + { url = "https://files.pythonhosted.org/packages/de/e9/7c3c2262b8cfa825c4c1764d62aa15e628bae257ccfd2ee4f3ffa4f81eaa/hypothesis-6.168.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c4dc037d8001bc6eccb8636f4a38d16ea6b250d6bf0a89075aaa5e5069f751cc", size = 793253, upload-time = "2026-10-05T23:26:14.331Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/7909ed7d29302491e9b7bc0e7ac3287c20736c05a0cc35bae65024aeec3b/hypothesis-6.168.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c90743321f29b65491d146adfc2ece85869bacb71ce18b47674795e896c81ee3", size = 784837, upload-time = "2026-10-05T23:25:24.728Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/57742c459349052e6a3e0c011855840f8cbbbadca91079d5b591f08b25ae/hypothesis-6.168.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09debb7f7f0f229da5f7e2ad515a5be7a8dc607ec204074775f8ab6731a447f0", size = 1114268, upload-time = "2026-10-05T23:24:27.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/07bd2449f91f9426f705fb689429bab6e26d1365f8ac4ef7d7c1cec9055e/hypothesis-6.168.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d227f8ac497eca0bde4e8562d32dd4e82fc9566526020bbd567f76b833b923b0", size = 1156838, upload-time = "2026-10-05T23:24:35.211Z" }, + { url = "https://files.pythonhosted.org/packages/fe/75/7f3dda517e5134f73e2ae41821bf40b3fd3ac6551a9a43ea9287471738a1/hypothesis-6.168.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc6ebd35601c72c842e5899c3f760f9ed26c69e786ee40a9a64fb5a4a3058315", size = 1288864, upload-time = "2026-10-05T23:24:42.645Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cd/4b1364140642cf3f1431ca59b5841fc322872dfa7197b2facb97692da234/hypothesis-6.168.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:503e103ad49e702bad200157d82778eebbc14d3045e9700a8e8fe5db40912953", size = 1323300, upload-time = "2026-10-05T23:24:14.826Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/47d7cffcaf15318a4308516b6b3d2fd0db599f18eacc0f2dc553be2206a7/hypothesis-6.168.5-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:bc5cc310f9f86ec62f0d0dd7eea5a4788f18ec793b70ee2c7163b916768e1057", size = 624239, upload-time = "2026-10-05T23:24:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/97/6e/2ca0f68150be175b7cfa7bfb6692260638d86aeb9313478ba82e198186e6/hypothesis-6.168.5-cp314-cp314-win_amd64.whl", hash = "sha256:71ce0599e806ce3a68f9f118edf450bf091e11b134f6bcc5f8dd706b42c91ebc", size = 681884, upload-time = "2026-10-05T23:25:00.757Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/4fc2b5970df0c27668ec08abc505f1d01314a69953f89dc0edc6528ff5a0/hypothesis-6.168.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f66b02c9e95e916a2c58f725a92377ec988146ed7b5aeccd5e78ceecac1eae6f", size = 791636, upload-time = "2026-10-05T23:26:22.365Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/03cdf5f052dcb441e045be1fd0aa531e85cde1a1cbabab60968625c570a3/hypothesis-6.168.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bab27926e1d1575fb43b70d4aeece05b74a5e477af0509b56cb6fd778070dd93", size = 783280, upload-time = "2026-10-05T23:26:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/615557af244e2f3ce4763029c03a62ed82dcbbd646b72a8c479ef0408b33/hypothesis-6.168.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edeb42c3009b5652dc1c44907ec91bfe9284100ad5e57993dfebabb76f2961a1", size = 1112149, upload-time = "2026-10-05T23:24:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/67/a6707fcd51dc5f2531bf88ac072e99f31ab9d8020488b01349a6d2981081/hypothesis-6.168.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8977456328147c521a16a089325017b2c728fddc23351693a4fd924cc7fc7001", size = 1155637, upload-time = "2026-10-05T23:24:24.047Z" }, + { url = "https://files.pythonhosted.org/packages/85/d4/ac2e852d2f163afd398854662bcbb0b849a767abbf2f95a75de6f685f821/hypothesis-6.168.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:36ecf7ac351f9c0b5489ba800884b607da754e88ef40713fbfcc170d2151e6eb", size = 1287040, upload-time = "2026-10-05T23:25:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/f77b1602704bda6ff3d9d0817120bd7fb94fd792bd25508b36ce4b8bd2a2/hypothesis-6.168.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0333aa5129ba3019a83fb81a7f0fc238180e415a9edddd9a15101f8deaaa517e", size = 1322166, upload-time = "2026-10-05T23:25:45.39Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2e/94138a73e0906b31cb5968d20be58688f582a09e5958f2c75d45a7049545/hypothesis-6.168.5-cp314-cp314t-win_amd64.whl", hash = "sha256:2fcb87341d76ae0183e8219c9a14d55957c50d14973879db5fea3e81da45ba1a", size = 681599, upload-time = "2026-10-05T23:24:37.827Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/92cb8092b680be2b6ec5ffe83b9f1a98dbf414117566f9e3ba4e8b569214/hypothesis-6.168.5-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:453ab7d0a1fadbaa54ae8722d22463cc2046fa8ef25b9b88715d28279bf79fc1", size = 790950, upload-time = "2026-10-05T23:24:45.852Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/78aea12694e3d1177e2980d44798b6d93e191faf59155b18bf5ae315f6a2/hypothesis-6.168.5-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:bbdbc43d1f9dad595b249b7bbe8ee5102bc94a4fcb0a79ff76d20e41fcfe342a", size = 782939, upload-time = "2026-10-05T23:25:39.961Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/5ef9947b2d149f773428e555bdf66688405aa5167510bdbe97c8ec5c6090/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bc36194d7b6083591060836c7872711a6820217b325bf432dd7e10b3d4af5cb", size = 1111380, upload-time = "2026-10-05T23:24:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/e1/65/7e668e203fb2659c6214dc0c24cc09b7dea8a02c7c8d0ad338f644a054c4/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22425e2b1543a43c157a81472c713ba8f291cbaf054c70ffe128e2cacc294f65", size = 1142644, upload-time = "2026-10-05T23:26:30.697Z" }, + { url = "https://files.pythonhosted.org/packages/c0/77/b112978676e795658d58c4294bf90cdbb8cb56cb8292c8c4874650468cf9/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eea0bc513d0e38d1d5ddfb581132928871cd02dc54dfe4511a5396727c48e9d0", size = 1135966, upload-time = "2026-10-05T23:24:49.806Z" }, + { url = "https://files.pythonhosted.org/packages/e6/27/cd3bf01e8246c4318ec3df15f5eeeee3214f444df0129a6c7f9a62859ee8/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb142bc70bbf6645e15c7ca72de3f7c8dae198aa2743a609f4f3e3bb4f9c3a52", size = 1189646, upload-time = "2026-10-05T23:26:04.471Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a6/4d3e882f31c289e432dfec34dbb9029296038a8c69e8b28cebb0a5fb7ea8/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a27b758707bd37f5a1759cca6eef83fe1a212c38dc4ca0a203434004c5647d15", size = 1154770, upload-time = "2026-10-05T23:25:10.814Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/96f5455e1b3d0409cbbb1434c98e792bcceefd614a4b11e600072520b487/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:77a111cb50c330fa7098f65852fa17a01ecd781a85be3cf5e5871bdeeeb0ecbc", size = 1109925, upload-time = "2026-10-05T23:25:26.369Z" }, + { url = "https://files.pythonhosted.org/packages/3d/64/0758985d9d36f0c5ec981a1457ea1c8173f62d46a94531417aec117df4d7/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdd0afc13e86ec76cae3d3659569c1f601f4e9ca52b5cf91c1685979eae64d7b", size = 1148476, upload-time = "2026-10-05T23:24:54.552Z" }, + { url = "https://files.pythonhosted.org/packages/43/5c/a9b8953e1d8aefcd3c22cf8d10dd8acf93e602b903278e2e51cf8544ccea/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:5fefb02035864c3d322e3b0969b296250923fdcfb574ea1ad4374f1a6333f663", size = 1285856, upload-time = "2026-10-05T23:25:38.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/66098444dc832523ddc4f2e05723662834e5f99bba3c759615d059f6420e/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:9db8aa1f5529e1b577ec18b775c2fb4225821712e946f7762b90c966604faf83", size = 1415500, upload-time = "2026-10-05T23:24:21.682Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/e971b6fe20ef8d7c2019cbf24b4f6149468efc88c42a744f5bc99e6ca0ed/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:59e07d2f62b5ff573b0059959ae9cef9edfb0f5393fdb35ea81fce1ee77b27ac", size = 1368623, upload-time = "2026-10-05T23:26:11.292Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ac/b279dfbd2c06cdb3030ba7eea042cb2cf0171d0013563d103d5207dde63b/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:8a03ca128bea29d6826fc545f1f6289fb1ea2e83a5bb811321761b2d515ca575", size = 1264457, upload-time = "2026-10-05T23:25:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/55/57/16ac9f8ddfada1cd278bd2185234d0d36ebd304926b69ad0497c210c6fed/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:5c03f2d3f84f626f3fd07f54573ab40455e1a1996e98a4f4971caf8b7e796afe", size = 1281744, upload-time = "2026-10-05T23:24:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d1/99a44430b82998fdef0ffd7d353f64ee5f078c2805ff70f8677ee102cb6c/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2bdf8ce9b72a620cd5ec4dd6b1c1837ff6971489a863851d11d9b0f58dd4062a", size = 1321504, upload-time = "2026-10-05T23:26:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6a/58ef2564d1985a5c1a1dc57906b8363a767094abca180e80a0aca4cb635f/hypothesis-6.168.5-cp315-abi3.abi3t-win32.whl", hash = "sha256:5c3abbef7b17571fd713b0922407d9cd8cbc652254c0f462875f15199fcb29f7", size = 674959, upload-time = "2026-10-05T23:24:36.482Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/153414f55eb0c85bd9d891bd7811d746978c7ad3de81ea79eeb4e62e088b/hypothesis-6.168.5-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:38172199abab94a04bc017613e055faa796d7175fbc6221aac504d406c960b60", size = 681329, upload-time = "2026-10-05T23:26:08.897Z" }, + { url = "https://files.pythonhosted.org/packages/6d/63/117c82f08ab3ba1dcfbf6562ac43b8deb8efa8106646494fadd15122cc1b/hypothesis-6.168.5-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:0600ddc24c32dab5ca8e780630ab6e2561df6d7f594f781d0608b38e04c4da91", size = 679632, upload-time = "2026-10-05T23:24:33.753Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1322,7 +1391,7 @@ wheels = [ [[package]] name = "policyengine-core" -version = "3.31.1" +version = "3.32.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dpath" }, @@ -1342,14 +1411,14 @@ dependencies = [ { name = "standard-imghdr" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/1e/cd58cb6947720a5b7850c06a988abbc0eef53e2be192ed49a17ed62d78d4/policyengine_core-3.31.1.tar.gz", hash = "sha256:3ca8f666c3e67b0c0ce992fa3dc9d1ff2c095a3860f3d719883588b2f9e44ec3", size = 500450, upload-time = "2026-08-28T05:06:43.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/0a/b6c27953e1083d3ab2c40330c49580130fff48a2fe9cbfc8d731e92882e6/policyengine_core-3.32.13.tar.gz", hash = "sha256:a870f7e212fdfa1b5fdad7df92aa2b325fd0280ede16a40aeeceea74d44fb864", size = 457899, upload-time = "2026-10-03T11:30:20.572Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/ce/995992049664b6b4cecd1e4db76b143e07aaee8239043057436f48ccbe75/policyengine_core-3.31.1-py3-none-any.whl", hash = "sha256:308200b28aa26bb79d7f0badebac391c0a81704d45ba4ef48ac79e88f0b21e6b", size = 245719, upload-time = "2026-08-28T05:06:41.769Z" }, + { url = "https://files.pythonhosted.org/packages/d8/01/00ebf4c13152347bc5396b40e80445fa92ea96712b9ec0bfdca7c5d11413/policyengine_core-3.32.13-py3-none-any.whl", hash = "sha256:262543c3ac696207f304f55664711b72e894fbad7b566407afa9d30b16086a41", size = 251251, upload-time = "2026-10-03T11:30:19.171Z" }, ] [[package]] name = "policyengine-uk" -version = "2.93.0" +version = "2.122.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microdf-python" }, @@ -1357,14 +1426,14 @@ dependencies = [ { name = "pydantic" }, { name = "tables" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/04/ab8fbb526c22d5c422eed374244d977b123241baa6f96a604c600876647a/policyengine_uk-2.93.0.tar.gz", hash = "sha256:e3b8eab824e7dad3a1fd61fa8503442a8b1b2285e05fff7ab60f93b6a9b22578", size = 1246786, upload-time = "2026-08-28T14:55:16.266Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/62/4378fd8e493df42fef30c58924dbb0f7ca8111c927302f6e60be862d67d0/policyengine_uk-2.122.2.tar.gz", hash = "sha256:0083793f1d398feef28a64db5508d5a64bcd7834feeade6cde1055d419d505ed", size = 1976779, upload-time = "2026-10-06T02:10:56.786Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/74/5e626f9f80b9485b10bcccbef8d93a635abe309bfdfad437b5de38a80fae/policyengine_uk-2.93.0-py3-none-any.whl", hash = "sha256:4f95feffa3b27fab927f9b903f8a9f1f0d63b2fba209d6b8ac010f0f07a5ec7c", size = 2046525, upload-time = "2026-08-28T14:55:14.114Z" }, + { url = "https://files.pythonhosted.org/packages/63/11/09948c900ca6d58c1050c89b9c93d8f5884244aa6ac95661ef1a2ee9eb9a/policyengine_uk-2.122.2-py3-none-any.whl", hash = "sha256:14cefcedce43ce4a37d659a0ff17ad05695de0dc8e0c5f6e2259a69944cb5558", size = 3118697, upload-time = "2026-10-06T02:10:54.896Z" }, ] [[package]] name = "policyengine-uk-data" -version = "1.56.16" +version = "1.57.4" source = { editable = "." } dependencies = [ { name = "google-auth" }, @@ -1392,6 +1461,7 @@ dependencies = [ dev = [ { name = "build" }, { name = "furo" }, + { name = "hypothesis" }, { name = "itables" }, { name = "l0-python" }, { name = "pytest" }, @@ -1410,6 +1480,7 @@ requires-dist = [ { name = "google-auth" }, { name = "google-cloud-storage" }, { name = "huggingface-hub" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.168.3" }, { name = "itables", marker = "extra == 'dev'" }, { name = "l0-python", marker = "extra == 'dev'", specifier = ">=0.4.0" }, { name = "microcalibrate", specifier = ">=0.18.0" }, @@ -1418,8 +1489,8 @@ requires-dist = [ { name = "openpyxl" }, { name = "pandas" }, { name = "policyengine" }, - { name = "policyengine-core", specifier = ">=3.19.4" }, - { name = "policyengine-uk", specifier = ">=2.93.0" }, + { name = "policyengine-core", specifier = ">=3.32.13" }, + { name = "policyengine-uk", specifier = ">=2.122.0" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pyyaml" },