diff --git a/README.md b/README.md index c1bc8def..288f8832 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,17 @@ files are diagnostic reports for gating and review. Consumer-contract rows must carry canonical constraints explicitly in `universe_constraints`; source-layout `dimensions` are metadata and are not target constraints. +For the UK source-package feed, build the curated UK suite and then a facts-only +consumer artifact: + +```bash +uv run chronicle build-bundle --suite uk --out /tmp/chronicle-uk --replace +uv run chronicle build-consumer-artifact --facts /tmp/chronicle-uk --out /tmp/chronicle-uk-artifact --replace +``` + +`--year` is inert for `--suite uk` because the UK packages are year-pinned. +The US off-year bundle behavior is unchanged and out of scope here. + Builds without an Axiom CLI still pass when the source package is otherwise valid, but `agent_acceptance.json` warns with `concept_alignment_validation_skipped`. For strict agent review, require every diff --git a/chronicle/bundle.py b/chronicle/bundle.py index 7f6368f8..ea749069 100644 --- a/chronicle/bundle.py +++ b/chronicle/bundle.py @@ -20,6 +20,113 @@ BUNDLE_COVERAGE_SCHEMA_VERSION = "ledger.bundle_coverage.v1" BUNDLE_SOURCES_SCHEMA_VERSION = "ledger.bundle_sources.v1" DEFAULT_BUNDLE_SOURCES = tuple(sorted(SOURCE_PACKAGE_ALIASES)) +UK_BUNDLE_SOURCE_PREFIXES = ( + "dft", + "dwp", + "hmrc", + "isc", + "mhclg", + "nisra", + "nrs", + "obr", + "ons", + "scotgov", + "slc", + "voa", + "welshgov", +) +UK_BUNDLE_SOURCES = ( + "dft-nts-vehicle-ownership-2024", + "dwp-benefit-cap-november-2025", + "dwp-benefit-statistics-february-2026", + "dwp-pip-daily-living-foi-2025", + "dwp-uc-deductions-march-2025-february-2026", + "dwp-uc-households-by-constituency-children-may-2025", + "dwp-uc-households-by-constituency-may-2025", + "dwp-uc-households-by-local-authority-may-2025", + "dwp-uc-households-children-may-2025", + "dwp-uc-households-family-type-may-2025", + "dwp-uc-payment-distribution-may-2025", + "dwp-uc-scotland-youngest-child-may-2025", + "dwp-uc-two-child-limit-2025", + "hmrc-cgt-statistics-2025", + "hmrc-salary-sacrifice-reform-2029-headcounts", + "hmrc-salary-sacrifice-relief-2024-25", + "hmrc-spi-income-bands-2023-24", + "hmrc-spi-income-by-area-2023-24", + "hmrc-vat-firm-sector-targets-2024-25", + "hmrc-vat-firm-targets-2024-25", + "isc-annual-census-2023", + "isc-annual-census-2024", + "mhclg-council-tax-levels-england-2026-27", + "mhclg-ehs-weekly-housing-costs-2023-24", + "nisra-census2021-households-lgd", + "nisra-census2021-households-pcon24", + "nisra-census2021-tenure-lgd", + "nisra-pcon24-population-by-age-2024", + "nrs-census2022-households-ukpc24", + "nrs-census2022-uv404-tenure-council-area", + "nrs-pcon24-population-by-age-2024", + "obr-efo-aggregates-march-2026", + "obr-efo-economy-march-2026", + "obr-efo-expenditure-march-2026", + "obr-efo-receipts-march-2026", + "ons-census2021-ts041-households-lad", + "ons-census2021-ts041-households-pcon24", + "ons-census2021-ts054-tenure-lad", + "ons-families-households-2025", + "ons-lad-population-by-age-2024", + "ons-mye-2023-england-regions", + "ons-mye-2023-uk-countries", + "ons-mye-2024-uk", + "ons-national-balance-sheet-land-2025", + "ons-pcon24-population-by-age-2024", + "ons-pipr-private-rent-march-2026", + "ons-pipr-rents-by-area-june-2026", + "ons-public-sector-employment-2026", + "ons-savings-interest-income", + "ons-small-area-income-msoa-fye2023", + "ons-subnational-dwellings-by-tenure-2024", + "ons-uk-business-firm-sector-targets-2025", + "ons-uk-business-firm-targets-2025", + "ons-uk-population-projections-2024", + "scotgov-band-d-council-tax-rates-2026-27", + "scotgov-band-d-equivalents-2025", + "scotgov-council-tax-bands-2025", + "scotgov-scottish-budget-social-security-assistance-2026", + "slc-student-loan-borrower-forecasts-england-2025", + "slc-student-loan-repayments-england-2025", + "slc-student-loan-repayments-northern-ireland-2025", + "slc-student-loan-repayments-scotland-2025", + "slc-student-loan-repayments-wales-2025", + "slc-student-support-england-2025", + "voa-council-tax-bands-2025", + "voa-council-tax-stock-by-lad-2025", + "welshgov-council-tax-levels-2026-27", +) + + +def uk_bundle_sources_from_aliases() -> tuple[str, ...]: + """Return UK-package aliases implied by the source-package directory prefixes.""" + return tuple( + sorted( + alias + for alias, path in SOURCE_PACKAGE_ALIASES.items() + if path.parts and path.parts[0] in UK_BUNDLE_SOURCE_PREFIXES + ) + ) + + +def assert_uk_bundle_sources_match_aliases() -> None: + """Fail loudly if the curated UK suite omits a UK-prefixed alias.""" + expected = uk_bundle_sources_from_aliases() + if UK_BUNDLE_SOURCES != expected: + missing = sorted(set(expected) - set(UK_BUNDLE_SOURCES)) + extra = sorted(set(UK_BUNDLE_SOURCES) - set(expected)) + raise ValueError( + "UK_BUNDLE_SOURCES drifted from UK-prefixed SOURCE_PACKAGE_ALIASES: " + f"missing={missing}, extra={extra}" + ) @dataclass(frozen=True) diff --git a/chronicle/harness.py b/chronicle/harness.py index 3f319d31..d9134b2e 100644 --- a/chronicle/harness.py +++ b/chronicle/harness.py @@ -19,7 +19,12 @@ publish_derived_artifacts, publish_source_artifacts, ) -from chronicle.bundle import BuildBundleReport, build_bundle +from chronicle.bundle import ( + UK_BUNDLE_SOURCES, + BuildBundleReport, + assert_uk_bundle_sources_match_aliases, + build_bundle, +) from chronicle.concepts import ConceptAlignmentReport, validate_concept_alignments from chronicle.consumer_contract import ( ConsumerFactExportReport, @@ -262,12 +267,18 @@ def build_bundle_dir( *, year: int, sources: list[str | Path] | None = None, + suite: str | None = None, axiom_command: list[str] | None = None, axiom_roots: list[str | Path] | None = None, require_axiom_validation: bool = False, replace: bool = False, ) -> BuildBundleReport: """Build a merged Chronicle consumer bundle from source-package suites.""" + if suite == "uk": + if sources: + raise ValueError("--suite uk cannot be combined with --source.") + assert_uk_bundle_sources_match_aliases() + sources = list(UK_BUNDLE_SOURCES) return build_bundle( output_dir, year=year, @@ -707,6 +718,11 @@ def main(argv: list[str] | None = None) -> int: "path. May be repeated. Defaults to available packages for --year." ), ) + bundle_parser.add_argument( + "--suite", + choices=["uk"], + help="Curated source-package suite to build.", + ) bundle_parser.add_argument( "--out", type=Path, @@ -1255,6 +1271,7 @@ def main(argv: list[str] | None = None) -> int: args.out, year=args.year, sources=args.source, + suite=args.suite, axiom_command=axiom_command, axiom_roots=args.axiom_root, require_axiom_validation=args.require_axiom_validation, diff --git a/chronicle/jurisdictions/uk/__init__.py b/chronicle/jurisdictions/uk/__init__.py index 69004986..da5a0162 100644 --- a/chronicle/jurisdictions/uk/__init__.py +++ b/chronicle/jurisdictions/uk/__init__.py @@ -1,9 +1,8 @@ -"""United Kingdom Chronicle target loaders.""" +"""United Kingdom Chronicle target loaders. -from chronicle.targets.loaders import ( - load_hmrc_targets, - load_obr_targets, - load_ons_targets, -) +UK national measures are facts-only source packages. Target selection belongs +to the consumer contract, so this module intentionally exports no legacy DB +target loaders. +""" -__all__ = ["load_hmrc_targets", "load_obr_targets", "load_ons_targets"] +__all__: list[str] = [] diff --git a/chronicle/source_package.py b/chronicle/source_package.py index 605122a8..9382c98a 100644 --- a/chronicle/source_package.py +++ b/chronicle/source_package.py @@ -295,6 +295,8 @@ "voa-council-tax-stock-by-lad-2025": Path("voa/council_tax_stock_by_lad_2025"), "obr-efo-receipts-march-2026": Path("obr/efo_receipts_march_2026"), "obr-efo-expenditure-march-2026": Path("obr/efo_expenditure_march_2026"), + "obr-efo-economy-march-2026": Path("obr/efo_economy_march_2026"), + "obr-efo-aggregates-march-2026": Path("obr/efo_aggregates_march_2026"), "ons-national-balance-sheet-land-2025": Path( "ons/national_balance_sheet_land_2025" ), diff --git a/chronicle/targets/loaders.py b/chronicle/targets/loaders.py index 22db451b..4e2a6a6e 100644 --- a/chronicle/targets/loaders.py +++ b/chronicle/targets/loaders.py @@ -5,10 +5,7 @@ from db.etl_cbo import load_cbo_targets from db.etl_census import load_census_targets from db.etl_cps import load_cps_targets -from db.etl_hmrc import load_hmrc_targets from db.etl_medicaid import load_medicaid_targets -from db.etl_obr import load_obr_targets -from db.etl_ons import load_ons_targets from db.etl_snap import load_snap_targets from db.etl_soi import load_soi_targets from db.etl_soi_credits import load_soi_credits_targets @@ -24,10 +21,7 @@ "load_cbo_targets", "load_census_targets", "load_cps_targets", - "load_hmrc_targets", "load_medicaid_targets", - "load_obr_targets", - "load_ons_targets", "load_snap_targets", "load_soi_targets", "load_soi_credits_targets", diff --git a/db/cli.py b/db/cli.py index 4a82cf64..52257240 100644 --- a/db/cli.py +++ b/db/cli.py @@ -94,13 +94,6 @@ def cmd_load(args): f"Loaded ACA Marketplace targets for years: {years or 'all available'}" ) - if args.source == "hmrc" or args.source == "all": - from .etl_hmrc import load_hmrc_targets - - years = [int(y) for y in args.years.split(",")] if args.years else None - load_hmrc_targets(session, years=years) - print(f"Loaded HMRC targets for years: {years or 'all available'}") - if args.source == "census" or args.source == "all": from .etl_census import load_census_targets @@ -143,20 +136,6 @@ def cmd_load(args): load_cbo_targets(session, years=years) print(f"Loaded CBO projections for years: {years or 'all available'}") - if args.source == "obr" or args.source == "all": - from .etl_obr import load_obr_targets - - years = [int(y) for y in args.years.split(",")] if args.years else None - load_obr_targets(session, years=years) - print(f"Loaded OBR projections for years: {years or 'all available'}") - - if args.source == "ons" or args.source == "all": - from .etl_ons import load_ons_targets - - years = [int(y) for y in args.years.split(",")] if args.years else None - load_ons_targets(session, years=years) - print(f"Loaded ONS projections for years: {years or 'all available'}") - def cmd_stats(args): """Show database statistics.""" @@ -198,13 +177,17 @@ def cmd_load_source_files(args): include_us = args.jurisdiction in {"all", "us"} include_uk = args.jurisdiction in {"all", "uk"} specs = pe_source_specs( - pe_us_root=Path(args.pe_us_root), - pe_uk_root=Path(args.pe_uk_root), + pe_us_root=Path(args.pe_us_root) if args.pe_us_root else None, + pe_uk_root=Path(args.pe_uk_root) if args.pe_uk_root else None, include_us=include_us, include_uk=include_uk, ) if args.limit: specs = specs[: args.limit] + if not specs and not args.limit: + raise ValueError( + "Refusing to prune source artifacts with an empty source-file inventory." + ) with Session(engine) as session: results = ingest_source_artifacts(session, specs) @@ -292,6 +275,17 @@ def cmd_query(args): ) +def _pe_source_root_env_default(jurisdiction: str) -> str | None: + from .pe_source_inventory import ( + PE_UK_DATA_ROOT_ENV, + PE_US_DATA_ROOT_ENV, + _env_value, + ) + + env_var = PE_US_DATA_ROOT_ENV if jurisdiction == "us" else PE_UK_DATA_ROOT_ENV + return _env_value(env_var) + + def main(): parser = argparse.ArgumentParser(description="Manage Chronicle target input data") parser.add_argument("--db", help=f"Database path (default: {DEFAULT_DB_PATH})") @@ -315,15 +309,12 @@ def main(): "snap", "medicaid", "aca", - "hmrc", "census", "ssa", "ssi", "bls", "cps", "cbo", - "obr", - "ons", "all", ], help="Data source to load", @@ -355,12 +346,12 @@ def main(): ) source_parser.add_argument( "--pe-us-root", - default="/Users/maxghenis/PolicyEngine/policyengine-us-data", + default=_pe_source_root_env_default("us"), help="Path to the policyengine-us-data checkout", ) source_parser.add_argument( "--pe-uk-root", - default="/Users/maxghenis/PolicyEngine/policyengine-uk-data", + default=_pe_source_root_env_default("uk"), help="Path to the policyengine-uk-data checkout", ) source_parser.add_argument( diff --git a/db/data/obr/efo_aggregates_march_2026/efo_aggregates.xlsx b/db/data/obr/efo_aggregates_march_2026/efo_aggregates.xlsx new file mode 100644 index 00000000..ebc9c5ca Binary files /dev/null and b/db/data/obr/efo_aggregates_march_2026/efo_aggregates.xlsx differ diff --git a/db/data/obr/efo_aggregates_march_2026/manifest.yaml b/db/data/obr/efo_aggregates_march_2026/manifest.yaml new file mode 100644 index 00000000..43dc8164 --- /dev/null +++ b/db/data/obr/efo_aggregates_march_2026/manifest.yaml @@ -0,0 +1,18 @@ +source_id: obr +package_id: obr-efo-aggregates-march-2026 +dataset: obr_obr-efo-aggregates-march-2026 +source_page: https://obr.uk/efo/economic-and-fiscal-outlook-march-2026/ +table: 'EFO March 2026 fiscal supplementary tables: aggregates' +files: + 2026: + filename: efo_aggregates.xlsx + source_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + sha256: f9eee880e9f39cc8c86d288a803a032b735fea5f9bbd1532f26bfca898a669d9 + size_bytes: 231008 + fetched_at: '2026-08-19T00:00:00+00:00' + storage: + r2: + provider: r2 + bucket: ledger-raw + key: raw/obr/obr-efo-aggregates-march-2026/2026/f9eee880e9f39cc8c86d288a803a032b735fea5f9bbd1532f26bfca898a669d9/efo_aggregates.xlsx + uri: r2://ledger-raw/raw/obr/obr-efo-aggregates-march-2026/2026/f9eee880e9f39cc8c86d288a803a032b735fea5f9bbd1532f26bfca898a669d9/efo_aggregates.xlsx diff --git a/db/data/obr/efo_economy_march_2026/efo_economy.xlsx b/db/data/obr/efo_economy_march_2026/efo_economy.xlsx new file mode 100644 index 00000000..4b69c853 Binary files /dev/null and b/db/data/obr/efo_economy_march_2026/efo_economy.xlsx differ diff --git a/db/data/obr/efo_economy_march_2026/manifest.yaml b/db/data/obr/efo_economy_march_2026/manifest.yaml new file mode 100644 index 00000000..11010eb5 --- /dev/null +++ b/db/data/obr/efo_economy_march_2026/manifest.yaml @@ -0,0 +1,18 @@ +source_id: obr +package_id: obr-efo-economy-march-2026 +dataset: obr_obr-efo-economy-march-2026 +source_page: https://obr.uk/efo/economic-and-fiscal-outlook-march-2026/ +table: 'EFO March 2026 detailed forecast tables: economy' +files: + 2026: + filename: efo_economy.xlsx + source_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + sha256: fde1de4bc5424dcf16c7b5c42eb75a35198e347f54db47690e95ede7541db510 + size_bytes: 483251 + fetched_at: '2026-08-19T00:00:00+00:00' + storage: + r2: + provider: r2 + bucket: ledger-raw + key: raw/obr/obr-efo-economy-march-2026/2026/fde1de4bc5424dcf16c7b5c42eb75a35198e347f54db47690e95ede7541db510/efo_economy.xlsx + uri: r2://ledger-raw/raw/obr/obr-efo-economy-march-2026/2026/fde1de4bc5424dcf16c7b5c42eb75a35198e347f54db47690e95ede7541db510/efo_economy.xlsx diff --git a/db/data/ons/families_households_2025/manifest.yaml b/db/data/ons/families_households_2025/manifest.yaml index f66420ad..7d662b55 100644 --- a/db/data/ons/families_households_2025/manifest.yaml +++ b/db/data/ons/families_households_2025/manifest.yaml @@ -2,7 +2,7 @@ source_id: ons package_id: ons-families-households-2025 dataset: ons_ons-families-households-2025 source_page: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds -table: 'Families and households in the UK 2025, Table 7: households by type' +table: 'Families and households in the UK 2025, Tables 5 and 7: household size and households by type' files: 2025: filename: familiesandhouseholdsuk2025.xlsx diff --git a/db/etl_hmrc.py b/db/etl_hmrc.py deleted file mode 100644 index 720179de..00000000 --- a/db/etl_hmrc.py +++ /dev/null @@ -1,349 +0,0 @@ -""" -ETL for UK HMRC (HM Revenue & Customs) targets. - -Loads data from HMRC statistics into the targets database. -Data sources: -- https://www.gov.uk/government/statistics/income-tax-liabilities-statistics -- https://www.gov.uk/government/collections/national-insurance-contributions-statistics -""" - -from __future__ import annotations - -from sqlmodel import Session, select - -from .schema import ( - DataSource, - Jurisdiction, - Stratum, - StratumConstraint, - Target, - TargetType, - init_db, -) - -# HMRC data by tax year -# Source: HMRC statistics publications -# Values in GBP (pounds sterling) -HMRC_DATA = { - 2022: { - # Tax revenue (billions GBP converted to GBP) - "income_tax": 248_800_000_000, # £248.8bn - "national_insurance": 176_500_000_000, # £176.5bn - "capital_gains_tax": 18_200_000_000, # £18.2bn - "inheritance_tax": 7_100_000_000, # £7.1bn - # Taxpayer counts - "taxpayers": 34_100_000, - "higher_rate_taxpayers": 6_100_000, - "additional_rate_taxpayers": 629_000, - # Total income - "total_income": 1_534_000_000_000, # £1.534tn - # Benefits (DWP data, included here for convenience) - "benefits": { - "universal_credit": { - "recipients": 6_200_000, - "expenditure": 52_300_000_000, # £52.3bn - }, - "child_benefit": { - "recipients": 7_100_000, # families - "expenditure": 12_100_000_000, - }, - "state_pension": { - "recipients": 12_700_000, - "expenditure": 124_300_000_000, - }, - "housing_benefit": { - "recipients": 2_800_000, - "expenditure": 18_400_000_000, - }, - "pension_credit": { - "recipients": 1_400_000, - "expenditure": 5_600_000_000, - }, - }, - }, - 2021: { - "income_tax": 224_700_000_000, - "national_insurance": 157_800_000_000, - "capital_gains_tax": 14_900_000_000, - "inheritance_tax": 6_100_000_000, - "taxpayers": 32_400_000, - "higher_rate_taxpayers": 5_500_000, - "additional_rate_taxpayers": 538_000, - "total_income": 1_423_000_000_000, - "benefits": { - "universal_credit": { - "recipients": 5_800_000, - "expenditure": 44_700_000_000, - }, - "child_benefit": { - "recipients": 7_200_000, - "expenditure": 11_900_000_000, - }, - "state_pension": { - "recipients": 12_500_000, - "expenditure": 105_500_000_000, - }, - "housing_benefit": { - "recipients": 3_100_000, - "expenditure": 20_100_000_000, - }, - "pension_credit": { - "recipients": 1_500_000, - "expenditure": 5_400_000_000, - }, - }, - }, -} - -SOURCE_URL_TAX = ( - "https://www.gov.uk/government/statistics/income-tax-liabilities-statistics" -) -SOURCE_URL_DWP = "https://www.gov.uk/government/collections/dwp-benefits-statistics" - - -def get_or_create_stratum( - session: Session, - name: str, - jurisdiction: Jurisdiction, - constraints: list[tuple[str, str, str]], - description: str | None = None, - parent_id: int | None = None, - stratum_group_id: str | None = None, -) -> Stratum: - """Get existing stratum or create new one.""" - definition_hash = Stratum.compute_hash(constraints, jurisdiction) - - existing = session.exec( - select(Stratum).where(Stratum.definition_hash == definition_hash) - ).first() - - if existing: - return existing - - stratum = Stratum( - name=name, - description=description, - jurisdiction=jurisdiction, - definition_hash=definition_hash, - parent_id=parent_id, - stratum_group_id=stratum_group_id, - ) - session.add(stratum) - session.flush() - - for variable, operator, value in constraints: - constraint = StratumConstraint( - stratum_id=stratum.id, - variable=variable, - operator=operator, - value=value, - ) - session.add(constraint) - - return stratum - - -def load_hmrc_targets(session: Session, years: list[int] | None = None): - """ - Load HMRC targets into database. - - Args: - session: Database session - years: Years to load (default: all available) - """ - if years is None: - years = list(HMRC_DATA.keys()) - - for year in years: - if year not in HMRC_DATA: - continue - - data = HMRC_DATA[year] - - # Create national taxpayer stratum - national_stratum = get_or_create_stratum( - session, - name="UK All Taxpayers", - jurisdiction=Jurisdiction.UK, - constraints=[ - ("is_taxpayer", "==", "1") - ], # Taxpayers only, not whole population - description="All UK taxpayers", - stratum_group_id="uk_national", - ) - - # Tax revenue targets - session.add( - Target( - stratum_id=national_stratum.id, - variable="income_tax", - period=year, - value=data["income_tax"], - target_type=TargetType.AMOUNT, - source=DataSource.HMRC, - source_url=SOURCE_URL_TAX, - ) - ) - - session.add( - Target( - stratum_id=national_stratum.id, - variable="national_insurance", - period=year, - value=data["national_insurance"], - target_type=TargetType.AMOUNT, - source=DataSource.HMRC, - source_url=SOURCE_URL_TAX, - ) - ) - - session.add( - Target( - stratum_id=national_stratum.id, - variable="capital_gains_tax", - period=year, - value=data["capital_gains_tax"], - target_type=TargetType.AMOUNT, - source=DataSource.HMRC, - source_url=SOURCE_URL_TAX, - ) - ) - - session.add( - Target( - stratum_id=national_stratum.id, - variable="inheritance_tax", - period=year, - value=data["inheritance_tax"], - target_type=TargetType.AMOUNT, - source=DataSource.HMRC, - source_url=SOURCE_URL_TAX, - ) - ) - - # Taxpayer counts - session.add( - Target( - stratum_id=national_stratum.id, - variable="taxpayer_count", - period=year, - value=data["taxpayers"], - target_type=TargetType.COUNT, - source=DataSource.HMRC, - source_url=SOURCE_URL_TAX, - ) - ) - - session.add( - Target( - stratum_id=national_stratum.id, - variable="total_income", - period=year, - value=data["total_income"], - target_type=TargetType.AMOUNT, - source=DataSource.HMRC, - source_url=SOURCE_URL_TAX, - ) - ) - - # Higher rate taxpayers stratum - higher_rate_stratum = get_or_create_stratum( - session, - name="UK Higher Rate Taxpayers", - jurisdiction=Jurisdiction.UK, - constraints=[("income_tax_band", "==", "higher")], - description="UK taxpayers in higher rate band", - parent_id=national_stratum.id, - stratum_group_id="uk_tax_bands", - ) - - session.add( - Target( - stratum_id=higher_rate_stratum.id, - variable="taxpayer_count", - period=year, - value=data["higher_rate_taxpayers"], - target_type=TargetType.COUNT, - source=DataSource.HMRC, - source_url=SOURCE_URL_TAX, - ) - ) - - # Additional rate taxpayers stratum - additional_rate_stratum = get_or_create_stratum( - session, - name="UK Additional Rate Taxpayers", - jurisdiction=Jurisdiction.UK, - constraints=[("income_tax_band", "==", "additional")], - description="UK taxpayers in additional rate band", - parent_id=national_stratum.id, - stratum_group_id="uk_tax_bands", - ) - - session.add( - Target( - stratum_id=additional_rate_stratum.id, - variable="taxpayer_count", - period=year, - value=data["additional_rate_taxpayers"], - target_type=TargetType.COUNT, - source=DataSource.HMRC, - source_url=SOURCE_URL_TAX, - ) - ) - - # Benefit targets - for benefit_name, benefit_data in data.get("benefits", {}).items(): - benefit_display = benefit_name.replace("_", " ").title() - - benefit_stratum = get_or_create_stratum( - session, - name=f"UK {benefit_display} Recipients", - jurisdiction=Jurisdiction.UK, - constraints=[(benefit_name, "==", "1")], - description=f"UK {benefit_display} recipients", - stratum_group_id="uk_benefits", - ) - - session.add( - Target( - stratum_id=benefit_stratum.id, - variable=f"{benefit_name}_recipients", - period=year, - value=benefit_data["recipients"], - target_type=TargetType.COUNT, - source=DataSource.DWP, - source_url=SOURCE_URL_DWP, - ) - ) - - session.add( - Target( - stratum_id=benefit_stratum.id, - variable=f"{benefit_name}_expenditure", - period=year, - value=benefit_data["expenditure"], - target_type=TargetType.AMOUNT, - source=DataSource.DWP, - source_url=SOURCE_URL_DWP, - ) - ) - - session.commit() - - -def run_etl(db_path=None): - """Run the HMRC ETL pipeline.""" - from pathlib import Path - from .schema import DEFAULT_DB_PATH - - path = Path(db_path) if db_path else DEFAULT_DB_PATH - engine = init_db(path) - - with Session(engine) as session: - load_hmrc_targets(session) - print(f"Loaded HMRC targets to {path}") - - -if __name__ == "__main__": - run_etl() diff --git a/db/etl_obr.py b/db/etl_obr.py deleted file mode 100644 index 2ed11875..00000000 --- a/db/etl_obr.py +++ /dev/null @@ -1,313 +0,0 @@ -""" -ETL for OBR (Office for Budget Responsibility) projections. - -Loads UK fiscal and economic projections from OBR's Economic and Fiscal Outlook. -Data source: https://obr.uk/efo/economic-and-fiscal-outlook-march-2024/ -""" - -from __future__ import annotations - -from sqlmodel import Session, select - -from .schema import ( - DataSource, - Jurisdiction, - Stratum, - StratumConstraint, - Target, - TargetType, - init_db, -) - -# OBR projections data (5-year forecast, 2024-2029) -# Source: OBR Economic and Fiscal Outlook, March 2024 -# https://obr.uk/efo/economic-and-fiscal-outlook-march-2024/ -# GDP and budget figures in billions of GBP -# Rates as percentages -OBR_DATA = { - # Fiscal Year 2024-25 - 2024: { - # GDP and public finances (billions GBP) - "gdp": 2_791_000_000_000, # £2.791 trillion - "total_receipts": 1_139_000_000_000, - "total_managed_expenditure": 1_226_000_000_000, - "public_sector_net_borrowing": 87_000_000_000, - "public_sector_net_debt": 2_654_000_000_000, - # Economic indicators (percentages) - "real_gdp_growth": 0.8, - "unemployment_rate": 4.2, - "cpi_inflation": 2.8, - "rpi_inflation": 3.9, - "bank_rate": 4.8, - # Employment (thousands) - "employment": 33_100_000, - }, - 2025: { - "gdp": 2_929_000_000_000, - "total_receipts": 1_207_000_000_000, - "total_managed_expenditure": 1_296_000_000_000, - "public_sector_net_borrowing": 89_000_000_000, - "public_sector_net_debt": 2_783_000_000_000, - "real_gdp_growth": 1.9, - "unemployment_rate": 4.3, - "cpi_inflation": 2.2, - "rpi_inflation": 3.2, - "bank_rate": 4.1, - "employment": 33_200_000, - }, - 2026: { - "gdp": 3_066_000_000_000, - "total_receipts": 1_273_000_000_000, - "total_managed_expenditure": 1_349_000_000_000, - "public_sector_net_borrowing": 76_000_000_000, - "public_sector_net_debt": 2_893_000_000_000, - "real_gdp_growth": 2.0, - "unemployment_rate": 4.1, - "cpi_inflation": 1.8, - "rpi_inflation": 2.8, - "bank_rate": 3.6, - "employment": 33_400_000, - }, - 2027: { - "gdp": 3_199_000_000_000, - "total_receipts": 1_337_000_000_000, - "total_managed_expenditure": 1_399_000_000_000, - "public_sector_net_borrowing": 62_000_000_000, - "public_sector_net_debt": 2_985_000_000_000, - "real_gdp_growth": 1.8, - "unemployment_rate": 4.0, - "cpi_inflation": 1.8, - "rpi_inflation": 2.8, - "bank_rate": 3.4, - "employment": 33_500_000, - }, - 2028: { - "gdp": 3_331_000_000_000, - "total_receipts": 1_399_000_000_000, - "total_managed_expenditure": 1_456_000_000_000, - "public_sector_net_borrowing": 57_000_000_000, - "public_sector_net_debt": 3_070_000_000_000, - "real_gdp_growth": 1.7, - "unemployment_rate": 4.0, - "cpi_inflation": 2.0, - "rpi_inflation": 3.0, - "bank_rate": 3.3, - "employment": 33_600_000, - }, - 2029: { - "gdp": 3_464_000_000_000, - "total_receipts": 1_459_000_000_000, - "total_managed_expenditure": 1_512_000_000_000, - "public_sector_net_borrowing": 53_000_000_000, - "public_sector_net_debt": 3_148_000_000_000, - "real_gdp_growth": 1.7, - "unemployment_rate": 4.0, - "cpi_inflation": 2.0, - "rpi_inflation": 3.0, - "bank_rate": 3.3, - "employment": 33_700_000, - }, -} - -SOURCE_URL = "https://obr.uk/efo/economic-and-fiscal-outlook-march-2024/" - - -def get_or_create_stratum( - session: Session, - name: str, - jurisdiction: Jurisdiction, - constraints: list[tuple[str, str, str]], - description: str | None = None, - parent_id: int | None = None, - stratum_group_id: str | None = None, -) -> Stratum: - """Get existing stratum or create new one.""" - definition_hash = Stratum.compute_hash(constraints, jurisdiction) - - existing = session.exec( - select(Stratum).where(Stratum.definition_hash == definition_hash) - ).first() - - if existing: - return existing - - stratum = Stratum( - name=name, - description=description, - jurisdiction=jurisdiction, - definition_hash=definition_hash, - parent_id=parent_id, - stratum_group_id=stratum_group_id, - ) - session.add(stratum) - session.flush() - - for variable, operator, value in constraints: - constraint = StratumConstraint( - stratum_id=stratum.id, - variable=variable, - operator=operator, - value=value, - ) - session.add(constraint) - - return stratum - - -def load_obr_targets(session: Session, years: list[int] | None = None): - """ - Load OBR projections into database. - - Args: - session: Database session - years: Years to load (default: all available 2024-2029) - """ - if years is None: - years = list(OBR_DATA.keys()) - - for year in years: - if year not in OBR_DATA: - continue - - data = OBR_DATA[year] - - # UK public finances stratum - budget_stratum = get_or_create_stratum( - session, - name="UK Public Finances", - jurisdiction=Jurisdiction.UK, - constraints=[("sector", "==", "public")], # Government sector - description="UK public sector finances", - stratum_group_id="obr_budget", - ) - - # Budget targets - budget_vars = [ - ("gdp", TargetType.AMOUNT), - ("total_receipts", TargetType.AMOUNT), - ("total_managed_expenditure", TargetType.AMOUNT), - ("public_sector_net_borrowing", TargetType.AMOUNT), - ("public_sector_net_debt", TargetType.AMOUNT), - ] - - for var_name, var_type in budget_vars: - if var_name not in data: - continue - - # Check for existing target - existing = session.exec( - select(Target).where( - Target.stratum_id == budget_stratum.id, - Target.variable == var_name, - Target.period == year, - Target.source == DataSource.OBR, - ) - ).first() - - if existing: - continue - - session.add( - Target( - stratum_id=budget_stratum.id, - variable=var_name, - period=year, - value=data[var_name], - target_type=var_type, - source=DataSource.OBR, - source_url=SOURCE_URL, - is_preliminary=year > 2024, - ) - ) - - # UK Economy stratum for macro indicators - economy_stratum = get_or_create_stratum( - session, - name="UK Economy", - jurisdiction=Jurisdiction.UK, - constraints=[], # Whole economy, not a subset - description="UK macroeconomic indicators", - stratum_group_id="obr_economy", - ) - - # Economic rate targets - rate_vars = [ - "real_gdp_growth", - "unemployment_rate", - "cpi_inflation", - "rpi_inflation", - "bank_rate", - ] - - for var_name in rate_vars: - if var_name not in data: - continue - - existing = session.exec( - select(Target).where( - Target.stratum_id == economy_stratum.id, - Target.variable == var_name, - Target.period == year, - Target.source == DataSource.OBR, - ) - ).first() - - if existing: - continue - - session.add( - Target( - stratum_id=economy_stratum.id, - variable=var_name, - period=year, - value=data[var_name], - target_type=TargetType.RATE, - source=DataSource.OBR, - source_url=SOURCE_URL, - is_preliminary=year > 2024, - ) - ) - - # Employment count - if "employment" in data: - existing = session.exec( - select(Target).where( - Target.stratum_id == economy_stratum.id, - Target.variable == "employment", - Target.period == year, - Target.source == DataSource.OBR, - ) - ).first() - - if not existing: - session.add( - Target( - stratum_id=economy_stratum.id, - variable="employment", - period=year, - value=data["employment"], - target_type=TargetType.COUNT, - source=DataSource.OBR, - source_url=SOURCE_URL, - is_preliminary=year > 2024, - ) - ) - - session.commit() - - -def run_etl(db_path=None): - """Run the OBR ETL pipeline.""" - from pathlib import Path - from .schema import DEFAULT_DB_PATH - - path = Path(db_path) if db_path else DEFAULT_DB_PATH - engine = init_db(path) - - with Session(engine) as session: - load_obr_targets(session) - print(f"Loaded OBR projections to {path}") - - -if __name__ == "__main__": - run_etl() diff --git a/db/etl_ons.py b/db/etl_ons.py deleted file mode 100644 index d4619231..00000000 --- a/db/etl_ons.py +++ /dev/null @@ -1,310 +0,0 @@ -""" -ETL for ONS (Office for National Statistics) projections. - -Loads UK population and household projections from ONS. -Data sources: -- National population projections: https://www.ons.gov.uk/peoplepopulationandcommunity/populationandmigration/populationprojections -- Household projections: https://www.ons.gov.uk/peoplepopulationandcommunity/populationandmigration/populationprojections/datasets/householdprojectionsforengland -""" - -from __future__ import annotations - -from sqlmodel import Session, select - -from .schema import ( - DataSource, - Jurisdiction, - Stratum, - StratumConstraint, - Target, - TargetType, - init_db, -) - -# ONS 2022-based projections data (2024-2034) -# Source: ONS National Population Projections (2022-based) -# https://www.ons.gov.uk/peoplepopulationandcommunity/populationandmigration/populationprojections -# Population counts are for the UK as a whole -# Household projections based on ONS household projections data -ONS_DATA = { - # Mid-2024 projections - 2024: { - # Population by broad age groups (counts) - "population_total": 68_265_000, - "population_age_0_15": 12_410_000, # Children - "population_age_16_64": 42_638_000, # Working age - "population_age_65_plus": 13_217_000, # Pension age - # Household projections - "households_total": 28_500_000, - "average_household_size": 2.40, - }, - 2025: { - "population_total": 68_626_000, - "population_age_0_15": 12_448_000, - "population_age_16_64": 42_703_000, - "population_age_65_plus": 13_475_000, - "households_total": 28_750_000, - "average_household_size": 2.39, - }, - 2026: { - "population_total": 68_978_000, - "population_age_0_15": 12_485_000, - "population_age_16_64": 42_759_000, - "population_age_65_plus": 13_734_000, - "households_total": 29_000_000, - "average_household_size": 2.38, - }, - 2027: { - "population_total": 69_321_000, - "population_age_0_15": 12_521_000, - "population_age_16_64": 42_808_000, - "population_age_65_plus": 13_992_000, - "households_total": 29_250_000, - "average_household_size": 2.37, - }, - 2028: { - "population_total": 69_656_000, - "population_age_0_15": 12_555_000, - "population_age_16_64": 42_852_000, - "population_age_65_plus": 14_249_000, - "households_total": 29_500_000, - "average_household_size": 2.36, - }, - 2029: { - "population_total": 69_984_000, - "population_age_0_15": 12_588_000, - "population_age_16_64": 42_891_000, - "population_age_65_plus": 14_505_000, - "households_total": 29_750_000, - "average_household_size": 2.35, - }, - 2030: { - "population_total": 70_306_000, - "population_age_0_15": 12_619_000, - "population_age_16_64": 42_927_000, - "population_age_65_plus": 14_760_000, - "households_total": 30_000_000, - "average_household_size": 2.34, - }, - 2031: { - "population_total": 70_622_000, - "population_age_0_15": 12_649_000, - "population_age_16_64": 42_960_000, - "population_age_65_plus": 15_013_000, - "households_total": 30_250_000, - "average_household_size": 2.34, - }, - 2032: { - "population_total": 70_933_000, - "population_age_0_15": 12_678_000, - "population_age_16_64": 42_991_000, - "population_age_65_plus": 15_264_000, - "households_total": 30_500_000, - "average_household_size": 2.33, - }, - 2033: { - "population_total": 71_240_000, - "population_age_0_15": 12_706_000, - "population_age_16_64": 43_020_000, - "population_age_65_plus": 15_514_000, - "households_total": 30_750_000, - "average_household_size": 2.32, - }, - 2034: { - "population_total": 71_543_000, - "population_age_0_15": 12_733_000, - "population_age_16_64": 43_047_000, - "population_age_65_plus": 15_763_000, - "households_total": 31_000_000, - "average_household_size": 2.31, - }, -} - -SOURCE_URL = "https://www.ons.gov.uk/peoplepopulationandcommunity/populationandmigration/populationprojections" - - -def get_or_create_stratum( - session: Session, - name: str, - jurisdiction: Jurisdiction, - constraints: list[tuple[str, str, str]], - description: str | None = None, - parent_id: int | None = None, - stratum_group_id: str | None = None, -) -> Stratum: - """Get existing stratum or create new one.""" - definition_hash = Stratum.compute_hash(constraints, jurisdiction) - - existing = session.exec( - select(Stratum).where(Stratum.definition_hash == definition_hash) - ).first() - - if existing: - return existing - - stratum = Stratum( - name=name, - description=description, - jurisdiction=jurisdiction, - definition_hash=definition_hash, - parent_id=parent_id, - stratum_group_id=stratum_group_id, - ) - session.add(stratum) - session.flush() - - for variable, operator, value in constraints: - constraint = StratumConstraint( - stratum_id=stratum.id, - variable=variable, - operator=operator, - value=value, - ) - session.add(constraint) - - return stratum - - -def load_ons_targets(session: Session, years: list[int] | None = None): - """ - Load ONS population and household projections into database. - - Args: - session: Database session - years: Years to load (default: all available 2024-2034) - """ - if years is None: - years = list(ONS_DATA.keys()) - - for year in years: - if year not in ONS_DATA: - continue - - data = ONS_DATA[year] - - # UK population stratum - population_stratum = get_or_create_stratum( - session, - name="UK Population", - jurisdiction=Jurisdiction.UK, - constraints=[("entity_type", "==", "person")], # Person-level data - description="UK total population and age group breakdowns", - stratum_group_id="ons_population", - ) - - # Population count targets - population_vars = [ - ("population_total", TargetType.COUNT), - ("population_age_0_15", TargetType.COUNT), - ("population_age_16_64", TargetType.COUNT), - ("population_age_65_plus", TargetType.COUNT), - ] - - for var_name, var_type in population_vars: - if var_name not in data: - continue - - # Check for existing target - existing = session.exec( - select(Target).where( - Target.stratum_id == population_stratum.id, - Target.variable == var_name, - Target.period == year, - Target.source == DataSource.ONS, - ) - ).first() - - if existing: - continue - - session.add( - Target( - stratum_id=population_stratum.id, - variable=var_name, - period=year, - value=data[var_name], - target_type=var_type, - source=DataSource.ONS, - source_url=SOURCE_URL, - is_preliminary=True, # All projections are preliminary - ) - ) - - # UK households stratum - households_stratum = get_or_create_stratum( - session, - name="UK Households", - jurisdiction=Jurisdiction.UK, - constraints=[("entity_type", "==", "household")], # Household-level data - description="UK household projections", - stratum_group_id="ons_households", - ) - - # Household count targets - if "households_total" in data: - existing = session.exec( - select(Target).where( - Target.stratum_id == households_stratum.id, - Target.variable == "households_total", - Target.period == year, - Target.source == DataSource.ONS, - ) - ).first() - - if not existing: - session.add( - Target( - stratum_id=households_stratum.id, - variable="households_total", - period=year, - value=data["households_total"], - target_type=TargetType.COUNT, - source=DataSource.ONS, - source_url=SOURCE_URL, - is_preliminary=True, - ) - ) - - # Average household size (rate) - if "average_household_size" in data: - existing = session.exec( - select(Target).where( - Target.stratum_id == households_stratum.id, - Target.variable == "average_household_size", - Target.period == year, - Target.source == DataSource.ONS, - ) - ).first() - - if not existing: - session.add( - Target( - stratum_id=households_stratum.id, - variable="average_household_size", - period=year, - value=data["average_household_size"], - target_type=TargetType.RATE, - source=DataSource.ONS, - source_url=SOURCE_URL, - is_preliminary=True, - ) - ) - - session.commit() - - -def run_etl(db_path=None): - """Run the ONS ETL pipeline.""" - from pathlib import Path - from .schema import DEFAULT_DB_PATH - - path = Path(db_path) if db_path else DEFAULT_DB_PATH - engine = init_db(path) - - with Session(engine) as session: - load_ons_targets(session) - print(f"Loaded ONS projections to {path}") - - -if __name__ == "__main__": - run_etl() diff --git a/db/pe_source_inventory.py b/db/pe_source_inventory.py index e40bafdb..ceac5bdd 100644 --- a/db/pe_source_inventory.py +++ b/db/pe_source_inventory.py @@ -2,13 +2,14 @@ from __future__ import annotations +import os from pathlib import Path from .schema import Jurisdiction from .source_files import SourceArtifactSpec, make_slug, make_url_slug -DEFAULT_PE_US_ROOT = Path("/Users/maxghenis/PolicyEngine/policyengine-us-data") -DEFAULT_PE_UK_ROOT = Path("/Users/maxghenis/PolicyEngine/policyengine-uk-data") +PE_US_DATA_ROOT_ENV = "LEDGER_PE_US_DATA_ROOT" +PE_UK_DATA_ROOT_ENV = "LEDGER_PE_UK_DATA_ROOT" SOURCE_SUFFIXES = { ".csv", @@ -270,6 +271,29 @@ ] +def _env_value(*names: str) -> str | None: + for name in names: + value = os.environ.get(name) + if value: + return value + return None + + +def _resolve_required_root( + root: Path | None, + *, + flag: str, + env_var: str, +) -> Path: + value = root if root is not None else _env_value(env_var) + if value is None: + raise ValueError(f"{flag} or {env_var} is required.") + path = Path(value).expanduser() + if not path.exists(): + raise ValueError(f"{flag} / {env_var} path does not exist: {path}") + return path + + def _is_source_file(path: Path) -> bool: if path.name.startswith("~$"): return False @@ -282,12 +306,8 @@ def _infer_us_source_id(path: Path) -> str: name = path.name.lower() if "irs_soi" in name or "soi" in name or "agi" in name or "eitc" in name: return "irs-soi" - if ( - name.startswith("acs") - or name.startswith("census") - or name.startswith("age_") - or name.startswith("population_") - or "real_estate_taxes" in name + if name.startswith(("acs", "census", "age_", "population_")) or ( + "real_estate_taxes" in name ): return "census" if "snap" in name: @@ -383,9 +403,14 @@ def _url_spec( def pe_us_source_specs( - pe_us_root: Path = DEFAULT_PE_US_ROOT, + pe_us_root: Path | None = None, ) -> list[SourceArtifactSpec]: """Return public PE-US target source files available in a local checkout.""" + pe_us_root = _resolve_required_root( + pe_us_root, + flag="--pe-us-root", + env_var=PE_US_DATA_ROOT_ENV, + ) root = pe_us_root / "policyengine_us_data" / "storage" specs: list[SourceArtifactSpec] = [] @@ -505,9 +530,14 @@ def pe_us_source_specs( def pe_uk_source_specs( - pe_uk_root: Path = DEFAULT_PE_UK_ROOT, + pe_uk_root: Path | None = None, ) -> list[SourceArtifactSpec]: """Return public PE-UK target source files available in a local checkout.""" + pe_uk_root = _resolve_required_root( + pe_uk_root, + flag="--pe-uk-root", + env_var=PE_UK_DATA_ROOT_ENV, + ) storage = pe_uk_root / "policyengine_uk_data" / "storage" specs: list[SourceArtifactSpec] = [] @@ -574,15 +604,17 @@ def pe_uk_source_specs( def pe_source_specs( - pe_us_root: Path = DEFAULT_PE_US_ROOT, - pe_uk_root: Path = DEFAULT_PE_UK_ROOT, + pe_us_root: Path | None = None, + pe_uk_root: Path | None = None, include_us: bool = True, include_uk: bool = True, ) -> list[SourceArtifactSpec]: """Return source files used by the PE-US and PE-UK calibration pipelines.""" specs: list[SourceArtifactSpec] = [] - if include_us and pe_us_root.exists(): + us_configured = pe_us_root is not None or _env_value(PE_US_DATA_ROOT_ENV) + uk_configured = pe_uk_root is not None or _env_value(PE_UK_DATA_ROOT_ENV) + if include_us and (us_configured or not include_uk or not uk_configured): specs.extend(pe_us_source_specs(pe_us_root)) - if include_uk and pe_uk_root.exists(): + if include_uk and (uk_configured or not include_us or not us_configured): specs.extend(pe_uk_source_specs(pe_uk_root)) return specs diff --git a/docs/agent-source-package-harness.md b/docs/agent-source-package-harness.md index f8dacc44..d4f863b4 100644 --- a/docs/agent-source-package-harness.md +++ b/docs/agent-source-package-harness.md @@ -501,6 +501,17 @@ The bundle emits a root `consumer_facts.jsonl`, `source_packages.json`, `coverage.json`, and `reports/build_bundle.json`, while preserving each source-package suite under `sources//`. +For the UK source-package feed, use the curated UK suite and build a facts-only +consumer artifact: + +```bash +uv run chronicle build-bundle --suite uk --out /tmp/chronicle-uk --replace +uv run chronicle build-consumer-artifact --facts /tmp/chronicle-uk --out /tmp/chronicle-uk-artifact --replace +``` + +`--year` is inert for `--suite uk` because the UK packages are year-pinned. +The US off-year bundle behavior is unchanged and out of scope here. + The first agent-facing gate is now `/tmp/chronicle-suite/reports/agent_acceptance.json`; it summarizes whether raw artifacts have R2 pointers, the full source document was parsed, facts have diff --git a/docs/pe-uk-source-checklist.md b/docs/pe-uk-source-checklist.md index d5bd6b67..526c280f 100644 --- a/docs/pe-uk-source-checklist.md +++ b/docs/pe-uk-source-checklist.md @@ -95,9 +95,54 @@ cannot be obtained. Nothing in uk-data is absent from this table. ## Legacy ETL cleanup -The pre-package `db/etl_obr.py` / `db/etl_ons.py` dicts are superseded by these -packages; cleanup tracked in -[#135](https://github.com/PolicyEngine/chronicle/issues/135). +| ETL series | Chronicle package concept | new package | gap | +|---|---|---|---| +| `db/etl_obr.py` `gdp` (2024-2029) | `obr.nominal_gdp_nsa` (calendar-year nominal GDP, GBP) | `obr-efo-economy-march-2026` | None for 2024-2029. Source table 1.4 publishes calendar-year nominal GDP in GBP billion; Chronicle stores the published level with `value_scale`. | +| `db/etl_obr.py` `total_receipts` (2024-2029) | `obr.public_sector_current_receipts` (fiscal-year public sector current receipts, GBP) | `obr-efo-aggregates-march-2026` | FY2024-25 remains a gap in the staged aggregates workbook: sheet 6.2 starts the public-sector current-receipts row at FY2025-26. FY2025-26 through FY2029-30 are ported. | +| `db/etl_obr.py` `total_managed_expenditure` (2024-2029) | `obr.total_managed_expenditure` (fiscal-year TME, GBP) | `obr-efo-aggregates-march-2026` | FY2024-25 remains a gap in the staged aggregates workbook: sheet 6.1 starts the TME row at FY2025-26. FY2025-26 through FY2029-30 are ported. | +| `db/etl_obr.py` `public_sector_net_borrowing` (2024-2029) | `obr.public_sector_net_borrowing` (fiscal-year net borrowing, GBP) | `obr-efo-aggregates-march-2026` | FY2024-25 remains a gap in the staged aggregates workbook: sheet 6.5 starts the net-borrowing row at FY2025-26. FY2025-26 through FY2029-30 are ported. | +| `db/etl_obr.py` `public_sector_net_debt` (2024-2029) | `obr.public_sector_net_debt_percent_gdp` (fiscal-year PSND, percent of GDP) | `obr-efo-aggregates-march-2026` | The staged aggregates workbook publishes PSND directly as percent of GDP in sheet 6.13, not as a GBP amount. The old ETL's GBP amount remains a gap because converting percent of GDP to GBP would be Chronicle-computed. | +| `db/etl_obr.py` `real_gdp_growth` (2024-2029) | — | — | Gap: the staged economy workbook publishes real GDP levels in sheet 1.1; annual real GDP growth would be derived from those levels and is therefore not a Chronicle fact. | +| `db/etl_obr.py` `unemployment_rate` (2024-2029) | `obr.ilo_unemployment_rate_16_plus` | `obr-efo-economy-march-2026` | None for 2024-2029. Source table 1.6 publishes calendar-year ILO unemployment rate, 16+, percent. | +| `db/etl_obr.py` `cpi_inflation` (2024-2029) | `obr.cpi_inflation` | `obr-efo-economy-march-2026` | None for 2024-2029. Source table 1.7 publishes CPI year-on-year growth. | +| `db/etl_obr.py` `rpi_inflation` (2024-2029) | `obr.rpi_inflation` | `obr-efo-economy-march-2026` | None for 2024-2029. Source table 1.7 publishes RPI year-on-year growth. | +| `db/etl_obr.py` `bank_rate` (2024-2029) | `obr.bank_rate` | `obr-efo-economy-march-2026` | None for 2024-2029. Source table 1.9 publishes the Bank Rate conditioning assumption. | +| `db/etl_obr.py` `employment` (2024-2029) | `obr.employment_16_plus` | `obr-efo-economy-march-2026` | None for 2024-2029. Source table 1.6 publishes calendar-year employment, 16+, in millions; Chronicle stores the published level with `value_scale`. | +| `db/etl_ons.py` `population_total` (2024-2034) | `ons.mid_year_population_estimate` for 2024; `ons.projected_population_principal` for 2024-based projections | existing `ons-mye-2024-uk`; existing `ons-uk-population-projections-2024` | None for 2024-2034 at fact level. The packages publish single-age/sex rows; broad totals are consumer rollups, not separate Chronicle-computed facts. | +| `db/etl_ons.py` `population_age_0_15` (2024-2034) | `ons.mid_year_population_estimate` for 2024; `ons.projected_population_principal` for 2024-based projections | existing `ons-mye-2024-uk`; existing `ons-uk-population-projections-2024` | None for 2024-2034 at fact level. The broad band is a consumer rollup from source-published single-age rows. | +| `db/etl_ons.py` `population_age_16_64` (2024-2034) | `ons.mid_year_population_estimate` for 2024; `ons.projected_population_principal` for 2024-based projections | existing `ons-mye-2024-uk`; existing `ons-uk-population-projections-2024` | None for 2024-2034 at fact level. The broad band is a consumer rollup from source-published single-age rows. | +| `db/etl_ons.py` `population_age_65_plus` (2024-2034) | `ons.mid_year_population_estimate` for 2024; `ons.projected_population_principal` for 2024-based projections | existing `ons-mye-2024-uk`; existing `ons-uk-population-projections-2024` | None for 2024-2034 at fact level. The broad band is a consumer rollup from source-published single-age rows. | +| `db/etl_ons.py` `households_total` (2024-2034) | `ons.households_total` for 2024-2025; `ons.households_by_type` and census TS041 for household-count source surfaces | existing `ons-families-households-2025` (new Table 5 record sets); existing census household packages | 2026-2034 remain gaps for UK household projections: the committed ONS Families & Households workbook publishes estimates through 2025 only. | +| `db/etl_ons.py` `average_household_size` (2024-2034) | `ons.average_household_size` for 2024-2025 | existing `ons-families-households-2025` (new Table 5 record sets) | 2026-2034 remain gaps: the committed ONS Families & Households workbook publishes average household size through 2025 only. | +| `db/etl_hmrc.py` `income_tax` (2021-2022) | HMRC publishes taxpayer counts and liabilities from SPI in its [Income Tax liabilities statistics](https://www.gov.uk/government/statistics/income-tax-liabilities-statistics-tax-year-2020-to-2021-to-tax-year-2023-to-2024). | — | **Explicit source gap or non-equivalence.** The legacy amount is an unexplained hardcoded value and cannot be replaced by a liability or receipts fact without proving the measure and definition year match. | +| `db/etl_hmrc.py` `national_insurance` (2021-2022) | HMRC's [National Insurance contributions statistics](https://www.gov.uk/government/statistics/main-features-of-national-insurance-contributions) and receipts tables are candidate sources, but the legacy amount does not identify contributions versus receipts. | — | **Explicit source gap or non-equivalence.** No exact publisher measure has been established for the legacy value. | +| `db/etl_hmrc.py` `capital_gains_tax` (2021-2022) | `hmrc.cgt_tax_total` in `hmrc-cgt-statistics-2025` (tax-year periods 2021, 2022, and 2023; Table 1 total tax liabilities). | `hmrc-cgt-statistics-2025` | **Covered by publisher fact, not by the legacy value.** The existing package is the single source of CGT facts; the legacy hardcoded amounts are not duplicated because their provenance and values do not match the Table 1 liabilities. | +| `db/etl_hmrc.py` `inheritance_tax` (2021-2022) | HMRC tax receipts tables are a candidate receipts source, but no Chronicle package currently carries an IHT record set. | — | **Explicit source gap or non-equivalence.** The legacy amount does not identify IHT liabilities, receipts, or fiscal-year definition. | +| `db/etl_hmrc.py` `taxpayers`, `higher_rate_taxpayers`, `additional_rate_taxpayers` (2021-2022) | `hmrc-spi-income-bands-2023-24` publishes 2023 tax-year income-band counts; HMRC's 2021-22 [Personal Incomes Statistics](https://www.gov.uk/government/statistics/personal-incomes-statistics-for-the-tax-year-2021-to-2022) is a candidate older artifact. | `hmrc-spi-income-bands-2023-24` for tax year 2023 only | **Explicit source gap or non-equivalence** for the legacy 2021-2022 counts. The existing SPI package must not be silently backdated or treated as the same band universe. | +| `db/etl_hmrc.py` `total_income` (2021-2022) | `hmrc-spi-income-bands-2023-24` publishes income-type distributions by total-income band for tax year 2023, not a national total-income amount. | `hmrc-spi-income-bands-2023-24` for its published band facts | **Explicit source gap or non-equivalence.** A national total would require a publisher total and cannot be reconstructed in Chronicle by summing/adjusting bands. | +| `db/etl_hmrc.py` `benefits.universal_credit.{recipients,expenditure}` (2021-2022) | Existing DWP packages cover 2025 UC caseload dimensions. The [DWP expenditure and caseload tables](https://www.gov.uk/government/publications/benefit-expenditure-and-caseload-tables-2022) are candidate sources for historical national rows. | — | **Explicit source gap or non-equivalence.** The legacy recipient universe and expenditure period are not defined well enough to map to the existing 2025 household facts. | +| `db/etl_hmrc.py` `benefits.child_benefit.{recipients,expenditure}` (2021-2022) | HMRC's [August 2022 Child Benefit statistics](https://www.gov.uk/government/statistics/child-benefit-statistics-annual-release-august-2022/child-benefit-statistics-annual-release-data-at-august-2022) is a candidate caseload source; receipts/expenditure requires a separate fiscal-year source. | — | **Explicit source gap or non-equivalence.** The legacy recipient and expenditure definitions are not interchangeable with the available caseload publication. | +| `db/etl_hmrc.py` `benefits.state_pension.{recipients,expenditure}` (2021-2022) | DWP expenditure/caseload tables are candidate sources; the existing SPI package's state-pension income bands are a different person/income measure. | — | **Explicit source gap or non-equivalence.** No exact DWP national recipient/expenditure record set is currently packaged. | +| `db/etl_hmrc.py` `benefits.housing_benefit.{recipients,expenditure}` (2021-2022) | DWP's benefit expenditure/caseload and [income-related benefit take-up](https://www.gov.uk/government/statistics/income-related-benefits-estimates-of-take-up-financial-year-ending-2022) tables are candidate sources. | — | **Explicit source gap or non-equivalence.** Existing OBR forecasts and DWP caseload/take-up measures cannot silently replace the legacy value. | +| `db/etl_hmrc.py` `benefits.pension_credit.{recipients,expenditure}` (2021-2022) | DWP benefit expenditure/caseload and income-related take-up tables are candidate sources. | — | **Explicit source gap or non-equivalence.** No exact packaged record set has been established for the legacy recipient and expenditure definitions. | + +### HMRC identity and period gate + +The rows above are an inventory of the legacy target surface, not permission to +carry its hardcoded values into Chronicle. A source fact is considered covered +only when its canonical concept, `source_concept`, unit and scale, geography, +entity/universe, aggregation, `period_type`, period, `assertion`, source +vintage, `artifact_year`, and `record_set_spec_id` have been checked. In +particular, an observation and a publisher projection for the same variable, +year, and unit are distinct facts and may coexist only when the consumer +selector distinguishes their `assertion`; no selector may silently choose +between them. + +For each HMRC source artifact, all publisher-provided definition years from +2023 through 2026 must be retained in Chronicle when available, even when a +Microcosm contract materializes only one year. Tax, fiscal, calendar, and +publication years must remain distinct. `artifact_year` identifies the +publication vintage; it does not redefine the fact period. ## Link-rot found during the migration diff --git a/packages/obr/efo_aggregates_march_2026/source_package.yaml b/packages/obr/efo_aggregates_march_2026/source_package.yaml new file mode 100644 index 00000000..5c945d40 --- /dev/null +++ b/packages/obr/efo_aggregates_march_2026/source_package.yaml @@ -0,0 +1,912 @@ +schema_version: ledger.source_package.v1 +package_id: obr-efo-aggregates-march-2026 +label: 'OBR EFO March 2026 fiscal supplementary tables: aggregates' +artifact: + source_name: obr + source_table: 'EFO March 2026 fiscal supplementary tables: aggregates' + resource_package: db + resource_directory: data/obr/efo_aggregates_march_2026 + manifest: manifest.yaml + vintage: efo_march_2026 + extracted_at: '2026-08-19' + extraction_method: xlsx whole-workbook used-range cell parse + parser: xlsx_used_range + artifact_year: 2026 +record_sets: +- record_set_id: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2025 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2025.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2025 + sheet_name: '6.2' + period_type: fiscal_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_current_receipts + label: 'Current Receipts ' + ordinal: 0 + row_number: 78 + expected_row_header_column: B + expected_row_header: 'Current Receipts ' + table_record_kind: total + measures: + - measure_id: public_sector_current_receipts + label: Current Receipts , FY2025-26 + ordinal: 0 + column: D + source_column_id: public_sector_current_receipts + concept: obr.public_sector_current_receipts + source_concept: obr.efo_aggregates.public_sector_current_receipts + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.2, Public + Sector Current Receipts row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2025-26 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2026 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2026.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2026 + sheet_name: '6.2' + period_type: fiscal_year + period: 2026 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_current_receipts + label: 'Current Receipts ' + ordinal: 0 + row_number: 78 + expected_row_header_column: B + expected_row_header: 'Current Receipts ' + table_record_kind: total + measures: + - measure_id: public_sector_current_receipts + label: Current Receipts , FY2026-27 + ordinal: 0 + column: E + source_column_id: public_sector_current_receipts + concept: obr.public_sector_current_receipts + source_concept: obr.efo_aggregates.public_sector_current_receipts + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.2, Public + Sector Current Receipts row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2026-27 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2027 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2027.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2027 + sheet_name: '6.2' + period_type: fiscal_year + period: 2027 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_current_receipts + label: 'Current Receipts ' + ordinal: 0 + row_number: 78 + expected_row_header_column: B + expected_row_header: 'Current Receipts ' + table_record_kind: total + measures: + - measure_id: public_sector_current_receipts + label: Current Receipts , FY2027-28 + ordinal: 0 + column: F + source_column_id: public_sector_current_receipts + concept: obr.public_sector_current_receipts + source_concept: obr.efo_aggregates.public_sector_current_receipts + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.2, Public + Sector Current Receipts row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2027-28 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2028 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2028.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2028 + sheet_name: '6.2' + period_type: fiscal_year + period: 2028 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_current_receipts + label: 'Current Receipts ' + ordinal: 0 + row_number: 78 + expected_row_header_column: B + expected_row_header: 'Current Receipts ' + table_record_kind: total + measures: + - measure_id: public_sector_current_receipts + label: Current Receipts , FY2028-29 + ordinal: 0 + column: G + source_column_id: public_sector_current_receipts + concept: obr.public_sector_current_receipts + source_concept: obr.efo_aggregates.public_sector_current_receipts + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.2, Public + Sector Current Receipts row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2028-29 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2029 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2029.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_current_receipts.fy2029 + sheet_name: '6.2' + period_type: fiscal_year + period: 2029 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_current_receipts + label: 'Current Receipts ' + ordinal: 0 + row_number: 78 + expected_row_header_column: B + expected_row_header: 'Current Receipts ' + table_record_kind: total + measures: + - measure_id: public_sector_current_receipts + label: Current Receipts , FY2029-30 + ordinal: 0 + column: H + source_column_id: public_sector_current_receipts + concept: obr.public_sector_current_receipts + source_concept: obr.efo_aggregates.public_sector_current_receipts + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.2, Public + Sector Current Receipts row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2029-30 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2025 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2025.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2025 + sheet_name: '6.1' + period_type: fiscal_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: total_managed_expenditure + label: Total Managed Expenditure1 + ordinal: 0 + row_number: 101 + expected_row_header_column: B + expected_row_header: Total Managed Expenditure1 + table_record_kind: total + measures: + - measure_id: total_managed_expenditure + label: Total Managed Expenditure1, FY2025-26 + ordinal: 0 + column: D + source_column_id: total_managed_expenditure + concept: obr.total_managed_expenditure + source_concept: obr.efo_aggregates.total_managed_expenditure + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.1, Total + Managed Expenditure row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2025-26 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2026 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2026.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2026 + sheet_name: '6.1' + period_type: fiscal_year + period: 2026 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: total_managed_expenditure + label: Total Managed Expenditure1 + ordinal: 0 + row_number: 101 + expected_row_header_column: B + expected_row_header: Total Managed Expenditure1 + table_record_kind: total + measures: + - measure_id: total_managed_expenditure + label: Total Managed Expenditure1, FY2026-27 + ordinal: 0 + column: E + source_column_id: total_managed_expenditure + concept: obr.total_managed_expenditure + source_concept: obr.efo_aggregates.total_managed_expenditure + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.1, Total + Managed Expenditure row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2026-27 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2027 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2027.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2027 + sheet_name: '6.1' + period_type: fiscal_year + period: 2027 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: total_managed_expenditure + label: Total Managed Expenditure1 + ordinal: 0 + row_number: 101 + expected_row_header_column: B + expected_row_header: Total Managed Expenditure1 + table_record_kind: total + measures: + - measure_id: total_managed_expenditure + label: Total Managed Expenditure1, FY2027-28 + ordinal: 0 + column: F + source_column_id: total_managed_expenditure + concept: obr.total_managed_expenditure + source_concept: obr.efo_aggregates.total_managed_expenditure + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.1, Total + Managed Expenditure row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2027-28 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2028 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2028.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2028 + sheet_name: '6.1' + period_type: fiscal_year + period: 2028 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: total_managed_expenditure + label: Total Managed Expenditure1 + ordinal: 0 + row_number: 101 + expected_row_header_column: B + expected_row_header: Total Managed Expenditure1 + table_record_kind: total + measures: + - measure_id: total_managed_expenditure + label: Total Managed Expenditure1, FY2028-29 + ordinal: 0 + column: G + source_column_id: total_managed_expenditure + concept: obr.total_managed_expenditure + source_concept: obr.efo_aggregates.total_managed_expenditure + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.1, Total + Managed Expenditure row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2028-29 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2029 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2029.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.total_managed_expenditure.fy2029 + sheet_name: '6.1' + period_type: fiscal_year + period: 2029 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: total_managed_expenditure + label: Total Managed Expenditure1 + ordinal: 0 + row_number: 101 + expected_row_header_column: B + expected_row_header: Total Managed Expenditure1 + table_record_kind: total + measures: + - measure_id: total_managed_expenditure + label: Total Managed Expenditure1, FY2029-30 + ordinal: 0 + column: H + source_column_id: total_managed_expenditure + concept: obr.total_managed_expenditure + source_concept: obr.efo_aggregates.total_managed_expenditure + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.1, Total + Managed Expenditure row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2029-30 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2025 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2025.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2025 + sheet_name: '6.5' + period_type: fiscal_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_borrowing + label: Net borrowing + ordinal: 0 + row_number: 15 + expected_row_header_column: B + expected_row_header: Net borrowing + table_record_kind: total + measures: + - measure_id: public_sector_net_borrowing + label: Net borrowing, FY2025-26 + ordinal: 0 + column: C + source_column_id: public_sector_net_borrowing + concept: obr.public_sector_net_borrowing + source_concept: obr.efo_aggregates.public_sector_net_borrowing + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.5, Net + borrowing row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2025-26 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2026 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2026.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2026 + sheet_name: '6.5' + period_type: fiscal_year + period: 2026 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_borrowing + label: Net borrowing + ordinal: 0 + row_number: 15 + expected_row_header_column: B + expected_row_header: Net borrowing + table_record_kind: total + measures: + - measure_id: public_sector_net_borrowing + label: Net borrowing, FY2026-27 + ordinal: 0 + column: D + source_column_id: public_sector_net_borrowing + concept: obr.public_sector_net_borrowing + source_concept: obr.efo_aggregates.public_sector_net_borrowing + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.5, Net + borrowing row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2026-27 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2027 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2027.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2027 + sheet_name: '6.5' + period_type: fiscal_year + period: 2027 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_borrowing + label: Net borrowing + ordinal: 0 + row_number: 15 + expected_row_header_column: B + expected_row_header: Net borrowing + table_record_kind: total + measures: + - measure_id: public_sector_net_borrowing + label: Net borrowing, FY2027-28 + ordinal: 0 + column: E + source_column_id: public_sector_net_borrowing + concept: obr.public_sector_net_borrowing + source_concept: obr.efo_aggregates.public_sector_net_borrowing + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.5, Net + borrowing row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2027-28 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2028 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2028.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2028 + sheet_name: '6.5' + period_type: fiscal_year + period: 2028 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_borrowing + label: Net borrowing + ordinal: 0 + row_number: 15 + expected_row_header_column: B + expected_row_header: Net borrowing + table_record_kind: total + measures: + - measure_id: public_sector_net_borrowing + label: Net borrowing, FY2028-29 + ordinal: 0 + column: F + source_column_id: public_sector_net_borrowing + concept: obr.public_sector_net_borrowing + source_concept: obr.efo_aggregates.public_sector_net_borrowing + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.5, Net + borrowing row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2028-29 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2029 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2029.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2029 + sheet_name: '6.5' + period_type: fiscal_year + period: 2029 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_borrowing + label: Net borrowing + ordinal: 0 + row_number: 15 + expected_row_header_column: B + expected_row_header: Net borrowing + table_record_kind: total + measures: + - measure_id: public_sector_net_borrowing + label: Net borrowing, FY2029-30 + ordinal: 0 + column: G + source_column_id: public_sector_net_borrowing + concept: obr.public_sector_net_borrowing + source_concept: obr.efo_aggregates.public_sector_net_borrowing + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.5, Net + borrowing row, GBP billion. + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + expected_column_header_row: 5 + expected_column_header: 2029-30 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2024 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2024.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2024 + sheet_name: '6.13' + period_type: fiscal_year + period: 2024 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), per cent of GDP + ordinal: 0 + row_number: 31 + expected_row_header_column: B + expected_row_header: "Public sector net debt\n (PSND)(d+g)" + table_record_kind: total + measures: + - measure_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), FY2024-25 + ordinal: 0 + column: C + source_column_id: public_sector_net_debt_percent_gdp + concept: obr.public_sector_net_debt_percent_gdp + source_concept: obr.efo_aggregates.public_sector_net_debt + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.13, Public + sector net debt (PSND) row, per cent of GDP. + unit: percent_of_gdp + aggregation: sum + expected_cell_type: number + expected_column_header_row: 5 + expected_column_header: 2024-25 + assertion: observation +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2025 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2025.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2025 + sheet_name: '6.13' + period_type: fiscal_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), per cent of GDP + ordinal: 0 + row_number: 31 + expected_row_header_column: B + expected_row_header: "Public sector net debt\n (PSND)(d+g)" + table_record_kind: total + measures: + - measure_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), FY2025-26 + ordinal: 0 + column: D + source_column_id: public_sector_net_debt_percent_gdp + concept: obr.public_sector_net_debt_percent_gdp + source_concept: obr.efo_aggregates.public_sector_net_debt + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.13, Public + sector net debt (PSND) row, per cent of GDP. + unit: percent_of_gdp + aggregation: sum + expected_cell_type: number + expected_column_header_row: 5 + expected_column_header: 2025-26 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2026 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2026.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2026 + sheet_name: '6.13' + period_type: fiscal_year + period: 2026 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), per cent of GDP + ordinal: 0 + row_number: 31 + expected_row_header_column: B + expected_row_header: "Public sector net debt\n (PSND)(d+g)" + table_record_kind: total + measures: + - measure_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), FY2026-27 + ordinal: 0 + column: E + source_column_id: public_sector_net_debt_percent_gdp + concept: obr.public_sector_net_debt_percent_gdp + source_concept: obr.efo_aggregates.public_sector_net_debt + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.13, Public + sector net debt (PSND) row, per cent of GDP. + unit: percent_of_gdp + aggregation: sum + expected_cell_type: number + expected_column_header_row: 5 + expected_column_header: 2026-27 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2027 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2027.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2027 + sheet_name: '6.13' + period_type: fiscal_year + period: 2027 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), per cent of GDP + ordinal: 0 + row_number: 31 + expected_row_header_column: B + expected_row_header: "Public sector net debt\n (PSND)(d+g)" + table_record_kind: total + measures: + - measure_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), FY2027-28 + ordinal: 0 + column: F + source_column_id: public_sector_net_debt_percent_gdp + concept: obr.public_sector_net_debt_percent_gdp + source_concept: obr.efo_aggregates.public_sector_net_debt + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.13, Public + sector net debt (PSND) row, per cent of GDP. + unit: percent_of_gdp + aggregation: sum + expected_cell_type: number + expected_column_header_row: 5 + expected_column_header: 2027-28 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2028 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2028.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2028 + sheet_name: '6.13' + period_type: fiscal_year + period: 2028 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), per cent of GDP + ordinal: 0 + row_number: 31 + expected_row_header_column: B + expected_row_header: "Public sector net debt\n (PSND)(d+g)" + table_record_kind: total + measures: + - measure_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), FY2028-29 + ordinal: 0 + column: G + source_column_id: public_sector_net_debt_percent_gdp + concept: obr.public_sector_net_debt_percent_gdp + source_concept: obr.efo_aggregates.public_sector_net_debt + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.13, Public + sector net debt (PSND) row, per cent of GDP. + unit: percent_of_gdp + aggregation: sum + expected_cell_type: number + expected_column_header_row: 5 + expected_column_header: 2028-29 + assertion: source_projection +- record_set_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2029 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2029.v1 + source_record_id_prefix: obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2029 + sheet_name: '6.13' + period_type: fiscal_year + period: 2029 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: public_sector + domain: public_sector_finances + groupby_dimension: obr.efo_line + rows: + - value_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), per cent of GDP + ordinal: 0 + row_number: 31 + expected_row_header_column: B + expected_row_header: "Public sector net debt\n (PSND)(d+g)" + table_record_kind: total + measures: + - measure_id: public_sector_net_debt_percent_gdp + label: Public sector net debt (PSND), FY2029-30 + ordinal: 0 + column: H + source_column_id: public_sector_net_debt_percent_gdp + concept: obr.public_sector_net_debt_percent_gdp + source_concept: obr.efo_aggregates.public_sector_net_debt + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-aggregates/ + concept_evidence_notes: OBR EFO March 2026 fiscal supplementary tables, sheet 6.13, Public + sector net debt (PSND) row, per cent of GDP. + unit: percent_of_gdp + aggregation: sum + expected_cell_type: number + expected_column_header_row: 5 + expected_column_header: 2029-30 + assertion: source_projection diff --git a/packages/obr/efo_economy_march_2026/source_package.yaml b/packages/obr/efo_economy_march_2026/source_package.yaml new file mode 100644 index 00000000..5e74bd6e --- /dev/null +++ b/packages/obr/efo_economy_march_2026/source_package.yaml @@ -0,0 +1,1557 @@ +schema_version: ledger.source_package.v1 +package_id: obr-efo-economy-march-2026 +label: 'OBR EFO March 2026 detailed forecast tables: economy' +artifact: + source_name: obr + source_table: 'EFO March 2026 detailed forecast tables: economy' + resource_package: db + resource_directory: data/obr/efo_economy_march_2026 + manifest: manifest.yaml + vintage: efo_march_2026 + extracted_at: '2026-08-19' + extraction_method: xlsx whole-workbook used-range cell parse + parser: xlsx_used_range + artifact_year: 2026 +record_sets: +- record_set_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2024 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2024.v1 + source_record_id_prefix: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2024 + sheet_name: '1.4' + period_type: calendar_year + period: 2024 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions + ordinal: 0 + row_number: 113 + expected_row_header_column: B + expected_row_header: 2024 + table_record_kind: total + guard_cells: + - column: B + row: 2 + expected_value: "1.4 Nominal GDP (\xA3 billion, non-seasonally adjusted)" + label: table title + measures: + - measure_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions, 2024 + ordinal: 0 + column: C + source_column_id: nominal_gdp_nsa + concept: obr.nominal_gdp_nsa + source_concept: obr.efo_economy.nominal_gdp_nsa + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: "OBR EFO March 2026 detailed forecast tables, sheet 1.4 ('Nominal\ + \ GDP (\xA3 billion, non-seasonally adjusted)'), calendar-year row; GBP billion." + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + assertion: observation +- record_set_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2025 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2025.v1 + source_record_id_prefix: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2025 + sheet_name: '1.4' + period_type: calendar_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions + ordinal: 0 + row_number: 114 + expected_row_header_column: B + expected_row_header: 2025 + table_record_kind: total + guard_cells: + - column: B + row: 2 + expected_value: "1.4 Nominal GDP (\xA3 billion, non-seasonally adjusted)" + label: table title + measures: + - measure_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions, 2025 + ordinal: 0 + column: C + source_column_id: nominal_gdp_nsa + concept: obr.nominal_gdp_nsa + source_concept: obr.efo_economy.nominal_gdp_nsa + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: "OBR EFO March 2026 detailed forecast tables, sheet 1.4 ('Nominal\ + \ GDP (\xA3 billion, non-seasonally adjusted)'), calendar-year row; GBP billion." + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2026 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2026.v1 + source_record_id_prefix: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2026 + sheet_name: '1.4' + period_type: calendar_year + period: 2026 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions + ordinal: 0 + row_number: 115 + expected_row_header_column: B + expected_row_header: 2026 + table_record_kind: total + guard_cells: + - column: B + row: 2 + expected_value: "1.4 Nominal GDP (\xA3 billion, non-seasonally adjusted)" + label: table title + measures: + - measure_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions, 2026 + ordinal: 0 + column: C + source_column_id: nominal_gdp_nsa + concept: obr.nominal_gdp_nsa + source_concept: obr.efo_economy.nominal_gdp_nsa + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: "OBR EFO March 2026 detailed forecast tables, sheet 1.4 ('Nominal\ + \ GDP (\xA3 billion, non-seasonally adjusted)'), calendar-year row; GBP billion." + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2027 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2027.v1 + source_record_id_prefix: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2027 + sheet_name: '1.4' + period_type: calendar_year + period: 2027 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions + ordinal: 0 + row_number: 116 + expected_row_header_column: B + expected_row_header: 2027 + table_record_kind: total + guard_cells: + - column: B + row: 2 + expected_value: "1.4 Nominal GDP (\xA3 billion, non-seasonally adjusted)" + label: table title + measures: + - measure_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions, 2027 + ordinal: 0 + column: C + source_column_id: nominal_gdp_nsa + concept: obr.nominal_gdp_nsa + source_concept: obr.efo_economy.nominal_gdp_nsa + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: "OBR EFO March 2026 detailed forecast tables, sheet 1.4 ('Nominal\ + \ GDP (\xA3 billion, non-seasonally adjusted)'), calendar-year row; GBP billion." + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2028 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2028.v1 + source_record_id_prefix: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2028 + sheet_name: '1.4' + period_type: calendar_year + period: 2028 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions + ordinal: 0 + row_number: 117 + expected_row_header_column: B + expected_row_header: 2028 + table_record_kind: total + guard_cells: + - column: B + row: 2 + expected_value: "1.4 Nominal GDP (\xA3 billion, non-seasonally adjusted)" + label: table title + measures: + - measure_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions, 2028 + ordinal: 0 + column: C + source_column_id: nominal_gdp_nsa + concept: obr.nominal_gdp_nsa + source_concept: obr.efo_economy.nominal_gdp_nsa + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: "OBR EFO March 2026 detailed forecast tables, sheet 1.4 ('Nominal\ + \ GDP (\xA3 billion, non-seasonally adjusted)'), calendar-year row; GBP billion." + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2029 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2029.v1 + source_record_id_prefix: obr.efo_2026_03.economy.nominal_gdp_nsa.cy2029 + sheet_name: '1.4' + period_type: calendar_year + period: 2029 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions + ordinal: 0 + row_number: 118 + expected_row_header_column: B + expected_row_header: 2029 + table_record_kind: total + guard_cells: + - column: B + row: 2 + expected_value: "1.4 Nominal GDP (\xA3 billion, non-seasonally adjusted)" + label: table title + measures: + - measure_id: nominal_gdp_nsa + label: Nominal GDP NSA, billions, 2029 + ordinal: 0 + column: C + source_column_id: nominal_gdp_nsa + concept: obr.nominal_gdp_nsa + source_concept: obr.efo_economy.nominal_gdp_nsa + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: "OBR EFO March 2026 detailed forecast tables, sheet 1.4 ('Nominal\ + \ GDP (\xA3 billion, non-seasonally adjusted)'), calendar-year row; GBP billion." + unit: gbp + aggregation: sum + expected_cell_type: number + value_scale: 1000000000 + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.employment_16_plus.cy2024 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.employment_16_plus.cy2024.v1 + source_record_id_prefix: obr.efo_2026_03.economy.employment_16_plus.cy2024 + sheet_name: '1.6' + period_type: calendar_year + period: 2024 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: employment_16_plus + label: Employment (16+, millions) + ordinal: 0 + row_number: 113 + expected_row_header_column: B + expected_row_header: 2024 + table_record_kind: total + measures: + - measure_id: employment_16_plus + label: Employment (16+, millions), 2024 + ordinal: 0 + column: C + source_column_id: employment_16_plus + concept: obr.employment_16_plus + source_concept: obr.efo_economy.employment_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'Employment (16+, millions)'. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000000 + expected_column_header_row: 3 + expected_column_header: Employment (16+, millions) + assertion: observation +- record_set_id: obr.efo_2026_03.economy.employment_16_plus.cy2025 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.employment_16_plus.cy2025.v1 + source_record_id_prefix: obr.efo_2026_03.economy.employment_16_plus.cy2025 + sheet_name: '1.6' + period_type: calendar_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: employment_16_plus + label: Employment (16+, millions) + ordinal: 0 + row_number: 114 + expected_row_header_column: B + expected_row_header: 2025 + table_record_kind: total + measures: + - measure_id: employment_16_plus + label: Employment (16+, millions), 2025 + ordinal: 0 + column: C + source_column_id: employment_16_plus + concept: obr.employment_16_plus + source_concept: obr.efo_economy.employment_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'Employment (16+, millions)'. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000000 + expected_column_header_row: 3 + expected_column_header: Employment (16+, millions) + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.employment_16_plus.cy2026 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.employment_16_plus.cy2026.v1 + source_record_id_prefix: obr.efo_2026_03.economy.employment_16_plus.cy2026 + sheet_name: '1.6' + period_type: calendar_year + period: 2026 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: employment_16_plus + label: Employment (16+, millions) + ordinal: 0 + row_number: 115 + expected_row_header_column: B + expected_row_header: 2026 + table_record_kind: total + measures: + - measure_id: employment_16_plus + label: Employment (16+, millions), 2026 + ordinal: 0 + column: C + source_column_id: employment_16_plus + concept: obr.employment_16_plus + source_concept: obr.efo_economy.employment_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'Employment (16+, millions)'. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000000 + expected_column_header_row: 3 + expected_column_header: Employment (16+, millions) + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.employment_16_plus.cy2027 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.employment_16_plus.cy2027.v1 + source_record_id_prefix: obr.efo_2026_03.economy.employment_16_plus.cy2027 + sheet_name: '1.6' + period_type: calendar_year + period: 2027 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: employment_16_plus + label: Employment (16+, millions) + ordinal: 0 + row_number: 116 + expected_row_header_column: B + expected_row_header: 2027 + table_record_kind: total + measures: + - measure_id: employment_16_plus + label: Employment (16+, millions), 2027 + ordinal: 0 + column: C + source_column_id: employment_16_plus + concept: obr.employment_16_plus + source_concept: obr.efo_economy.employment_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'Employment (16+, millions)'. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000000 + expected_column_header_row: 3 + expected_column_header: Employment (16+, millions) + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.employment_16_plus.cy2028 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.employment_16_plus.cy2028.v1 + source_record_id_prefix: obr.efo_2026_03.economy.employment_16_plus.cy2028 + sheet_name: '1.6' + period_type: calendar_year + period: 2028 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: employment_16_plus + label: Employment (16+, millions) + ordinal: 0 + row_number: 117 + expected_row_header_column: B + expected_row_header: 2028 + table_record_kind: total + measures: + - measure_id: employment_16_plus + label: Employment (16+, millions), 2028 + ordinal: 0 + column: C + source_column_id: employment_16_plus + concept: obr.employment_16_plus + source_concept: obr.efo_economy.employment_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'Employment (16+, millions)'. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000000 + expected_column_header_row: 3 + expected_column_header: Employment (16+, millions) + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.employment_16_plus.cy2029 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.employment_16_plus.cy2029.v1 + source_record_id_prefix: obr.efo_2026_03.economy.employment_16_plus.cy2029 + sheet_name: '1.6' + period_type: calendar_year + period: 2029 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: employment_16_plus + label: Employment (16+, millions) + ordinal: 0 + row_number: 118 + expected_row_header_column: B + expected_row_header: 2029 + table_record_kind: total + measures: + - measure_id: employment_16_plus + label: Employment (16+, millions), 2029 + ordinal: 0 + column: C + source_column_id: employment_16_plus + concept: obr.employment_16_plus + source_concept: obr.efo_economy.employment_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'Employment (16+, millions)'. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000000 + expected_column_header_row: 3 + expected_column_header: Employment (16+, millions) + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2024 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2024.v1 + source_record_id_prefix: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2024 + sheet_name: '1.6' + period_type: calendar_year + period: 2024 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent) + ordinal: 0 + row_number: 113 + expected_row_header_column: B + expected_row_header: 2024 + table_record_kind: total + measures: + - measure_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent), 2024 + ordinal: 0 + column: G + source_column_id: ilo_unemployment_rate_16_plus + concept: obr.ilo_unemployment_rate_16_plus + source_concept: obr.efo_economy.ilo_unemployment_rate_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'ILO unemployment rate (16+, per cent)'. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: "ILO unemployment rate \n(16+, per cent)" + assertion: observation +- record_set_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2025 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2025.v1 + source_record_id_prefix: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2025 + sheet_name: '1.6' + period_type: calendar_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent) + ordinal: 0 + row_number: 114 + expected_row_header_column: B + expected_row_header: 2025 + table_record_kind: total + measures: + - measure_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent), 2025 + ordinal: 0 + column: G + source_column_id: ilo_unemployment_rate_16_plus + concept: obr.ilo_unemployment_rate_16_plus + source_concept: obr.efo_economy.ilo_unemployment_rate_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'ILO unemployment rate (16+, per cent)'. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: "ILO unemployment rate \n(16+, per cent)" + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2026 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2026.v1 + source_record_id_prefix: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2026 + sheet_name: '1.6' + period_type: calendar_year + period: 2026 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent) + ordinal: 0 + row_number: 115 + expected_row_header_column: B + expected_row_header: 2026 + table_record_kind: total + measures: + - measure_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent), 2026 + ordinal: 0 + column: G + source_column_id: ilo_unemployment_rate_16_plus + concept: obr.ilo_unemployment_rate_16_plus + source_concept: obr.efo_economy.ilo_unemployment_rate_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'ILO unemployment rate (16+, per cent)'. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: "ILO unemployment rate \n(16+, per cent)" + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2027 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2027.v1 + source_record_id_prefix: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2027 + sheet_name: '1.6' + period_type: calendar_year + period: 2027 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent) + ordinal: 0 + row_number: 116 + expected_row_header_column: B + expected_row_header: 2027 + table_record_kind: total + measures: + - measure_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent), 2027 + ordinal: 0 + column: G + source_column_id: ilo_unemployment_rate_16_plus + concept: obr.ilo_unemployment_rate_16_plus + source_concept: obr.efo_economy.ilo_unemployment_rate_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'ILO unemployment rate (16+, per cent)'. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: "ILO unemployment rate \n(16+, per cent)" + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2028 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2028.v1 + source_record_id_prefix: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2028 + sheet_name: '1.6' + period_type: calendar_year + period: 2028 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent) + ordinal: 0 + row_number: 117 + expected_row_header_column: B + expected_row_header: 2028 + table_record_kind: total + measures: + - measure_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent), 2028 + ordinal: 0 + column: G + source_column_id: ilo_unemployment_rate_16_plus + concept: obr.ilo_unemployment_rate_16_plus + source_concept: obr.efo_economy.ilo_unemployment_rate_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'ILO unemployment rate (16+, per cent)'. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: "ILO unemployment rate \n(16+, per cent)" + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2029 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2029.v1 + source_record_id_prefix: obr.efo_2026_03.economy.ilo_unemployment_rate_16_plus.cy2029 + sheet_name: '1.6' + period_type: calendar_year + period: 2029 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent) + ordinal: 0 + row_number: 118 + expected_row_header_column: B + expected_row_header: 2029 + table_record_kind: total + measures: + - measure_id: ilo_unemployment_rate_16_plus + label: ILO unemployment rate (16+, per cent), 2029 + ordinal: 0 + column: G + source_column_id: ilo_unemployment_rate_16_plus + concept: obr.ilo_unemployment_rate_16_plus + source_concept: obr.efo_economy.ilo_unemployment_rate_16_plus + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.6, calendar-year + row, column 'ILO unemployment rate (16+, per cent)'. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: "ILO unemployment rate \n(16+, per cent)" + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.rpi_inflation.cy2024 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.rpi_inflation.cy2024.v1 + source_record_id_prefix: obr.efo_2026_03.economy.rpi_inflation.cy2024 + sheet_name: '1.7' + period_type: calendar_year + period: 2024 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: rpi_inflation + label: RPI year-on-year growth + ordinal: 0 + row_number: 114 + expected_row_header_column: B + expected_row_header: 2024 + table_record_kind: total + measures: + - measure_id: rpi_inflation + label: RPI year-on-year growth, 2024 + ordinal: 0 + column: C + source_column_id: rpi_inflation + concept: obr.rpi_inflation + source_concept: obr.efo_economy.rpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, RPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: RPI + assertion: observation +- record_set_id: obr.efo_2026_03.economy.rpi_inflation.cy2025 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.rpi_inflation.cy2025.v1 + source_record_id_prefix: obr.efo_2026_03.economy.rpi_inflation.cy2025 + sheet_name: '1.7' + period_type: calendar_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: rpi_inflation + label: RPI year-on-year growth + ordinal: 0 + row_number: 115 + expected_row_header_column: B + expected_row_header: 2025 + table_record_kind: total + measures: + - measure_id: rpi_inflation + label: RPI year-on-year growth, 2025 + ordinal: 0 + column: C + source_column_id: rpi_inflation + concept: obr.rpi_inflation + source_concept: obr.efo_economy.rpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, RPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: RPI + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.rpi_inflation.cy2026 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.rpi_inflation.cy2026.v1 + source_record_id_prefix: obr.efo_2026_03.economy.rpi_inflation.cy2026 + sheet_name: '1.7' + period_type: calendar_year + period: 2026 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: rpi_inflation + label: RPI year-on-year growth + ordinal: 0 + row_number: 116 + expected_row_header_column: B + expected_row_header: 2026 + table_record_kind: total + measures: + - measure_id: rpi_inflation + label: RPI year-on-year growth, 2026 + ordinal: 0 + column: C + source_column_id: rpi_inflation + concept: obr.rpi_inflation + source_concept: obr.efo_economy.rpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, RPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: RPI + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.rpi_inflation.cy2027 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.rpi_inflation.cy2027.v1 + source_record_id_prefix: obr.efo_2026_03.economy.rpi_inflation.cy2027 + sheet_name: '1.7' + period_type: calendar_year + period: 2027 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: rpi_inflation + label: RPI year-on-year growth + ordinal: 0 + row_number: 117 + expected_row_header_column: B + expected_row_header: 2027 + table_record_kind: total + measures: + - measure_id: rpi_inflation + label: RPI year-on-year growth, 2027 + ordinal: 0 + column: C + source_column_id: rpi_inflation + concept: obr.rpi_inflation + source_concept: obr.efo_economy.rpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, RPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: RPI + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.rpi_inflation.cy2028 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.rpi_inflation.cy2028.v1 + source_record_id_prefix: obr.efo_2026_03.economy.rpi_inflation.cy2028 + sheet_name: '1.7' + period_type: calendar_year + period: 2028 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: rpi_inflation + label: RPI year-on-year growth + ordinal: 0 + row_number: 118 + expected_row_header_column: B + expected_row_header: 2028 + table_record_kind: total + measures: + - measure_id: rpi_inflation + label: RPI year-on-year growth, 2028 + ordinal: 0 + column: C + source_column_id: rpi_inflation + concept: obr.rpi_inflation + source_concept: obr.efo_economy.rpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, RPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: RPI + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.rpi_inflation.cy2029 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.rpi_inflation.cy2029.v1 + source_record_id_prefix: obr.efo_2026_03.economy.rpi_inflation.cy2029 + sheet_name: '1.7' + period_type: calendar_year + period: 2029 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: rpi_inflation + label: RPI year-on-year growth + ordinal: 0 + row_number: 119 + expected_row_header_column: B + expected_row_header: 2029 + table_record_kind: total + measures: + - measure_id: rpi_inflation + label: RPI year-on-year growth, 2029 + ordinal: 0 + column: C + source_column_id: rpi_inflation + concept: obr.rpi_inflation + source_concept: obr.efo_economy.rpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, RPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: RPI + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.cpi_inflation.cy2024 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.cpi_inflation.cy2024.v1 + source_record_id_prefix: obr.efo_2026_03.economy.cpi_inflation.cy2024 + sheet_name: '1.7' + period_type: calendar_year + period: 2024 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: cpi_inflation + label: CPI year-on-year growth + ordinal: 0 + row_number: 114 + expected_row_header_column: B + expected_row_header: 2024 + table_record_kind: total + measures: + - measure_id: cpi_inflation + label: CPI year-on-year growth, 2024 + ordinal: 0 + column: E + source_column_id: cpi_inflation + concept: obr.cpi_inflation + source_concept: obr.efo_economy.cpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, CPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: CPI + assertion: observation +- record_set_id: obr.efo_2026_03.economy.cpi_inflation.cy2025 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.cpi_inflation.cy2025.v1 + source_record_id_prefix: obr.efo_2026_03.economy.cpi_inflation.cy2025 + sheet_name: '1.7' + period_type: calendar_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: cpi_inflation + label: CPI year-on-year growth + ordinal: 0 + row_number: 115 + expected_row_header_column: B + expected_row_header: 2025 + table_record_kind: total + measures: + - measure_id: cpi_inflation + label: CPI year-on-year growth, 2025 + ordinal: 0 + column: E + source_column_id: cpi_inflation + concept: obr.cpi_inflation + source_concept: obr.efo_economy.cpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, CPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: CPI + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.cpi_inflation.cy2026 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.cpi_inflation.cy2026.v1 + source_record_id_prefix: obr.efo_2026_03.economy.cpi_inflation.cy2026 + sheet_name: '1.7' + period_type: calendar_year + period: 2026 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: cpi_inflation + label: CPI year-on-year growth + ordinal: 0 + row_number: 116 + expected_row_header_column: B + expected_row_header: 2026 + table_record_kind: total + measures: + - measure_id: cpi_inflation + label: CPI year-on-year growth, 2026 + ordinal: 0 + column: E + source_column_id: cpi_inflation + concept: obr.cpi_inflation + source_concept: obr.efo_economy.cpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, CPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: CPI + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.cpi_inflation.cy2027 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.cpi_inflation.cy2027.v1 + source_record_id_prefix: obr.efo_2026_03.economy.cpi_inflation.cy2027 + sheet_name: '1.7' + period_type: calendar_year + period: 2027 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: cpi_inflation + label: CPI year-on-year growth + ordinal: 0 + row_number: 117 + expected_row_header_column: B + expected_row_header: 2027 + table_record_kind: total + measures: + - measure_id: cpi_inflation + label: CPI year-on-year growth, 2027 + ordinal: 0 + column: E + source_column_id: cpi_inflation + concept: obr.cpi_inflation + source_concept: obr.efo_economy.cpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, CPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: CPI + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.cpi_inflation.cy2028 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.cpi_inflation.cy2028.v1 + source_record_id_prefix: obr.efo_2026_03.economy.cpi_inflation.cy2028 + sheet_name: '1.7' + period_type: calendar_year + period: 2028 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: cpi_inflation + label: CPI year-on-year growth + ordinal: 0 + row_number: 118 + expected_row_header_column: B + expected_row_header: 2028 + table_record_kind: total + measures: + - measure_id: cpi_inflation + label: CPI year-on-year growth, 2028 + ordinal: 0 + column: E + source_column_id: cpi_inflation + concept: obr.cpi_inflation + source_concept: obr.efo_economy.cpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, CPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: CPI + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.cpi_inflation.cy2029 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.cpi_inflation.cy2029.v1 + source_record_id_prefix: obr.efo_2026_03.economy.cpi_inflation.cy2029 + sheet_name: '1.7' + period_type: calendar_year + period: 2029 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: cpi_inflation + label: CPI year-on-year growth + ordinal: 0 + row_number: 119 + expected_row_header_column: B + expected_row_header: 2029 + table_record_kind: total + measures: + - measure_id: cpi_inflation + label: CPI year-on-year growth, 2029 + ordinal: 0 + column: E + source_column_id: cpi_inflation + concept: obr.cpi_inflation + source_concept: obr.efo_economy.cpi_inflation + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.7, calendar-year + row, CPI year-on-year growth column. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 4 + expected_column_header: CPI + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.bank_rate.cy2024 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.bank_rate.cy2024.v1 + source_record_id_prefix: obr.efo_2026_03.economy.bank_rate.cy2024 + sheet_name: '1.9' + period_type: calendar_year + period: 2024 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: bank_rate + label: Bank Rate + ordinal: 0 + row_number: 113 + expected_row_header_column: B + expected_row_header: 2024 + table_record_kind: total + measures: + - measure_id: bank_rate + label: Bank Rate, 2024 + ordinal: 0 + column: C + source_column_id: bank_rate + concept: obr.bank_rate + source_concept: obr.efo_economy.bank_rate + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.9, calendar-year + row, Bank Rate conditioning assumption. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: Bank Rate + assertion: observation +- record_set_id: obr.efo_2026_03.economy.bank_rate.cy2025 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.bank_rate.cy2025.v1 + source_record_id_prefix: obr.efo_2026_03.economy.bank_rate.cy2025 + sheet_name: '1.9' + period_type: calendar_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: bank_rate + label: Bank Rate + ordinal: 0 + row_number: 114 + expected_row_header_column: B + expected_row_header: 2025 + table_record_kind: total + measures: + - measure_id: bank_rate + label: Bank Rate, 2025 + ordinal: 0 + column: C + source_column_id: bank_rate + concept: obr.bank_rate + source_concept: obr.efo_economy.bank_rate + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.9, calendar-year + row, Bank Rate conditioning assumption. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: Bank Rate + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.bank_rate.cy2026 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.bank_rate.cy2026.v1 + source_record_id_prefix: obr.efo_2026_03.economy.bank_rate.cy2026 + sheet_name: '1.9' + period_type: calendar_year + period: 2026 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: bank_rate + label: Bank Rate + ordinal: 0 + row_number: 115 + expected_row_header_column: B + expected_row_header: 2026 + table_record_kind: total + measures: + - measure_id: bank_rate + label: Bank Rate, 2026 + ordinal: 0 + column: C + source_column_id: bank_rate + concept: obr.bank_rate + source_concept: obr.efo_economy.bank_rate + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.9, calendar-year + row, Bank Rate conditioning assumption. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: Bank Rate + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.bank_rate.cy2027 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.bank_rate.cy2027.v1 + source_record_id_prefix: obr.efo_2026_03.economy.bank_rate.cy2027 + sheet_name: '1.9' + period_type: calendar_year + period: 2027 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: bank_rate + label: Bank Rate + ordinal: 0 + row_number: 116 + expected_row_header_column: B + expected_row_header: 2027 + table_record_kind: total + measures: + - measure_id: bank_rate + label: Bank Rate, 2027 + ordinal: 0 + column: C + source_column_id: bank_rate + concept: obr.bank_rate + source_concept: obr.efo_economy.bank_rate + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.9, calendar-year + row, Bank Rate conditioning assumption. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: Bank Rate + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.bank_rate.cy2028 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.bank_rate.cy2028.v1 + source_record_id_prefix: obr.efo_2026_03.economy.bank_rate.cy2028 + sheet_name: '1.9' + period_type: calendar_year + period: 2028 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: bank_rate + label: Bank Rate + ordinal: 0 + row_number: 117 + expected_row_header_column: B + expected_row_header: 2028 + table_record_kind: total + measures: + - measure_id: bank_rate + label: Bank Rate, 2028 + ordinal: 0 + column: C + source_column_id: bank_rate + concept: obr.bank_rate + source_concept: obr.efo_economy.bank_rate + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.9, calendar-year + row, Bank Rate conditioning assumption. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: Bank Rate + assertion: source_projection +- record_set_id: obr.efo_2026_03.economy.bank_rate.cy2029 + provenance_class: model_output + record_set_spec_id: obr.efo_2026_03.economy.bank_rate.cy2029.v1 + source_record_id_prefix: obr.efo_2026_03.economy.bank_rate.cy2029 + sheet_name: '1.9' + period_type: calendar_year + period: 2029 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: government + entity_role: forecaster + domain: economic_outlook + groupby_dimension: obr.efo_line + rows: + - value_id: bank_rate + label: Bank Rate + ordinal: 0 + row_number: 118 + expected_row_header_column: B + expected_row_header: 2029 + table_record_kind: total + measures: + - measure_id: bank_rate + label: Bank Rate, 2029 + ordinal: 0 + column: C + source_column_id: bank_rate + concept: obr.bank_rate + source_concept: obr.efo_economy.bank_rate + concept_relation: source_label + concept_authority: obr + concept_evidence_url: https://obr.uk/download/march-2026-economic-and-fiscal-outlook-detailed-forecast-tables-economy/ + concept_evidence_notes: OBR EFO March 2026 detailed forecast tables, sheet 1.9, calendar-year + row, Bank Rate conditioning assumption. + unit: percent + aggregation: sum + expected_cell_type: number + expected_column_header_row: 3 + expected_column_header: Bank Rate + assertion: source_projection diff --git a/packages/ons/families_households_2025/source_package.yaml b/packages/ons/families_households_2025/source_package.yaml index 4af5f86c..5b2553e2 100644 --- a/packages/ons/families_households_2025/source_package.yaml +++ b/packages/ons/families_households_2025/source_package.yaml @@ -5,10 +5,10 @@ # production fixture are enumerated at the populace consumption gate. schema_version: ledger.source_package.v1 package_id: ons-families-households-2025 -label: 'ONS families and households 2025, Table 7: UK households by household type, 2018-2025' +label: 'ONS families and households 2025, Tables 5 and 7: UK household size and households by household type, 2018-2025' artifact: source_name: ons - source_table: 'Families and households in the UK 2025, Table 7: households by type' + source_table: 'Families and households in the UK 2025, Tables 5 and 7: household size and households by type' resource_package: db resource_directory: data/ons/families_households_2025 manifest: manifest.yaml @@ -1426,3 +1426,683 @@ record_sets: aggregation: sum value_scale: 1000 expected_cell_type: number +- record_set_id: ons.families_households_2025.table5.all_households.cy2018 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.all_households.cy2018.v1 + source_record_id_prefix: ons.families_households_2025.table5.all_households.cy2018 + sheet_name: '5' + period_type: calendar_year + period: 2018 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: all_households + label: All households + ordinal: 0 + row_number: 21 + expected_row_header_column: A + expected_row_header: All households + table_record_kind: total + measures: + - measure_id: households_total + label: All households, 2018 estimate + ordinal: 0 + column: W + source_column_id: households_total + concept: ons.households_total + source_concept: ons.families_households_table5.all_households + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 all households + row, thousands. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000 + expected_column_header_row: 12 + expected_column_header: 2018 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.average_household_size.cy2018 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.average_household_size.cy2018.v1 + source_record_id_prefix: ons.families_households_2025.table5.average_household_size.cy2018 + sheet_name: '5' + period_type: calendar_year + period: 2018 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: average_household_size + label: Average household size (number of people) + ordinal: 0 + row_number: 22 + expected_row_header_column: A + expected_row_header: Average household size (number of people) + table_record_kind: total + measures: + - measure_id: average_household_size + label: Average household size, 2018 estimate + ordinal: 0 + column: W + source_column_id: average_household_size + concept: ons.average_household_size + source_concept: ons.families_households_table5.average_household_size + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 average + household size row, number of people. + unit: people_per_household + aggregation: sum + expected_cell_type: number + expected_column_header_row: 12 + expected_column_header: 2018 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.all_households.cy2019 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.all_households.cy2019.v1 + source_record_id_prefix: ons.families_households_2025.table5.all_households.cy2019 + sheet_name: '5' + period_type: calendar_year + period: 2019 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: all_households + label: All households + ordinal: 0 + row_number: 21 + expected_row_header_column: A + expected_row_header: All households + table_record_kind: total + measures: + - measure_id: households_total + label: All households, 2019 estimate + ordinal: 0 + column: T + source_column_id: households_total + concept: ons.households_total + source_concept: ons.families_households_table5.all_households + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 all households + row, thousands. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000 + expected_column_header_row: 12 + expected_column_header: 2019 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.average_household_size.cy2019 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.average_household_size.cy2019.v1 + source_record_id_prefix: ons.families_households_2025.table5.average_household_size.cy2019 + sheet_name: '5' + period_type: calendar_year + period: 2019 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: average_household_size + label: Average household size (number of people) + ordinal: 0 + row_number: 22 + expected_row_header_column: A + expected_row_header: Average household size (number of people) + table_record_kind: total + measures: + - measure_id: average_household_size + label: Average household size, 2019 estimate + ordinal: 0 + column: T + source_column_id: average_household_size + concept: ons.average_household_size + source_concept: ons.families_households_table5.average_household_size + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 average + household size row, number of people. + unit: people_per_household + aggregation: sum + expected_cell_type: number + expected_column_header_row: 12 + expected_column_header: 2019 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.all_households.cy2020 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.all_households.cy2020.v1 + source_record_id_prefix: ons.families_households_2025.table5.all_households.cy2020 + sheet_name: '5' + period_type: calendar_year + period: 2020 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: all_households + label: All households + ordinal: 0 + row_number: 21 + expected_row_header_column: A + expected_row_header: All households + table_record_kind: total + measures: + - measure_id: households_total + label: All households, 2020 estimate + ordinal: 0 + column: Q + source_column_id: households_total + concept: ons.households_total + source_concept: ons.families_households_table5.all_households + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 all households + row, thousands. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000 + expected_column_header_row: 12 + expected_column_header: 2020 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.average_household_size.cy2020 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.average_household_size.cy2020.v1 + source_record_id_prefix: ons.families_households_2025.table5.average_household_size.cy2020 + sheet_name: '5' + period_type: calendar_year + period: 2020 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: average_household_size + label: Average household size (number of people) + ordinal: 0 + row_number: 22 + expected_row_header_column: A + expected_row_header: Average household size (number of people) + table_record_kind: total + measures: + - measure_id: average_household_size + label: Average household size, 2020 estimate + ordinal: 0 + column: Q + source_column_id: average_household_size + concept: ons.average_household_size + source_concept: ons.families_households_table5.average_household_size + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 average + household size row, number of people. + unit: people_per_household + aggregation: sum + expected_cell_type: number + expected_column_header_row: 12 + expected_column_header: 2020 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.all_households.cy2021 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.all_households.cy2021.v1 + source_record_id_prefix: ons.families_households_2025.table5.all_households.cy2021 + sheet_name: '5' + period_type: calendar_year + period: 2021 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: all_households + label: All households + ordinal: 0 + row_number: 21 + expected_row_header_column: A + expected_row_header: All households + table_record_kind: total + measures: + - measure_id: households_total + label: All households, 2021 estimate + ordinal: 0 + column: N + source_column_id: households_total + concept: ons.households_total + source_concept: ons.families_households_table5.all_households + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 all households + row, thousands. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000 + expected_column_header_row: 12 + expected_column_header: 2021 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.average_household_size.cy2021 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.average_household_size.cy2021.v1 + source_record_id_prefix: ons.families_households_2025.table5.average_household_size.cy2021 + sheet_name: '5' + period_type: calendar_year + period: 2021 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: average_household_size + label: Average household size (number of people) + ordinal: 0 + row_number: 22 + expected_row_header_column: A + expected_row_header: Average household size (number of people) + table_record_kind: total + measures: + - measure_id: average_household_size + label: Average household size, 2021 estimate + ordinal: 0 + column: N + source_column_id: average_household_size + concept: ons.average_household_size + source_concept: ons.families_households_table5.average_household_size + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 average + household size row, number of people. + unit: people_per_household + aggregation: sum + expected_cell_type: number + expected_column_header_row: 12 + expected_column_header: 2021 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.all_households.cy2022 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.all_households.cy2022.v1 + source_record_id_prefix: ons.families_households_2025.table5.all_households.cy2022 + sheet_name: '5' + period_type: calendar_year + period: 2022 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: all_households + label: All households + ordinal: 0 + row_number: 21 + expected_row_header_column: A + expected_row_header: All households + table_record_kind: total + measures: + - measure_id: households_total + label: All households, 2022 estimate + ordinal: 0 + column: K + source_column_id: households_total + concept: ons.households_total + source_concept: ons.families_households_table5.all_households + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 all households + row, thousands. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000 + expected_column_header_row: 12 + expected_column_header: 2022 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.average_household_size.cy2022 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.average_household_size.cy2022.v1 + source_record_id_prefix: ons.families_households_2025.table5.average_household_size.cy2022 + sheet_name: '5' + period_type: calendar_year + period: 2022 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: average_household_size + label: Average household size (number of people) + ordinal: 0 + row_number: 22 + expected_row_header_column: A + expected_row_header: Average household size (number of people) + table_record_kind: total + measures: + - measure_id: average_household_size + label: Average household size, 2022 estimate + ordinal: 0 + column: K + source_column_id: average_household_size + concept: ons.average_household_size + source_concept: ons.families_households_table5.average_household_size + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 average + household size row, number of people. + unit: people_per_household + aggregation: sum + expected_cell_type: number + expected_column_header_row: 12 + expected_column_header: 2022 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.all_households.cy2023 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.all_households.cy2023.v1 + source_record_id_prefix: ons.families_households_2025.table5.all_households.cy2023 + sheet_name: '5' + period_type: calendar_year + period: 2023 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: all_households + label: All households + ordinal: 0 + row_number: 21 + expected_row_header_column: A + expected_row_header: All households + table_record_kind: total + measures: + - measure_id: households_total + label: All households, 2023 estimate + ordinal: 0 + column: H + source_column_id: households_total + concept: ons.households_total + source_concept: ons.families_households_table5.all_households + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 all households + row, thousands. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000 + expected_column_header_row: 12 + expected_column_header: 2023 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.average_household_size.cy2023 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.average_household_size.cy2023.v1 + source_record_id_prefix: ons.families_households_2025.table5.average_household_size.cy2023 + sheet_name: '5' + period_type: calendar_year + period: 2023 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: average_household_size + label: Average household size (number of people) + ordinal: 0 + row_number: 22 + expected_row_header_column: A + expected_row_header: Average household size (number of people) + table_record_kind: total + measures: + - measure_id: average_household_size + label: Average household size, 2023 estimate + ordinal: 0 + column: H + source_column_id: average_household_size + concept: ons.average_household_size + source_concept: ons.families_households_table5.average_household_size + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 average + household size row, number of people. + unit: people_per_household + aggregation: sum + expected_cell_type: number + expected_column_header_row: 12 + expected_column_header: 2023 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.all_households.cy2024 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.all_households.cy2024.v1 + source_record_id_prefix: ons.families_households_2025.table5.all_households.cy2024 + sheet_name: '5' + period_type: calendar_year + period: 2024 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: all_households + label: All households + ordinal: 0 + row_number: 21 + expected_row_header_column: A + expected_row_header: All households + table_record_kind: total + measures: + - measure_id: households_total + label: All households, 2024 estimate + ordinal: 0 + column: E + source_column_id: households_total + concept: ons.households_total + source_concept: ons.families_households_table5.all_households + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 all households + row, thousands. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000 + expected_column_header_row: 12 + expected_column_header: 2024 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.average_household_size.cy2024 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.average_household_size.cy2024.v1 + source_record_id_prefix: ons.families_households_2025.table5.average_household_size.cy2024 + sheet_name: '5' + period_type: calendar_year + period: 2024 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: average_household_size + label: Average household size (number of people) + ordinal: 0 + row_number: 22 + expected_row_header_column: A + expected_row_header: Average household size (number of people) + table_record_kind: total + measures: + - measure_id: average_household_size + label: Average household size, 2024 estimate + ordinal: 0 + column: E + source_column_id: average_household_size + concept: ons.average_household_size + source_concept: ons.families_households_table5.average_household_size + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 average + household size row, number of people. + unit: people_per_household + aggregation: sum + expected_cell_type: number + expected_column_header_row: 12 + expected_column_header: 2024 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.all_households.cy2025 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.all_households.cy2025.v1 + source_record_id_prefix: ons.families_households_2025.table5.all_households.cy2025 + sheet_name: '5' + period_type: calendar_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: all_households + label: All households + ordinal: 0 + row_number: 21 + expected_row_header_column: A + expected_row_header: All households + table_record_kind: total + measures: + - measure_id: households_total + label: All households, 2025 estimate + ordinal: 0 + column: B + source_column_id: households_total + concept: ons.households_total + source_concept: ons.families_households_table5.all_households + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 all households + row, thousands. + unit: count + aggregation: sum + expected_cell_type: number + value_scale: 1000 + expected_column_header_row: 12 + expected_column_header: 2025 Estimate + survey_instrument: Labour Force Survey +- record_set_id: ons.families_households_2025.table5.average_household_size.cy2025 + provenance_class: survey_aggregate + record_set_spec_id: ons.families_households_2025.table5.average_household_size.cy2025.v1 + source_record_id_prefix: ons.families_households_2025.table5.average_household_size.cy2025 + sheet_name: '5' + period_type: calendar_year + period: 2025 + geography_id: K02000001 + geography_level: country + geography_name: United Kingdom + geography_vintage: current + entity: household + entity_role: resident_household + domain: household_composition + groupby_dimension: ons.household_size_summary + rows: + - value_id: average_household_size + label: Average household size (number of people) + ordinal: 0 + row_number: 22 + expected_row_header_column: A + expected_row_header: Average household size (number of people) + table_record_kind: total + measures: + - measure_id: average_household_size + label: Average household size, 2025 estimate + ordinal: 0 + column: B + source_column_id: average_household_size + concept: ons.average_household_size + source_concept: ons.families_households_table5.average_household_size + concept_relation: source_label + concept_authority: ons + concept_evidence_url: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/families/datasets/familiesandhouseholdsfamiliesandhouseholds + concept_evidence_notes: Families and households in the UK, 2025 edition, Table 5 average + household size row, number of people. + unit: people_per_household + aggregation: sum + expected_cell_type: number + expected_column_header_row: 12 + expected_column_header: 2025 Estimate + survey_instrument: Labour Force Survey diff --git a/policyengine_chronicle/consumer.py b/policyengine_chronicle/consumer.py index 01fec4b7..07bf2ef2 100644 --- a/policyengine_chronicle/consumer.py +++ b/policyengine_chronicle/consumer.py @@ -1117,10 +1117,7 @@ def _load_profiles( profile_paths: Sequence[str | Path], ) -> dict[str, dict[str, Any]]: if not profile_ids and not profile_paths: - raise ValueError( - "Consumer artifacts need at least one target profile " - "(profile_ids or profile_paths)." - ) + return {} payloads: dict[str, dict[str, Any]] = {} for profile_id in profile_ids: profile = load_target_profile(profile_id) diff --git a/tests/test_chronicle_boundaries.py b/tests/test_chronicle_boundaries.py index b5259b46..68352a12 100644 --- a/tests/test_chronicle_boundaries.py +++ b/tests/test_chronicle_boundaries.py @@ -38,3 +38,76 @@ def test_repository_does_not_ship_raw_microdata_namespace(): assert not (repo_root / "micro").exists() assert not (repo_root / "calibration").exists() assert not (repo_root / "storage").exists() + + +def test_uk_legacy_etl_cleanup_checklist_accounts_for_legacy_series(): + repo_root = Path(__file__).resolve().parents[1] + checklist = (repo_root / "docs" / "pe-uk-source-checklist.md").read_text() + + required_entries = [ + "`db/etl_obr.py` `gdp`", + "`db/etl_obr.py` `total_receipts`", + "`db/etl_obr.py` `total_managed_expenditure`", + "`db/etl_obr.py` `public_sector_net_borrowing`", + "`db/etl_obr.py` `public_sector_net_debt`", + "`db/etl_obr.py` `real_gdp_growth`", + "`db/etl_obr.py` `unemployment_rate`", + "`db/etl_obr.py` `cpi_inflation`", + "`db/etl_obr.py` `rpi_inflation`", + "`db/etl_obr.py` `bank_rate`", + "`db/etl_obr.py` `employment`", + "`db/etl_ons.py` `population_total`", + "`db/etl_ons.py` `population_age_0_15`", + "`db/etl_ons.py` `population_age_16_64`", + "`db/etl_ons.py` `population_age_65_plus`", + "`db/etl_ons.py` `households_total`", + "`db/etl_ons.py` `average_household_size`", + "`db/etl_hmrc.py` `income_tax`", + "`db/etl_hmrc.py` `national_insurance`", + "`db/etl_hmrc.py` `capital_gains_tax`", + "`db/etl_hmrc.py` `inheritance_tax`", + "`db/etl_hmrc.py` `taxpayers`, `higher_rate_taxpayers`, `additional_rate_taxpayers`", + "`db/etl_hmrc.py` `total_income`", + "`db/etl_hmrc.py` `benefits.universal_credit.{recipients,expenditure}`", + "`db/etl_hmrc.py` `benefits.child_benefit.{recipients,expenditure}`", + "`db/etl_hmrc.py` `benefits.state_pension.{recipients,expenditure}`", + "`db/etl_hmrc.py` `benefits.housing_benefit.{recipients,expenditure}`", + "`db/etl_hmrc.py` `benefits.pension_credit.{recipients,expenditure}`", + ] + + for entry in required_entries: + assert entry in checklist + + assert "obr-efo-economy-march-2026" in checklist + assert "obr-efo-aggregates-march-2026" in checklist + assert "ons-families-households-2025" in checklist + assert "2026-2034 remain gaps" in checklist + assert "Explicit source gap or non-equivalence" in checklist + assert "assertion" in checklist + assert "period_type" in checklist + assert "2023 through 2026" in checklist + + +def test_retired_uk_legacy_etl_modules_are_not_shipped(): + repo_root = Path(__file__).resolve().parents[1] + + assert not (repo_root / "db" / "etl_obr.py").exists() + assert not (repo_root / "db" / "etl_ons.py").exists() + assert not (repo_root / "tests" / "test_etl_obr.py").exists() + assert not (repo_root / "tests" / "test_etl_ons.py").exists() + assert not (repo_root / "db" / "etl_hmrc.py").exists() + assert not (repo_root / "tests" / "test_etl_hmrc.py").exists() + + +def test_load_cli_no_longer_accepts_retired_uk_legacy_etl_sources(): + cli_source = (Path(__file__).resolve().parents[1] / "db" / "cli.py").read_text() + + assert 'if args.source == "obr"' not in cli_source + assert 'if args.source == "ons"' not in cli_source + assert 'if args.source == "hmrc"' not in cli_source + assert "load_obr_targets" not in cli_source + assert "load_ons_targets" not in cli_source + assert "load_hmrc_targets" not in cli_source + assert '"obr",' not in cli_source + assert '"ons",' not in cli_source + assert '"hmrc",' not in cli_source diff --git a/tests/test_chronicle_bundle.py b/tests/test_chronicle_bundle.py index 34626840..d27d6532 100644 --- a/tests/test_chronicle_bundle.py +++ b/tests/test_chronicle_bundle.py @@ -4,7 +4,8 @@ import json -from chronicle.bundle import build_bundle, build_bundle_coverage +from chronicle.bundle import UK_BUNDLE_SOURCES, build_bundle, build_bundle_coverage +from chronicle.harness import build_bundle_dir from chronicle.harness import main as harness_main @@ -12,6 +13,51 @@ def _load_jsonl(path): return [json.loads(line) for line in path.read_text().splitlines() if line] +def test_build_bundle_dir_uk_suite_uses_curated_sources(tmp_path, monkeypatch): + captured = {} + + class FakeReport: + valid = True + + def to_dict(self): + return {"valid": True} + + def fake_build_bundle(output_dir, **kwargs): + captured["output_dir"] = output_dir + captured.update(kwargs) + return FakeReport() + + monkeypatch.setattr("chronicle.harness.build_bundle", fake_build_bundle) + + report = build_bundle_dir(tmp_path / "bundle", year=2023, suite="uk") + + assert report.valid + assert tuple(captured["sources"]) == UK_BUNDLE_SOURCES + assert captured["output_dir"] == tmp_path / "bundle" + + +def test_build_bundle_cli_accepts_uk_suite(tmp_path, monkeypatch): + captured = {} + + class FakeReport: + valid = True + + def to_dict(self): + return {"valid": True} + + def fake_build_bundle_dir(output_dir, **kwargs): + captured["output_dir"] = output_dir + captured.update(kwargs) + return FakeReport() + + monkeypatch.setattr("chronicle.harness.build_bundle_dir", fake_build_bundle_dir) + + status = harness_main(["build-bundle", "--suite", "uk", "--out", str(tmp_path)]) + + assert status == 0 + assert captured["suite"] == "uk" + + def test_build_bundle_writes_merged_consumer_contract(tmp_path): output_dir = tmp_path / "bundle" @@ -27,16 +73,16 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "aggregate_duplicate_key_count": 0, "entity_count": 11, "error_count": 0, - "fact_count": 155431, + "fact_count": 155504, "geography_count": 12536, "period_count": 151, "semantic_duplicate_key_count": 12, "skipped_source_count": 10, "source_count": 41, - "source_package_count": 128, + "source_package_count": 130, "warning_count": 1, } - assert len(rows) == 155431 + assert len(rows) == 155504 assert {row["provenance_class"] for row in rows} <= { "administrative", "census", @@ -54,7 +100,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): ) assert rows[0]["aggregate_fact_key"].startswith("ledger.aggregate_fact.v2:") assert rows[0]["semantic_fact_key"].startswith("ledger.semantic_fact.v2:") - assert source_packages["source_package_count"] == 128 + assert source_packages["source_package_count"] == 130 assert source_packages["skipped_source_count"] == 10 assert sorted(item["source"] for item in source_packages["skipped_sources"]) == [ "census-acs-s0101-congressional-district-age-2024", @@ -68,7 +114,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "jct-obbba-revenue-estimates-2025", "jct-tax-expenditures-2024", ] - assert coverage["fact_count"] == 155431 + assert coverage["fact_count"] == 155504 assert coverage["counts"]["by_source"] == { "bea": 445, "bfp_economic_outlook": 5, @@ -96,9 +142,9 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "nbb_national_accounts": 1, "nisra": 510, "nrs": 5589, - "obr": 196, + "obr": 253, "onem_rva_unemployment": 1, - "ons": 65630, + "ons": 65646, "onss_contributions": 1, "opgroeien_groeipakket": 11, "scotgov": 2504, @@ -113,7 +159,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "welshgov": 198, } table_counts = coverage["counts"]["by_source_table"] - assert len(table_counts) == 123 + assert len(table_counts) == 125 assert table_counts["usda_snap:SNAP FY2025 Monthly State Participation"] == 636 assert table_counts["irs_soi:Congressional District Data 2022"] == 26880 assert table_counts["irs_soi:IRS SOI County Data 2022"] == 6286 @@ -315,18 +361,18 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "calendar_year:2015": 74, "calendar_year:2016": 72, "calendar_year:2017": 72, - "calendar_year:2018": 84, - "calendar_year:2019": 83, - "calendar_year:2020": 83, - "calendar_year:2021": 3990, - "calendar_year:2022": 1911, - "calendar_year:2023": 6200, - "calendar_year:2024": 33791, - "calendar_year:2025": 4438, - "calendar_year:2026": 235, - "calendar_year:2027": 214, - "calendar_year:2028": 214, - "calendar_year:2029": 214, + "calendar_year:2018": 86, + "calendar_year:2019": 85, + "calendar_year:2020": 85, + "calendar_year:2021": 3992, + "calendar_year:2022": 1913, + "calendar_year:2023": 6202, + "calendar_year:2024": 33799, + "calendar_year:2025": 4446, + "calendar_year:2026": 241, + "calendar_year:2027": 220, + "calendar_year:2028": 220, + "calendar_year:2029": 220, "calendar_year:2031": 2, "fiscal_year:1996": 33, "fiscal_year:1997": 33, @@ -356,12 +402,12 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "fiscal_year:2021": 33, "fiscal_year:2022": 33, "fiscal_year:2023": 385, - "fiscal_year:2024": 610, - "fiscal_year:2025": 1284, - "fiscal_year:2026": 1439, - "fiscal_year:2027": 28, - "fiscal_year:2028": 28, - "fiscal_year:2029": 28, + "fiscal_year:2024": 611, + "fiscal_year:2025": 1288, + "fiscal_year:2026": 1443, + "fiscal_year:2027": 32, + "fiscal_year:2028": 32, + "fiscal_year:2029": 32, "fiscal_year:2030": 28, "month:2023-01": 1, "month:2023-12": 6, @@ -435,7 +481,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): assert ( coverage["counts"]["by_geography"]["congressional_district:5001700US0601"] == 56 ) - assert coverage["counts"]["by_geography"]["country:K02000001"] == 4189 + assert coverage["counts"]["by_geography"]["country:K02000001"] == 4262 assert coverage["counts"]["by_geography"]["country:K03000001"] == 312 assert len(coverage["counts"]["by_geography"]) == 12536 assert coverage["counts"]["by_entity"] == { @@ -443,8 +489,8 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "dwelling": 12708, "family": 107, "firm": 1439, - "government": 237, - "household": 40432, + "government": 294, + "household": 40448, "institutional_sector": 103, "pension_plan": 2, "person": 60272, diff --git a/tests/test_chronicle_consumer.py b/tests/test_chronicle_consumer.py index d3b37799..325ef726 100644 --- a/tests/test_chronicle_consumer.py +++ b/tests/test_chronicle_consumer.py @@ -464,13 +464,25 @@ def test_artifact_load_rejects_malformed_provenance(tmp_path, case, message): load_consumer_artifact(out_dir) -def test_artifact_requires_profiles(tmp_path): +def test_artifact_facts_only_round_trips(tmp_path): facts_path, _ = _write_artifact_inputs(tmp_path) - with pytest.raises(ValueError, match="at least one target profile"): - build_consumer_artifact( - tmp_path / "artifact", - facts_path=facts_path, - ) + out_dir = tmp_path / "artifact" + + report = build_consumer_artifact( + out_dir, + facts_path=facts_path, + ) + artifact = load_consumer_artifact(out_dir) + manifest = json.loads((out_dir / "manifest.json").read_text()) + + assert report.fact_row_count == 3 + assert report.profile_ids == () + assert report.coverage == {} + assert manifest["profiles"] == {} + assert (out_dir / "profiles").is_dir() + assert list((out_dir / "profiles").iterdir()) == [] + assert len(artifact.rows) == 3 + assert artifact.profiles == {} def test_artifact_is_reproducible(tmp_path): diff --git a/tests/test_chronicle_source_package.py b/tests/test_chronicle_source_package.py index 7fec3999..99d880bc 100644 --- a/tests/test_chronicle_source_package.py +++ b/tests/test_chronicle_source_package.py @@ -168,6 +168,63 @@ def test_record_set_provenance_fields_propagate_to_specs_and_facts(): assert survey_spec.survey_instrument == "ACS 1-year" +def test_hmrc_packages_preserve_provenance_and_definition_year_metadata(): + """HMRC facts must retain the axes needed to distinguish source claims.""" + hmrc_packages = sorted( + (REPO_ROOT / "packages" / "hmrc").glob("*/source_package.yaml") + ) + + assert hmrc_packages + for path in hmrc_packages: + payload = yaml.safe_load(path.read_text()) + artifact = payload["artifact"] + assert artifact["vintage"] + assert isinstance(artifact["artifact_year"], int) + for record_set in payload["record_sets"]: + assert record_set["period_type"] + assert record_set["period"] is not None + assert record_set["record_set_spec_id"] + assert record_set["provenance_class"] + for measure in record_set["measures"]: + assert measure["concept"] + assert measure.get("source_concept", measure["concept"]) + assert measure["unit"] + + +def test_hmrc_cgt_reuses_one_publisher_series_across_definition_years(): + """CGT years are facts in one package, not competing hardcoded targets.""" + package = load_source_package("hmrc-cgt-statistics-2025") + facts = [ + fact + for fact in package.build_facts(2023) + if fact.measure.concept == "hmrc.cgt_tax_total" + ] + + assert {fact.period.type for fact in facts} == {"tax_year"} + assert {fact.period.value for fact in facts} >= {2021, 2022, 2023} + assert {fact.assertion for fact in facts} == {"observation"} + assert all(fact.source.source_name == "hmrc" for fact in facts) + assert all(fact.measure.source_concept == "hmrc.cgt_tax_total" for fact in facts) + assert len( + { + ( + fact.measure.concept, + fact.measure.source_concept, + fact.measure.unit, + fact.period.type, + fact.period.value, + fact.geography.level, + fact.geography.id, + fact.entity.name, + fact.entity.role, + fact.assertion, + fact.layout.record_set_spec_id, + ) + for fact in facts + } + ) == len(facts) + + def test_every_source_package_record_set_declares_provenance_class(): missing: list[str] = [] malformed: list[str] = [] @@ -3463,6 +3520,124 @@ def test_kff_marketplace_effectuated_enrollment_builds_2024_state_facts(): assert values_by_record[ca].value == 1_795_695 +def test_obr_efo_economy_package_validates_macro_series_counts(): + report = validate_source_package("obr-efo-economy-march-2026", year=2026) + + assert report.valid + assert report.counts == { + "record_set_count": 36, + "row_count": 36, + "measure_count": 36, + "source_record_count": 36, + "source_region_count": 36, + } + + +def test_obr_efo_economy_package_builds_cell_faithful_macro_facts(): + package = load_source_package("obr-efo-economy-march-2026") + cells = package.build_source_cells(2026) + facts = package.build_facts(2026, cells=cells) + values_by_record = {fact.source_record_id: fact for fact in facts} + + assert validate_source_cells(cells).valid + assert validate_facts(facts).valid + assert len(facts) == 36 + + nominal_gdp_2024 = values_by_record[ + "obr.efo_2026_03.economy.nominal_gdp_nsa.cy2024." + "nominal_gdp_nsa.nominal_gdp_nsa" + ] + cpi_2025 = values_by_record[ + "obr.efo_2026_03.economy.cpi_inflation.cy2025." + "cpi_inflation.cpi_inflation" + ] + bank_rate_2029 = values_by_record[ + "obr.efo_2026_03.economy.bank_rate.cy2029.bank_rate.bank_rate" + ] + + assert nominal_gdp_2024.value == 2_890_664_000_000 + assert nominal_gdp_2024.period.type == "calendar_year" + assert nominal_gdp_2024.assertion == "observation" + assert nominal_gdp_2024.measure.concept == "obr.nominal_gdp_nsa" + assert cpi_2025.value == pytest.approx(3.3730368724933735) + assert cpi_2025.assertion == "source_projection" + assert bank_rate_2029.value == pytest.approx(3.741552661678499) + + +def test_obr_efo_aggregates_package_validates_fiscal_series_counts(): + report = validate_source_package("obr-efo-aggregates-march-2026", year=2026) + + assert report.valid + assert report.counts == { + "record_set_count": 21, + "row_count": 21, + "measure_count": 21, + "source_record_count": 21, + "source_region_count": 21, + } + + +def test_obr_efo_aggregates_package_builds_cell_faithful_fiscal_facts(): + package = load_source_package("obr-efo-aggregates-march-2026") + cells = package.build_source_cells(2026) + facts = package.build_facts(2026, cells=cells) + values_by_record = {fact.source_record_id: fact for fact in facts} + + assert validate_source_cells(cells).valid + assert validate_facts(facts).valid + assert len(facts) == 21 + + psnb_2025 = values_by_record[ + "obr.efo_2026_03.aggregates.public_sector_net_borrowing.fy2025." + "public_sector_net_borrowing.public_sector_net_borrowing" + ] + psnd_2024 = values_by_record[ + "obr.efo_2026_03.aggregates.public_sector_net_debt_percent_gdp.fy2024." + "public_sector_net_debt_percent_gdp.public_sector_net_debt_percent_gdp" + ] + + assert psnb_2025.value == pytest.approx(132_735_075_755.52568) + assert psnb_2025.period.type == "fiscal_year" + assert psnb_2025.assertion == "source_projection" + assert psnb_2025.measure.concept == "obr.public_sector_net_borrowing" + assert psnd_2024.value == pytest.approx(93.16136288446387) + assert psnd_2024.measure.unit == "percent_of_gdp" + assert psnd_2024.assertion == "observation" + + +def test_ons_families_households_package_adds_household_totals_and_size(): + report = validate_source_package("ons-families-households-2025", year=2025) + + assert report.valid + assert report.counts == { + "record_set_count": 24, + "row_count": 96, + "measure_count": 24, + "source_record_count": 96, + "source_region_count": 24, + } + + package = load_source_package("ons-families-households-2025") + cells = package.build_source_cells(2025) + facts = package.build_facts(2025, cells=cells) + values_by_record = {fact.source_record_id: fact for fact in facts} + + households_2025 = values_by_record[ + "ons.families_households_2025.table5.all_households.cy2025." + "all_households.households_total" + ] + average_size_2025 = values_by_record[ + "ons.families_households_2025.table5.average_household_size.cy2025." + "average_household_size.average_household_size" + ] + + assert households_2025.value == 29_003_000 + assert households_2025.measure.concept == "ons.households_total" + assert average_size_2025.value == pytest.approx(2.36) + assert average_size_2025.measure.concept == "ons.average_household_size" + assert average_size_2025.measure.unit == "people_per_household" + + def test_render_string_templates_integer_year_with_filing_year(): """Integer years must still template both ``{year}`` and ``{filing_year}``.""" template = "Tax Year {year} (Filing Year {filing_year})" diff --git a/tests/test_etl_hmrc.py b/tests/test_etl_hmrc.py deleted file mode 100644 index 2a653830..00000000 --- a/tests/test_etl_hmrc.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Tests for UK HMRC ETL.""" - -import tempfile -from pathlib import Path - -import pytest -from sqlmodel import Session, select - -from db.schema import ( - DataSource, - Jurisdiction, - Stratum, - Target, - TargetType, - init_db, -) - - -@pytest.fixture -def temp_db(): - """Create a temporary database for testing.""" - with tempfile.TemporaryDirectory() as tmpdir: - db_path = Path(tmpdir) / "test_hmrc.db" - engine = init_db(db_path) - yield engine - - -class TestHmrcETL: - """Tests for HMRC ETL loader.""" - - def test_load_hmrc_creates_national_stratum(self, temp_db): - """Loading HMRC data should create a UK national stratum.""" - from db.etl_hmrc import load_hmrc_targets - - with Session(temp_db) as session: - load_hmrc_targets(session, years=[2022]) - - national = session.exec( - select(Stratum).where(Stratum.name == "UK All Taxpayers") - ).first() - - assert national is not None - assert national.jurisdiction == Jurisdiction.UK - - def test_load_hmrc_creates_income_tax_targets(self, temp_db): - """Loading HMRC should create income tax revenue targets.""" - from db.etl_hmrc import load_hmrc_targets, HMRC_DATA - - with Session(temp_db) as session: - load_hmrc_targets(session, years=[2022]) - - national = session.exec( - select(Stratum).where(Stratum.name == "UK All Taxpayers") - ).first() - - income_tax = session.exec( - select(Target) - .where(Target.stratum_id == national.id) - .where(Target.variable == "income_tax") - .where(Target.period == 2022) - ).first() - - assert income_tax is not None - assert income_tax.value == HMRC_DATA[2022]["income_tax"] - assert income_tax.target_type == TargetType.AMOUNT - assert income_tax.source == DataSource.HMRC - - def test_load_hmrc_creates_ni_targets(self, temp_db): - """Loading HMRC should create National Insurance targets.""" - from db.etl_hmrc import load_hmrc_targets, HMRC_DATA - - with Session(temp_db) as session: - load_hmrc_targets(session, years=[2022]) - - national = session.exec( - select(Stratum).where(Stratum.name == "UK All Taxpayers") - ).first() - - ni = session.exec( - select(Target) - .where(Target.stratum_id == national.id) - .where(Target.variable == "national_insurance") - .where(Target.period == 2022) - ).first() - - assert ni is not None - assert ni.value == HMRC_DATA[2022]["national_insurance"] - - def test_load_hmrc_creates_taxpayer_count(self, temp_db): - """Loading HMRC should create taxpayer count targets.""" - from db.etl_hmrc import load_hmrc_targets, HMRC_DATA - - with Session(temp_db) as session: - load_hmrc_targets(session, years=[2022]) - - national = session.exec( - select(Stratum).where(Stratum.name == "UK All Taxpayers") - ).first() - - count = session.exec( - select(Target) - .where(Target.stratum_id == national.id) - .where(Target.variable == "taxpayer_count") - .where(Target.period == 2022) - ).first() - - assert count is not None - assert count.value == HMRC_DATA[2022]["taxpayers"] - assert count.target_type == TargetType.COUNT - - def test_load_hmrc_creates_benefit_recipients(self, temp_db): - """Loading HMRC should create benefit recipient targets.""" - from db.etl_hmrc import load_hmrc_targets - - with Session(temp_db) as session: - load_hmrc_targets(session, years=[2022]) - - uc_stratum = session.exec( - select(Stratum).where(Stratum.name == "UK Universal Credit Recipients") - ).first() - - assert uc_stratum is not None - - uc_count = session.exec( - select(Target) - .where(Target.stratum_id == uc_stratum.id) - .where(Target.variable == "universal_credit_recipients") - ).first() - - assert uc_count is not None - assert uc_count.target_type == TargetType.COUNT - - def test_load_hmrc_creates_benefit_expenditure(self, temp_db): - """Loading HMRC should create benefit expenditure targets.""" - from db.etl_hmrc import load_hmrc_targets, HMRC_DATA - - with Session(temp_db) as session: - load_hmrc_targets(session, years=[2022]) - - uc_stratum = session.exec( - select(Stratum).where(Stratum.name == "UK Universal Credit Recipients") - ).first() - - uc_spend = session.exec( - select(Target) - .where(Target.stratum_id == uc_stratum.id) - .where(Target.variable == "universal_credit_expenditure") - ).first() - - assert uc_spend is not None - assert ( - uc_spend.value - == HMRC_DATA[2022]["benefits"]["universal_credit"]["expenditure"] - ) - assert uc_spend.target_type == TargetType.AMOUNT - - def test_load_multiple_years(self, temp_db): - """Loading multiple years should create targets for each.""" - from db.etl_hmrc import load_hmrc_targets - - with Session(temp_db) as session: - load_hmrc_targets(session, years=[2021, 2022]) - - national = session.exec( - select(Stratum).where(Stratum.name == "UK All Taxpayers") - ).first() - - targets = session.exec( - select(Target) - .where(Target.stratum_id == national.id) - .where(Target.variable == "income_tax") - ).all() - - years = {t.period for t in targets} - assert years == {2021, 2022} - - def test_load_hmrc_idempotent(self, temp_db): - """Loading HMRC twice should not duplicate data.""" - from db.etl_hmrc import load_hmrc_targets - - with Session(temp_db) as session: - load_hmrc_targets(session, years=[2022]) - load_hmrc_targets(session, years=[2022]) - - national_strata = session.exec( - select(Stratum).where(Stratum.name == "UK All Taxpayers") - ).all() - - assert len(national_strata) == 1 diff --git a/tests/test_etl_obr.py b/tests/test_etl_obr.py deleted file mode 100644 index 1f6e3055..00000000 --- a/tests/test_etl_obr.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Tests for OBR (Office for Budget Responsibility) projections ETL.""" - -import pytest -from sqlmodel import Session, select - -from db.schema import ( - DataSource, - Jurisdiction, - Stratum, - Target, - TargetType, - init_db, -) -from db.etl_obr import load_obr_targets, OBR_DATA - - -class TestObrETL: - """Tests for OBR projections ETL.""" - - @pytest.fixture - def session(self, tmp_path): - """Create a test database session.""" - db_path = tmp_path / "test.db" - engine = init_db(db_path) - with Session(engine) as session: - yield session - - def test_load_creates_uk_budget_stratum(self, session): - """Should create UK budget stratum.""" - load_obr_targets(session, years=[2024]) - - stratum = session.exec( - select(Stratum).where(Stratum.name == "UK Public Finances") - ).first() - - assert stratum is not None - assert stratum.jurisdiction == Jurisdiction.UK - - def test_load_creates_gdp_projection(self, session): - """Should create UK GDP projection targets.""" - load_obr_targets(session, years=[2024]) - - target = session.exec( - select(Target).where( - Target.variable == "gdp", - Target.period == 2024, - Target.source == DataSource.OBR, - ) - ).first() - - assert target is not None - assert target.value == OBR_DATA[2024]["gdp"] - assert target.target_type == TargetType.AMOUNT - - def test_load_creates_receipts_projection(self, session): - """Should create total receipts projection.""" - load_obr_targets(session, years=[2024]) - - target = session.exec( - select(Target).where( - Target.variable == "total_receipts", - Target.period == 2024, - ) - ).first() - - assert target is not None - assert target.value == OBR_DATA[2024]["total_receipts"] - - def test_load_creates_expenditure_projection(self, session): - """Should create total managed expenditure projection.""" - load_obr_targets(session, years=[2024]) - - target = session.exec( - select(Target).where( - Target.variable == "total_managed_expenditure", - Target.period == 2024, - ) - ).first() - - assert target is not None - assert target.value == OBR_DATA[2024]["total_managed_expenditure"] - - def test_load_creates_borrowing_projection(self, session): - """Should create public sector net borrowing projection.""" - load_obr_targets(session, years=[2024]) - - target = session.exec( - select(Target).where( - Target.variable == "public_sector_net_borrowing", - Target.period == 2024, - ) - ).first() - - assert target is not None - - def test_load_creates_economy_stratum(self, session): - """Should create UK economy stratum for macro indicators.""" - load_obr_targets(session, years=[2024]) - - stratum = session.exec( - select(Stratum).where(Stratum.name == "UK Economy") - ).first() - - assert stratum is not None - assert stratum.jurisdiction == Jurisdiction.UK - - def test_load_creates_unemployment_projection(self, session): - """Should create UK unemployment rate projection.""" - load_obr_targets(session, years=[2025]) - - target = session.exec( - select(Target).where( - Target.variable == "unemployment_rate", - Target.period == 2025, - Target.source == DataSource.OBR, - ) - ).first() - - assert target is not None - assert target.target_type == TargetType.RATE - - def test_load_creates_inflation_projection(self, session): - """Should create CPI inflation projection.""" - load_obr_targets(session, years=[2024]) - - target = session.exec( - select(Target).where( - Target.variable == "cpi_inflation", - Target.period == 2024, - Target.source == DataSource.OBR, - ) - ).first() - - assert target is not None - assert target.target_type == TargetType.RATE - - def test_load_future_years(self, session): - """Should load projection years through 2029.""" - load_obr_targets(session, years=[2028, 2029]) - - targets_2028 = session.exec( - select(Target).where( - Target.period == 2028, - Target.source == DataSource.OBR, - ) - ).all() - targets_2029 = session.exec( - select(Target).where( - Target.period == 2029, - Target.source == DataSource.OBR, - ) - ).all() - - assert len(targets_2028) > 0 - assert len(targets_2029) > 0 - - def test_load_multiple_years(self, session): - """Should load multiple projection years.""" - load_obr_targets(session, years=[2024, 2025, 2026]) - - targets = session.exec( - select(Target).where(Target.source == DataSource.OBR) - ).all() - periods = {t.period for t in targets} - - assert 2024 in periods - assert 2025 in periods - assert 2026 in periods - - def test_load_idempotent(self, session): - """Loading twice should not duplicate data.""" - load_obr_targets(session, years=[2024]) - count1 = len( - session.exec(select(Target).where(Target.source == DataSource.OBR)).all() - ) - - load_obr_targets(session, years=[2024]) - count2 = len( - session.exec(select(Target).where(Target.source == DataSource.OBR)).all() - ) - - assert count1 == count2 diff --git a/tests/test_etl_ons.py b/tests/test_etl_ons.py deleted file mode 100644 index 32a361d5..00000000 --- a/tests/test_etl_ons.py +++ /dev/null @@ -1,205 +0,0 @@ -"""Tests for ONS (Office for National Statistics) projections ETL.""" - -import pytest -from sqlmodel import Session, select - -from db.schema import ( - DataSource, - Jurisdiction, - Stratum, - Target, - TargetType, - init_db, -) -from db.etl_ons import load_ons_targets, ONS_DATA - - -class TestOnsETL: - """Tests for ONS projections ETL.""" - - @pytest.fixture - def session(self, tmp_path): - """Create a test database session.""" - db_path = tmp_path / "test.db" - engine = init_db(db_path) - with Session(engine) as session: - yield session - - def test_load_creates_uk_population_stratum(self, session): - """Should create UK population stratum.""" - load_ons_targets(session, years=[2024]) - - stratum = session.exec( - select(Stratum).where(Stratum.name == "UK Population") - ).first() - - assert stratum is not None - assert stratum.jurisdiction == Jurisdiction.UK - - def test_load_creates_total_population_projection(self, session): - """Should create UK total population projection targets.""" - load_ons_targets(session, years=[2024]) - - target = session.exec( - select(Target).where( - Target.variable == "population_total", - Target.period == 2024, - Target.source == DataSource.ONS, - ) - ).first() - - assert target is not None - assert target.value == ONS_DATA[2024]["population_total"] - assert target.target_type == TargetType.COUNT - assert target.is_preliminary is True - - def test_load_creates_age_group_projections(self, session): - """Should create population projections by age groups.""" - load_ons_targets(session, years=[2024]) - - # Check 0-15 age group - target_0_15 = session.exec( - select(Target).where( - Target.variable == "population_age_0_15", - Target.period == 2024, - ) - ).first() - - assert target_0_15 is not None - assert target_0_15.value == ONS_DATA[2024]["population_age_0_15"] - - # Check 16-64 age group - target_16_64 = session.exec( - select(Target).where( - Target.variable == "population_age_16_64", - Target.period == 2024, - ) - ).first() - - assert target_16_64 is not None - assert target_16_64.value == ONS_DATA[2024]["population_age_16_64"] - - # Check 65+ age group - target_65_plus = session.exec( - select(Target).where( - Target.variable == "population_age_65_plus", - Target.period == 2024, - ) - ).first() - - assert target_65_plus is not None - assert target_65_plus.value == ONS_DATA[2024]["population_age_65_plus"] - - def test_load_creates_uk_households_stratum(self, session): - """Should create UK households stratum.""" - load_ons_targets(session, years=[2024]) - - stratum = session.exec( - select(Stratum).where(Stratum.name == "UK Households") - ).first() - - assert stratum is not None - assert stratum.jurisdiction == Jurisdiction.UK - - def test_load_creates_household_projections(self, session): - """Should create household count projection.""" - load_ons_targets(session, years=[2024]) - - target = session.exec( - select(Target).where( - Target.variable == "households_total", - Target.period == 2024, - Target.source == DataSource.ONS, - ) - ).first() - - assert target is not None - assert target.value == ONS_DATA[2024]["households_total"] - assert target.target_type == TargetType.COUNT - - def test_load_creates_average_household_size(self, session): - """Should create average household size projection.""" - load_ons_targets(session, years=[2024]) - - target = session.exec( - select(Target).where( - Target.variable == "average_household_size", - Target.period == 2024, - Target.source == DataSource.ONS, - ) - ).first() - - assert target is not None - assert target.value == ONS_DATA[2024]["average_household_size"] - assert target.target_type == TargetType.RATE - - def test_load_future_years(self, session): - """Should load projection years through 2034.""" - load_ons_targets(session, years=[2033, 2034]) - - targets_2033 = session.exec( - select(Target).where( - Target.period == 2033, - Target.source == DataSource.ONS, - ) - ).all() - targets_2034 = session.exec( - select(Target).where( - Target.period == 2034, - Target.source == DataSource.ONS, - ) - ).all() - - assert len(targets_2033) > 0 - assert len(targets_2034) > 0 - - def test_load_multiple_years(self, session): - """Should load multiple projection years.""" - load_ons_targets(session, years=[2024, 2025, 2026]) - - targets = session.exec( - select(Target).where(Target.source == DataSource.ONS) - ).all() - periods = {t.period for t in targets} - - assert 2024 in periods - assert 2025 in periods - assert 2026 in periods - - def test_load_idempotent(self, session): - """Loading twice should not duplicate data.""" - load_ons_targets(session, years=[2024]) - count1 = len( - session.exec(select(Target).where(Target.source == DataSource.ONS)).all() - ) - - load_ons_targets(session, years=[2024]) - count2 = len( - session.exec(select(Target).where(Target.source == DataSource.ONS)).all() - ) - - assert count1 == count2 - - def test_all_projections_marked_preliminary(self, session): - """All ONS projections should be marked as preliminary.""" - load_ons_targets(session, years=[2024, 2030]) - - targets = session.exec( - select(Target).where(Target.source == DataSource.ONS) - ).all() - - for target in targets: - assert target.is_preliminary is True - - def test_load_all_years(self, session): - """Should load all available years when years=None.""" - load_ons_targets(session, years=None) - - targets = session.exec( - select(Target).where(Target.source == DataSource.ONS) - ).all() - periods = {t.period for t in targets} - - # Should have data for all years 2024-2034 - expected_years = set(range(2024, 2035)) - assert periods == expected_years diff --git a/tests/test_source_files.py b/tests/test_source_files.py index aad7aafe..c7f00137 100644 --- a/tests/test_source_files.py +++ b/tests/test_source_files.py @@ -3,12 +3,22 @@ from __future__ import annotations import json +import os import zipfile +from types import SimpleNamespace +from unittest.mock import patch +import pytest from sqlmodel import Session, select -from db.pe_source_inventory import pe_source_specs -from db.pe_source_inventory import UK_TARGET_URL_FILES +from db import source_files +from db.cli import cmd_load_source_files +from db.pe_source_inventory import ( + PE_UK_DATA_ROOT_ENV, + PE_US_DATA_ROOT_ENV, + UK_TARGET_URL_FILES, + pe_source_specs, +) from db.schema import ( Jurisdiction, SourceArtifact, @@ -17,7 +27,6 @@ get_engine, init_db, ) -from db import source_files from db.source_files import SourceArtifactSpec, ingest_source_artifact @@ -182,6 +191,84 @@ def test_pe_source_inventory_finds_both_pipeline_roots(tmp_path): assert {spec.source_id for spec in specs} == {"irs-soi", "usda-snap"} +def test_pe_source_inventory_requires_us_root_flag_or_env(): + with patch.dict(os.environ, {}, clear=True), pytest.raises(ValueError) as excinfo: + pe_source_specs(include_us=True, include_uk=False) + + assert "--pe-us-root" in str(excinfo.value) + assert PE_US_DATA_ROOT_ENV in str(excinfo.value) + + +def test_pe_source_inventory_reads_us_root_from_env(tmp_path): + pe_us = tmp_path / "policyengine-us-data" + raw_inputs = ( + pe_us / "policyengine_us_data" / "storage" / "calibration" / "raw_inputs" + ) + raw_inputs.mkdir(parents=True) + (raw_inputs / "irs_soi_sample.csv").write_text("x\n1\n", encoding="utf-8") + + with patch.dict(os.environ, {PE_US_DATA_ROOT_ENV: str(pe_us)}, clear=True): + specs = pe_source_specs(include_us=True, include_uk=False) + + assert [spec.source_id for spec in specs] == ["irs-soi"] + + +def test_pe_source_inventory_scopes_required_roots_to_jurisdiction(tmp_path): + pe_us = tmp_path / "policyengine-us-data" + raw_inputs = ( + pe_us / "policyengine_us_data" / "storage" / "calibration" / "raw_inputs" + ) + raw_inputs.mkdir(parents=True) + (raw_inputs / "irs_soi_sample.csv").write_text("x\n1\n", encoding="utf-8") + + with patch.dict(os.environ, {PE_US_DATA_ROOT_ENV: str(pe_us)}, clear=True): + specs = pe_source_specs(include_us=True, include_uk=False) + + assert specs + + +def test_pe_source_inventory_all_uses_configured_uk_root_without_us_root(tmp_path): + pe_uk = tmp_path / "policyengine-uk-data" + pe_uk.mkdir() + + with patch.dict(os.environ, {PE_UK_DATA_ROOT_ENV: str(pe_uk)}, clear=True): + specs = pe_source_specs(include_us=True, include_uk=True) + + assert specs + assert {spec.jurisdiction for spec in specs} == {Jurisdiction.UK} + + +def test_pe_source_inventory_all_uses_configured_us_root_without_uk_root(tmp_path): + pe_us = tmp_path / "policyengine-us-data" + raw_inputs = ( + pe_us / "policyengine_us_data" / "storage" / "calibration" / "raw_inputs" + ) + raw_inputs.mkdir(parents=True) + (raw_inputs / "irs_soi_sample.csv").write_text("x\n1\n", encoding="utf-8") + + with patch.dict(os.environ, {PE_US_DATA_ROOT_ENV: str(pe_us)}, clear=True): + specs = pe_source_specs(include_us=True, include_uk=True) + + assert specs + assert {spec.jurisdiction for spec in specs} == {Jurisdiction.US} + + +def test_pe_source_inventory_reports_nonexistent_uk_root(tmp_path): + missing_root = tmp_path / "missing-policyengine-uk-data" + + with pytest.raises(ValueError) as excinfo: + pe_source_specs( + pe_uk_root=missing_root, + include_us=False, + include_uk=True, + ) + + assert "--pe-uk-root" in str(excinfo.value) + assert PE_UK_DATA_ROOT_ENV in str(excinfo.value) + assert "does not exist" in str(excinfo.value) + assert str(missing_root) in str(excinfo.value) + + def test_pe_source_inventory_includes_long_term_ssa_inputs(tmp_path): pe_us = tmp_path / "policyengine-us-data" storage = pe_us / "policyengine_us_data" / "storage" @@ -221,3 +308,22 @@ def test_pe_uk_inventory_includes_registry_config_and_reference_pages(tmp_path): assert len(filenames) >= len(UK_TARGET_URL_FILES) assert "dwp_stat_xplore.html" in filenames assert "uk_government_2025_spp_review.pdf" in filenames + + +def test_load_source_files_refuses_empty_inventory_prune(tmp_path): + pe_us = tmp_path / "policyengine-us-data" + pe_us.mkdir() + + args = SimpleNamespace( + db=tmp_path / "sources.db", + inventory="pe", + jurisdiction="us", + pe_us_root=pe_us, + pe_uk_root=None, + limit=None, + ) + + with pytest.raises(ValueError) as excinfo: + cmd_load_source_files(args) + + assert "Refusing to prune source artifacts" in str(excinfo.value) diff --git a/tests/test_source_package_alias_drift.py b/tests/test_source_package_alias_drift.py index b410d170..f35f841b 100644 --- a/tests/test_source_package_alias_drift.py +++ b/tests/test_source_package_alias_drift.py @@ -11,8 +11,14 @@ import pytest -from chronicle.bundle import build_bundle +from chronicle.bundle import ( + UK_BUNDLE_SOURCE_PREFIXES, + UK_BUNDLE_SOURCES, + assert_uk_bundle_sources_match_aliases, + build_bundle, +) from chronicle.source_package import ( + SOURCE_PACKAGE_ALIASES, SourcePackageAliasDriftError, assert_alias_map_covers_packages, discover_source_package_dirs, @@ -45,6 +51,20 @@ def test_repo_alias_map_matches_packages_on_disk(): assert_alias_map_covers_packages() +def test_uk_bundle_sources_match_uk_prefixed_aliases(): + """The UK bundle suite must include every alias under a UK source prefix.""" + expected = tuple( + sorted( + alias + for alias, path in SOURCE_PACKAGE_ALIASES.items() + if path.parts and path.parts[0] in UK_BUNDLE_SOURCE_PREFIXES + ) + ) + + assert UK_BUNDLE_SOURCES == expected + assert_uk_bundle_sources_match_aliases() + + def test_discover_source_package_dirs_finds_real_packages(): """Discovery must return real package directories (prove it can find some).""" discovered = discover_source_package_dirs()