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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 27 additions & 38 deletions chronicle/source_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@
"hmrc-spi-income-bands-2023-24": Path("hmrc/spi_income_bands_2023_24"),
"hmrc-cgt-statistics-2025": Path("hmrc/cgt_statistics_2025"),
"hmrc-spi-income-by-area-2023-24": Path("hmrc/spi_income_by_area_2023_24"),
"hmrc-salary-sacrifice-relief-2024-25": Path("hmrc/salary_sacrifice_relief_2024_25"),
"hmrc-salary-sacrifice-relief-2024-25": Path(
"hmrc/salary_sacrifice_relief_2024_25"
),
"hmrc-salary-sacrifice-reform-2029-headcounts": Path(
"hmrc/salary_sacrifice_reform_2029_headcounts"
),
Expand All @@ -100,9 +102,7 @@
"mhclg/ehs_weekly_housing_costs_2023_24"
),
"statbel-population-structure-2026": Path("statbel/population_structure_2026"),
"statbel-fiscal-income-2023-nis-2025": Path(
"statbel/fiscal_income_2023_nis_2025"
),
"statbel-fiscal-income-2023-nis-2025": Path("statbel/fiscal_income_2023_nis_2025"),
"spf-finances-pit-2023": Path("spf_finances/pit_2023"),
"onss-contributions-2024": Path("onss/contributions_2024"),
"onem-rva-unemployment-2024": Path("onem_rva/unemployment_2024"),
Expand All @@ -112,18 +112,17 @@
"jrc-euromod-be-baseline-statistics-2025": Path(
"jrc/euromod_be_baseline_statistics_2025"
),
"sfpd-legal-pension-caseload-2025": Path(
"sfpd/legal_pension_caseload_2025"
),
"opgroeien-groeipakket-caseload-2025": Path(
"opgroeien/groeipakket_caseload_2025"
),
"sfpd-legal-pension-caseload-2025": Path("sfpd/legal_pension_caseload_2025"),
"opgroeien-groeipakket-caseload-2025": Path("opgroeien/groeipakket_caseload_2025"),
"bfp-economic-outlook-2026-06": Path("bfp/economic_outlook_2026_06"),
"dft-nts-vehicle-ownership-2024": Path("dft/nts_vehicle_ownership_2024"),
"dwp-benefit-cap-november-2025": Path("dwp/benefit_cap_november_2025"),
"dwp-benefit-statistics-february-2026": Path(
"dwp/benefit_statistics_february_2026"
),
"dwp-uc-deductions-march-2025-february-2026": Path(
"dwp/uc_deductions_march_2025_february_2026"
),
"dwp-pip-daily-living-foi-2025": Path("dwp/pip_daily_living_foi_2025"),
"dwp-uc-households-by-constituency-may-2025": Path(
"dwp/uc_households_by_constituency_may_2025"
Expand Down Expand Up @@ -258,9 +257,7 @@
"kff-marketplace-effectuated-enrollment": Path(
"kff/marketplace_effectuated_enrollment"
),
"ons-census2021-ts041-households-lad": Path(
"ons/census2021_ts041_households_lad"
),
"ons-census2021-ts041-households-lad": Path("ons/census2021_ts041_households_lad"),
"ons-census2021-ts041-households-pcon24": Path(
"ons/census2021_ts041_households_pcon24"
),
Expand All @@ -269,28 +266,20 @@
"ons-mye-2024-uk": Path("ons/mye_2024_uk"),
"ons-lad-population-by-age-2024": Path("ons/lad_population_by_age_2024"),
"ons-pcon24-population-by-age-2024": Path("ons/pcon24_population_by_age_2024"),
"ons-small-area-income-msoa-fye2023": Path(
"ons/small_area_income_msoa_fye2023"
),
"ons-small-area-income-msoa-fye2023": Path("ons/small_area_income_msoa_fye2023"),
"ons-census2021-ts054-tenure-lad": Path("ons/census2021_ts054_tenure_lad"),
"ons-subnational-dwellings-by-tenure-2024": Path(
"ons/subnational_dwellings_by_tenure_2024"
),
"ons-pipr-rents-by-area-june-2026": Path("ons/pipr_rents_by_area_june_2026"),
"nrs-census2022-households-ukpc24": Path(
"nrs/census2022_households_ukpc24"
),
"nrs-census2022-households-ukpc24": Path("nrs/census2022_households_ukpc24"),
"nrs-pcon24-population-by-age-2024": Path("nrs/pcon24_population_by_age_2024"),
"nrs-census2022-uv404-tenure-council-area": Path(
"nrs/census2022_uv404_tenure_council_area"
),
"nisra-census2021-households-lgd": Path("nisra/census2021_households_lgd"),
"nisra-census2021-households-pcon24": Path(
"nisra/census2021_households_pcon24"
),
"nisra-pcon24-population-by-age-2024": Path(
"nisra/pcon24_population_by_age_2024"
),
"nisra-census2021-households-pcon24": Path("nisra/census2021_households_pcon24"),
"nisra-pcon24-population-by-age-2024": Path("nisra/pcon24_population_by_age_2024"),
"nisra-census2021-tenure-lgd": Path("nisra/census2021_tenure_lgd"),
"ons-uk-population-projections-2024": Path("ons/npp_2024_uk"),
"scotgov-band-d-council-tax-rates-2026-27": Path(
Expand All @@ -301,13 +290,9 @@
"scotgov-scottish-budget-social-security-assistance-2026": Path(
"scotgov/scottish_budget_social_security_assistance_2026"
),
"welshgov-council-tax-levels-2026-27": Path(
"welshgov/council_tax_levels_2026_27"
),
"welshgov-council-tax-levels-2026-27": Path("welshgov/council_tax_levels_2026_27"),
"voa-council-tax-bands-2025": Path("voa/council_tax_bands_2025"),
"voa-council-tax-stock-by-lad-2025": Path(
"voa/council_tax_stock_by_lad_2025"
),
"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"),
"ons-national-balance-sheet-land-2025": Path(
Expand All @@ -334,7 +319,9 @@
SOURCE_PACKAGE_FILENAME = "source_package.yaml"
# Repository ``packages/`` directory: the source-package authoring surface a
# complete bundle must cover (see PolicyEngine/chronicle#78).
SOURCE_PACKAGE_ROOT = Path(__file__).resolve().parents[1] / SOURCE_PACKAGE_RESOURCE_PACKAGE
SOURCE_PACKAGE_ROOT = (
Path(__file__).resolve().parents[1] / SOURCE_PACKAGE_RESOURCE_PACKAGE
)
EXCEL_COLUMN_RE = re.compile(r"^[A-Z]+$")


Expand Down Expand Up @@ -1825,6 +1812,12 @@ def _measure_from_mapping(
unit=_required(payload, "unit", "measure"),
aggregation=_required(payload, "aggregation", "measure"),
value_scale=_render_value(payload.get("value_scale", 1), year=year),
divisor_column=payload.get("divisor_column"),
round_to=(
_render_value(payload["round_to"], year=year)
if "round_to" in payload
else None
),
source_column_id=(
str(_render_value(payload["source_column_id"], year=year))
if payload.get("source_column_id") is not None
Expand Down Expand Up @@ -2041,9 +2034,7 @@ def _provenance_fields_from_mapping(
survey_instrument = payload.get("survey_instrument")
if provenance_class == "survey_aggregate":
if not has_survey_instrument:
raise KeyError(
"Missing required record_set field: survey_instrument"
)
raise KeyError("Missing required record_set field: survey_instrument")
if type(survey_instrument) is not str or not survey_instrument.strip():
raise TypeError(
"record_set survey_instrument must be a non-empty string for "
Expand Down Expand Up @@ -2095,9 +2086,7 @@ def _period_coverage_from_mapping(
}
unknown = sorted(set(payload) - allowed_keys)
if unknown:
raise ValueError(
f"record_set period_coverage has unknown keys: {unknown}."
)
raise ValueError(f"record_set period_coverage has unknown keys: {unknown}.")
return PeriodCoverage(
**{
key: _optional_rendered_string(payload.get(key), year=year)
Expand Down
41 changes: 41 additions & 0 deletions chronicle/sources/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class SourceRecordSpec:
filters: dict[str, Scalar] = field(default_factory=dict)
constraints: tuple[AggregateConstraint, ...] = ()
value_scale: int | float = 1
divisor_selector: CellSelectorSpec | None = None
round_to: int | float | None = None
source_concept: str | None = None
concept_relation: str | None = None
concept_authority: str | None = None
Expand Down Expand Up @@ -162,6 +164,8 @@ class SourceRecordSetMeasure:
unit: str
aggregation: str
value_scale: int | float = 1
divisor_column: str | None = None
round_to: int | float | None = None
source_column_id: str | None = None
expected_cell_type: str = "number"
expected_column_header_row: int | None = None
Expand Down Expand Up @@ -298,6 +302,17 @@ def compile_source_record_set_specs(
provenance_class=spec.provenance_class,
survey_instrument=spec.survey_instrument,
value_scale=measure.value_scale * row.value_scale,
divisor_selector=(
CellSelectorSpec(
selector_id=f"{source_record_id}.divisor_selector",
sheet_name=spec.sheet_name,
address=f"{measure.divisor_column}{row.row_number}",
expected_cell_type="number",
)
if measure.divisor_column is not None
else None
),
round_to=measure.round_to,
assertion=spec.assertion,
period_coverage=spec.period_coverage,
source_concept=measure.source_concept,
Expand Down Expand Up @@ -343,6 +358,11 @@ def source_regions_from_record_set_spec(
columns = [
1,
*(_excel_column_number(measure.column) for measure in spec.measures),
*(
_excel_column_number(measure.divisor_column)
for measure in spec.measures
if measure.divisor_column is not None
),
*(
_excel_column_number(row.column)
for row in spec.rows
Expand Down Expand Up @@ -435,8 +455,25 @@ def resolve_source_record(
cells_by_sheet_address=cells_by_sheet_address,
)
value = _scale_value(_sum_cell_values(value_cells), spec.value_scale)
divisor_cells: list[SourceCell] = []
if spec.divisor_selector is not None:
divisor_cells = _resolve_value_cells(
cells,
spec.divisor_selector,
cells_by_sheet_address=cells_by_sheet_address,
)
divisor = _sum_cell_values(divisor_cells)
if not isinstance(divisor, (int, float)) or divisor == 0:
raise ValueError(
f"Source record {spec.source_record_id!r} has invalid divisor "
f"{divisor!r}."
)
value = value / divisor
if spec.round_to is not None:
value = round(value / spec.round_to) * spec.round_to
lineage_cells = [
*value_cells,
*divisor_cells,
*_selector_lineage_guard_cells(
cells,
spec.selector,
Expand Down Expand Up @@ -805,6 +842,10 @@ def _record_set_spec_hash(spec: SourceRecordSetSpec) -> str:
if row.get(key) is None:
row.pop(key, None)
for measure in payload["measures"]:
if measure.get("divisor_column") is None:
measure.pop("divisor_column", None)
if measure.get("round_to") is None:
measure.pop("round_to", None)
if measure.get("expected_column_header") is None:
measure.pop("expected_column_header", None)
if measure.get("expected_column_header_row") is None:
Expand Down
18 changes: 18 additions & 0 deletions db/data/dwp/uc_deductions_march_2025_february_2026/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
source_id: dwp
package_id: dwp-uc-deductions-march-2025-february-2026
dataset: dwp_uc_deductions_march_2025_february_2026
source_page: https://www.gov.uk/government/statistics/universal-credit-quarterly-statistics-29-april-2013-to-12-february-2026
table: 'Universal Credit deductions statistics March 2025 to February 2026, supplementary data tables'
files:
2026:
filename: universal-credit-deductions-march-2025-to-february-2026.ods
source_url: https://assets.publishing.service.gov.uk/media/69fb3ea22a6137e93226b7ce/universal-credit-deductions-march-2025-to-february-2026.ods
sha256: 307ec8fa49a1f1e23db3c59f1282e16609de03444e501824e204b4151e2e5c9b
size_bytes: 148671
fetched_at: '2026-08-19T00:00:00+00:00'
storage:
r2:
provider: r2
bucket: ledger-raw
key: raw/dwp/dwp-uc-deductions-march-2025-february-2026/2026/307ec8fa49a1f1e23db3c59f1282e16609de03444e501824e204b4151e2e5c9b/universal-credit-deductions-march-2025-to-february-2026.ods
uri: r2://ledger-raw/raw/dwp/dwp-uc-deductions-march-2025-february-2026/2026/307ec8fa49a1f1e23db3c59f1282e16609de03444e501824e204b4151e2e5c9b/universal-credit-deductions-march-2025-to-february-2026.ods
Binary file not shown.
Loading
Loading