From ee10161024db6f128464b09e7fc9f0b6e37230a2 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:03:37 +0400 Subject: [PATCH 01/13] Test that one dimension value carries one label (#376) Tests only. They fail until the next commit. - test_issue_376_one_label_per_value.py (new): the week-47 filing-season rows for adjusted gross income `under_1` carry "No adjusted gross income", the label the other IRS SOI packages use, while their column guard keeps the IRS header "No adjusted gross\nincome and deficit". A bundle of week 47, Historic Table 2 and Table 2.5 reports nothing for that value. build-bundle refuses a value that two packages label differently, or that one package's groupby rows label two ways, unless every label is in REVIEWED_VALUE_LABEL_VARIANTS; those wordings stay warnings. - test_issue_265: the week-47 pair it used as its example of a warning is now an error. - test_issue_261: a groupby value whose row labels differ is now an error when it is not a reviewed wording. - test_chronicle_bundle (merged bundle, CI only): 15 cross-package value warnings instead of 16, 121 warnings instead of 122, and the register lists exactly the value wordings the bundle still warns about. On this commit the first three files give 10 failed, 37 passed: `_cross_package_value_label_errors` and REVIEWED_VALUE_LABEL_VARIANTS do not exist yet, the week-47 label is still the IRS header, and a groupby drift is still a warning. The merged-bundle test was not run locally. Co-Authored-By: Claude Opus 5.5 --- tests/test_chronicle_bundle.py | 26 ++- tests/test_issue_261_dimension_labels.py | 7 +- tests/test_issue_265_non_uk_labels.py | 34 +-- tests/test_issue_376_one_label_per_value.py | 233 ++++++++++++++++++++ 4 files changed, 281 insertions(+), 19 deletions(-) create mode 100644 tests/test_issue_376_one_label_per_value.py diff --git a/tests/test_chronicle_bundle.py b/tests/test_chronicle_bundle.py index 77ff4fbf..caa8bbe0 100644 --- a/tests/test_chronicle_bundle.py +++ b/tests/test_chronicle_bundle.py @@ -470,7 +470,11 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): # chronicle#356 adds two: DESNZ's LSOA workbooks name data zones # S01019676 and S01019677 'Newtown St Boswells', where NRS writes # 'Newton St Boswells'. - "warning_count": 122, + # chronicle#376 removes one: the week-47 filing-season rows now label + # adjusted gross income under_1 as the other IRS SOI packages do. Since + # then every value wording kept here is listed in the reviewed register + # (REVIEWED_VALUE_LABEL_VARIANTS); any other is a bundle error. + "warning_count": 121, } row_count = 0 provenance_classes = set() @@ -1982,7 +1986,7 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): assert Counter(warning["code"] for warning in summary["warnings"]) == { "conflicting_geography_name_across_packages": 82, "conflicting_groupby_value_label": 23, - "conflicting_value_label_across_packages": 16, + "conflicting_value_label_across_packages": 15, "duplicate_semantic_fact_key": 1, } assert [ @@ -2041,8 +2045,24 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): "sex=female", "sex=male", "us:statutes/26/62#adjusted_gross_income=all", - "us:statutes/26/62#adjusted_gross_income=under_1", ] + # A value may carry two labels only as a reviewed publisher wording + # (chronicle#376), and the register lists exactly the ones this bundle + # still warns about: an entry leaves it once its packages agree. + from chronicle.dimension_labels import REVIEWED_VALUE_LABEL_VARIANTS + + assert { + warning["key"] + for warning in summary["warnings"] + if warning["code"] + in { + "conflicting_value_label_across_packages", + "conflicting_groupby_value_label", + } + } == { + f"{dimension_id}={value_id}" + for dimension_id, value_id in REVIEWED_VALUE_LABEL_VARIANTS + } # E12000003 is the case chronicle#281 was opened on: six packages wrote # "Yorkshire and The Humber" and five "Yorkshire and the Humber". The # register carries the identifier, so the export names it once and the diff --git a/tests/test_issue_261_dimension_labels.py b/tests/test_issue_261_dimension_labels.py index 5e17fd93..1443c960 100644 --- a/tests/test_issue_261_dimension_labels.py +++ b/tests/test_issue_261_dimension_labels.py @@ -430,12 +430,15 @@ def test_bundle_requires_labels_for_uk_packages_only(): UK_BUNDLE_SOURCES[0], [unlabelled, _row(), drifting] ) + # Row labels that drift across one groupby value are refused too, unless + # the value is a reviewed publisher wording (chronicle#376). assert {error.code for error in errors} == { "missing_dimension_label", "missing_dimension_value_label", + "conflicting_groupby_value_label", } - assert [warning.code for warning in warnings] == ["conflicting_groupby_value_label"] - assert all(issue.source == UK_BUNDLE_SOURCES[0] for issue in errors + warnings) + assert warnings == [] + assert all(issue.source == UK_BUNDLE_SOURCES[0] for issue in errors) def test_one_uk_dimension_id_needs_one_label_across_packages(): diff --git a/tests/test_issue_265_non_uk_labels.py b/tests/test_issue_265_non_uk_labels.py index 8a75f2b7..72d95b37 100644 --- a/tests/test_issue_265_non_uk_labels.py +++ b/tests/test_issue_265_non_uk_labels.py @@ -151,23 +151,29 @@ def test_the_packages_microcosm_targets_outside_the_uk_declare_their_labels(): } -def test_a_value_two_packages_word_differently_is_a_warning(): - warnings = _cross_package_value_label_warnings( - { - ("income_range", "under_1"): { - "No adjusted gross income": ["soi-table-1-1"], - "No adjusted gross income and deficit": [ - "soi-filing-season-week47-2024-eitc-total" - ], - }, - ("filing_status", "all"): {"All filing statuses": ["soi-table-1-1"]}, - } - ) +def test_a_value_two_packages_word_differently_is_an_error(): + """One value id carries one label across packages (chronicle#376). + + Only the publisher wordings Chronicle has reviewed and kept stay warnings; + test_issue_376_one_label_per_value.py covers those. + """ + labels_by_value = { + ("income_range", "under_1"): { + "No adjusted gross income": ["soi-table-1-1"], + "No adjusted gross income and deficit": [ + "soi-filing-season-week47-2024-eitc-total" + ], + }, + ("filing_status", "all"): {"All filing statuses": ["soi-table-1-1"]}, + } - assert [(warning.code, warning.key) for warning in warnings] == [ + errors = bundle._cross_package_value_label_errors(labels_by_value) + + assert [(error.code, error.key) for error in errors] == [ ("conflicting_value_label_across_packages", "income_range=under_1") ] - assert "'No adjusted gross income' in ['soi-table-1-1']" in warnings[0].message + assert "'No adjusted gross income' in ['soi-table-1-1']" in errors[0].message + assert _cross_package_value_label_warnings(labels_by_value) == [] def test_an_area_name_is_left_to_the_geography_warning(): diff --git a/tests/test_issue_376_one_label_per_value.py b/tests/test_issue_376_one_label_per_value.py new file mode 100644 index 00000000..1155c56f --- /dev/null +++ b/tests/test_issue_376_one_label_per_value.py @@ -0,0 +1,233 @@ +"""One dimension value carries one label (chronicle#376). + +Microcosm copies every fact's dimension and value labels into the targets it +compiles, and the calibration-diagnostics dashboard refuses a release whose +targets label one value two ways. ``build-bundle`` reported a value that two +packages label differently only as a warning (chronicle#265), so IRS SOI's +filing-season week-47 package labelled ``under_1`` of the adjusted-gross-income +dimension "No adjusted gross income and deficit" while every other IRS SOI +package writes "No adjusted gross income", and US release +``populace-us-2024-0fb05b6-4b57d15a287c-20260930T150401Z`` carried both. + +``build-bundle`` now refuses a value labelled two ways, inside one package or +across packages, unless every label is a publisher wording Chronicle has +reviewed and kept (``REVIEWED_VALUE_LABEL_VARIANTS``); those stay warnings. +""" + +from __future__ import annotations + +from pathlib import Path + +import yaml + +from chronicle import bundle, dimension_labels +from chronicle.source_package import SOURCE_PACKAGE_ALIASES, load_source_package + +ADJUSTED_GROSS_INCOME = "us:statutes/26/62#adjusted_gross_income" +WEEK47 = "soi-filing-season-week47-2024-eitc-total" +IRS_SOI_LABEL = "No adjusted gross income" +WEEK47_COLUMN_HEADER = "No adjusted gross\nincome and deficit" +_PACKAGES = Path(__file__).parents[1] / "packages" + + +def _groupby_row(*, value_label: str) -> dict: + """A consumer row whose groupby value carries the given row label.""" + return { + "aggregate_fact_key": f"ledger.aggregate_fact.v2:{value_label}", + "dimensions": {"income_range": "under_1"}, + "dimension_labels": { + "income_range": "Income range", + ADJUSTED_GROSS_INCOME: "Adjusted gross income", + }, + "dimension_value_labels": { + "income_range": {"under_1": IRS_SOI_LABEL}, + ADJUSTED_GROSS_INCOME: {"under_1": value_label}, + }, + "layout": { + "groupby_dimension": ADJUSTED_GROSS_INCOME, + "groupby_dimension_label": "Adjusted gross income", + "groupby_value_id": "under_1", + "groupby_value_label": value_label, + }, + } + + +def test_the_week47_band_below_one_dollar_takes_the_irs_soi_label(): + package = load_source_package(WEEK47) + facts = package.build_facts(2024, cells=package.build_source_cells(2024)) + below_one_dollar = [ + fact + for fact in facts + if fact.layout.groupby_dimension == ADJUSTED_GROSS_INCOME + and fact.layout.groupby_value_id == "under_1" + ] + + assert sorted(fact.source_record_id for fact in below_one_dollar) == [ + "irs_soi.ty2024.filing_season_week47.eitc_by_agi.under_1." + "total_earned_income_credit_amount", + "irs_soi.ty2024.filing_season_week47.eitc_by_agi.under_1." + "total_earned_income_credit_returns", + ] + for fact in below_one_dollar: + assert fact.layout.groupby_value_label == IRS_SOI_LABEL + assert fact.dimension_value_labels[ADJUSTED_GROSS_INCOME] == { + "under_1": IRS_SOI_LABEL + } + assert fact.dimension_value_labels["income_range"] == {"under_1": IRS_SOI_LABEL} + # The band itself is unchanged: adjusted gross income below $1. + assert [ + (constraint.variable, constraint.operator, constraint.value) + for constraint in fact.constraints + ] == [(ADJUSTED_GROSS_INCOME, "<", 1)] + + +def test_the_week47_column_guard_keeps_the_irs_wording(): + """The label follows the other IRS SOI packages; the guard still reads the IRS.""" + package_yaml = _PACKAGES / SOURCE_PACKAGE_ALIASES[WEEK47] / "source_package.yaml" + package = yaml.safe_load(package_yaml.read_text()) + rows = [ + row + for record_set in package["record_sets"] + if record_set["groupby_dimension"] == ADJUSTED_GROSS_INCOME + for row in record_set["rows"] + if row["value_id"] == "under_1" + ] + + assert [(row["label"], row["expected_column_header"]) for row in rows] == [ + (IRS_SOI_LABEL, WEEK47_COLUMN_HEADER), + (IRS_SOI_LABEL, WEEK47_COLUMN_HEADER), + ] + + +def test_irs_soi_packages_label_the_band_below_one_dollar_one_way(tmp_path): + report = bundle.build_bundle( + tmp_path / "bundle", + year=2023, + sources=[ + WEEK47, + "soi-historic-table-2", + "soi-table-2-5-eitc-agi-children-2023", + ], + ) + + assert report.valid, [issue.to_dict() for issue in report.errors] + assert [ + issue.to_dict() + for issue in (*report.errors, *report.warnings) + if issue.key == f"{ADJUSTED_GROSS_INCOME}=under_1" + ] == [] + + +def test_a_value_two_packages_label_differently_is_refused(): + labels_by_value = { + (ADJUSTED_GROSS_INCOME, "under_1"): { + IRS_SOI_LABEL: ["soi-table-1-4"], + "No adjusted gross income and deficit": [WEEK47], + }, + ("filing_status", "all"): {"All filing statuses": ["soi-table-1-1"]}, + } + + errors = bundle._cross_package_value_label_errors(labels_by_value) + + assert [(error.code, error.key) for error in errors] == [ + ( + "conflicting_value_label_across_packages", + f"{ADJUSTED_GROSS_INCOME}=under_1", + ) + ] + assert f"'No adjusted gross income and deficit' in ['{WEEK47}']" in ( + errors[0].message + ) + assert bundle._cross_package_value_label_warnings(labels_by_value) == [] + + +def test_a_reviewed_publisher_wording_across_packages_stays_a_warning(): + labels_by_value = { + ("property_type", "semi_detached"): { + "Semi-detached houses": ["ons-hpssa-pcon24-year-ending-march-2026"], + "Semi detached": ["desnz-need-england-wales-2024"], + } + } + + assert bundle._cross_package_value_label_errors(labels_by_value) == [] + assert [ + (warning.code, warning.key) + for warning in bundle._cross_package_value_label_warnings(labels_by_value) + ] == [("conflicting_value_label_across_packages", "property_type=semi_detached")] + + +def test_a_new_wording_of_a_reviewed_value_is_refused(): + labels_by_value = { + ("property_type", "semi_detached"): { + "Semi-detached houses": ["ons-hpssa-pcon24-year-ending-march-2026"], + "Semi detached": ["desnz-need-england-wales-2024"], + "Semi-detached": ["a-new-package"], + } + } + + errors = bundle._cross_package_value_label_errors(labels_by_value) + + assert [error.key for error in errors] == ["property_type=semi_detached"] + assert bundle._cross_package_value_label_warnings(labels_by_value) == [] + + +def test_one_package_labelling_a_groupby_value_two_ways_is_refused(): + errors, warnings = bundle._dimension_label_reports( + WEEK47, + [ + _groupby_row(value_label=IRS_SOI_LABEL), + _groupby_row(value_label="No adjusted gross income and deficit"), + ], + ) + + assert [(error.code, error.key) for error in errors] == [ + ("conflicting_groupby_value_label", f"{ADJUSTED_GROSS_INCOME}=under_1") + ] + assert errors[0].source == WEEK47 + assert warnings == [] + + +def test_a_reviewed_publisher_wording_inside_one_package_stays_a_warning(): + """SLC prints two footnote spellings for one row (Table 7C(ii)!B75, B111).""" + axis = "early_year_award_line" + axis_label = "Early-in-year support line" + returning_total_id = "new_returning_total_returning_31_33_35" + rows = [ + { + "aggregate_fact_key": f"ledger.aggregate_fact.v2:{value_label}", + "dimensions": {axis: returning_total_id}, + "dimension_labels": {axis: axis_label}, + "dimension_value_labels": {axis: {returning_total_id: value_label}}, + "layout": { + "groupby_dimension": axis, + "groupby_dimension_label": axis_label, + "groupby_value_id": returning_total_id, + "groupby_value_label": value_label, + }, + } + for value_label in ( + "Total returning [31][33][35]", + "Total returning [31[[33][35]", + ) + ] + + errors, warnings = bundle._dimension_label_reports( + "slc-student-support-england-2025-provisional-2025-26", rows + ) + + assert errors == [] + assert [(warning.code, warning.key) for warning in warnings] == [ + ("conflicting_groupby_value_label", f"{axis}={returning_total_id}") + ] + + +def test_every_reviewed_variant_names_two_or_more_labels(): + register = dimension_labels.REVIEWED_VALUE_LABEL_VARIANTS + + assert register + for (dimension_id, value_id), labels in register.items(): + assert dimension_id.strip() == dimension_id and dimension_id + assert value_id.strip() == value_id and value_id + assert len(labels) >= 2, (dimension_id, value_id) + assert all(label.strip() == label and label for label in labels) + assert (ADJUSTED_GROSS_INCOME, "under_1") not in register From 6d74e429262f85e4a9420d034993c0d1e622aa4c Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:12:57 +0400 Subject: [PATCH 02/13] Label week-47 AGI under_1 as other IRS SOI packages do; refuse two labels (#376) The IRS filing-season week-47 package labelled value `under_1` of `us:statutes/26/62#adjusted_gross_income` "No adjusted gross income and deficit", the IRS's own column header, where every other IRS SOI package writes "No adjusted gross income". It is the same class. Column C of 25inweek47.xlsx holds every return with adjusted gross income below $1: its AGI is -$120,197,225 thousand, and the 16 income columns add up to all 156,221,133 returns. The "No adjusted gross income" rows of Publication 1304 Tables 1.1, 1.4 and 2.5 and Historic Table 2's AGI_STUB 1 are negative too. Microcosm copied both labels into US release populace-us-2024-0fb05b6-4b57d15a287c-20260930T150401Z, and the dashboard refuses a value with two labels when it builds the release's tree. - The two eitc_by_agi rows now read "No adjusted gross income"; their column guard still checks the IRS header. Built at 2024, the package's 34 facts keep their keys and values. The two under_1 facts' value labels change, and layout.record_set_spec_hash changes on the 32 eitc_by_agi facts, because that hash covers row labels. - build-bundle now refuses a dimension value that two packages label differently (`_cross_package_value_label_errors`), and a groupby value whose row labels differ inside one package, unless every label is listed in REVIEWED_VALUE_LABEL_VARIANTS (chronicle/dimension_labels.py). The register holds the values the merged bundle warned about at year 2023 before this change, except under_1: 15 across packages and 23 inside one package, each with every label it carries. They stay warnings. - The source-package harness guide states the rule; changelog fragment. The tests from the previous commit pass (47 passed in test_issue_376, test_issue_265 and test_issue_261). Applying this commit's label and geography-name checks to the facts of all 405 default-bundle packages built at year 2023 gives 0 errors and 120 warnings (23 within-package and 15 cross-package value labels, 82 geography names); with the unchanged duplicate-semantic-key warning that is the merged-bundle test's 121. The value-label warnings are exactly the register's 38 keys. Co-Authored-By: Claude Opus 5.5 --- changelog.d/376.fixed.md | 1 + chronicle/bundle.py | 114 +++++++--- chronicle/dimension_labels.py | 206 ++++++++++++++++++ docs/agent-source-package-harness.md | 16 +- .../source_package.yaml | 11 +- 5 files changed, 316 insertions(+), 32 deletions(-) create mode 100644 changelog.d/376.fixed.md diff --git a/changelog.d/376.fixed.md b/changelog.d/376.fixed.md new file mode 100644 index 00000000..17df81b2 --- /dev/null +++ b/changelog.d/376.fixed.md @@ -0,0 +1 @@ +Label the IRS filing-season week-47 rows for adjusted gross income `under_1` "No adjusted gross income", as every other IRS SOI package does. The IRS heads that column "No adjusted gross income and deficit", and the package's guard still checks that text. It is the same class, adjusted gross income below $1 with deficits included. The two facts' value labels change, and so does `layout.record_set_spec_hash` on the 32 facts of the two `eitc_by_agi` record sets, since that hash covers row labels; no value, value id, constraint or fact key moves. `build-bundle` now reports an error for a dimension value labelled two ways, whether by two packages or by one package's groupby rows, unless every label is a reviewed publisher wording listed in `REVIEWED_VALUE_LABEL_VARIANTS`; those 38 values stay warnings (chronicle#376). diff --git a/chronicle/bundle.py b/chronicle/bundle.py index 96224cc1..14e8377f 100644 --- a/chronicle/bundle.py +++ b/chronicle/bundle.py @@ -9,7 +9,11 @@ from pathlib import Path from typing import Any, Callable, Sequence -from chronicle.dimension_labels import dimension_label_issues, dimension_labels_by_id +from chronicle.dimension_labels import ( + dimension_label_issues, + dimension_labels_by_id, + is_reviewed_value_label_variant, +) from chronicle.epoch import canonicalize_key, schema_id from chronicle.source_package import ( ARTIFACT_YEAR_RESTAMP_CODE, @@ -673,6 +677,7 @@ def build_bundle( ).append(source) errors.extend(_cross_package_dimension_label_errors(dimension_labels)) + errors.extend(_cross_package_value_label_errors(value_labels)) warnings.extend(_cross_package_geography_name_warnings(geography_names)) warnings.extend(_cross_package_value_label_warnings(value_labels)) aggregate_duplicates = _duplicate_key_reports( @@ -751,24 +756,42 @@ def build_bundle( return report +_ONE_LABEL_PER_VALUE = ( + "One dimension value carries one label (chronicle#376): use the label the " + "other facts give this value. Only reviewed publisher wordings, listed in " + "REVIEWED_VALUE_LABEL_VARIANTS, may differ." +) + + def _dimension_label_reports( source: str, rows: list[dict[str, Any]], ) -> tuple[list[BuildBundleIssue], list[BuildBundleIssue]]: - """Report one source's dimension-label issues (chronicle#261, #265). + """Report one source's dimension-label issues (chronicle#261, #265, #376). Every package must label every dimension and value it emits, whichever country it publishes for: the row contract is one contract, and a consumer - reads the labels the same way everywhere. The exception is publisher row - labels that differ across the rows of one groupby value: Chronicle keeps - that text as published and warns. + reads the labels the same way everywhere. Publisher row labels that differ + across the rows of one groupby value are an error too, since a consumer + then sees one value under two names, unless every one of them is a + reviewed wording Chronicle keeps as published + (``REVIEWED_VALUE_LABEL_VARIANTS``), which is a warning. """ errors: list[BuildBundleIssue] = [] warnings: list[BuildBundleIssue] = [] for issue in dimension_label_issues(rows): + reviewed_wording = ( + issue.code == "conflicting_groupby_value_label" + and is_reviewed_value_label_variant( + issue.dimension_id, issue.value_id, issue.labels + ) + ) + message = issue.message + if issue.code == "conflicting_groupby_value_label" and not reviewed_wording: + message = f"{message} {_ONE_LABEL_PER_VALUE}" report = BuildBundleIssue( code=issue.code, - message=issue.message, + message=message, source=source, key=( issue.dimension_id @@ -776,7 +799,7 @@ def _dimension_label_reports( else f"{issue.dimension_id}={issue.value_id}" ), ) - if issue.code == "conflicting_groupby_value_label": + if reviewed_wording: warnings.append(report) else: errors.append(report) @@ -894,36 +917,73 @@ def _value_labels(rows: list[dict[str, Any]]) -> dict[tuple[str, str], set[str]] return found +def _cross_package_value_label_errors( + labels_by_value: dict[tuple[str, str], dict[str, list[str]]], +) -> list[BuildBundleIssue]: + """Refuse a dimension value two packages label differently (chronicle#376). + + Microcosm copies each fact's labels into every target it compiles, so a + release built from both packages labels one value two ways, and a consumer + that groups targets by value cannot show it as one (the IRS filing-season + week-47 table labelled adjusted gross income ``under_1`` "No adjusted gross + income and deficit" where every other IRS SOI package writes "No adjusted + gross income"). Two tables may still word one band differently; the + package then takes the label the others use, and its guards keep the + publisher's text. Reviewed wordings Chronicle keeps as published are the + warnings of :func:`_cross_package_value_label_warnings` instead. + """ + return [ + _cross_package_value_label_issue( + dimension_id, value_id, labels, remedy=_ONE_LABEL_PER_VALUE + ) + for (dimension_id, value_id), labels in sorted(labels_by_value.items()) + if len(labels) > 1 + and _spans_two_sources(labels) + and not is_reviewed_value_label_variant(dimension_id, value_id, labels) + ] + + def _cross_package_value_label_warnings( labels_by_value: dict[tuple[str, str], dict[str, list[str]]], ) -> list[BuildBundleIssue]: - """Report a dimension value two packages label differently (chronicle#265). + """Report a reviewed wording two packages keep for one value (chronicle#265). - One label per dimension id is an error because a consumer selects on the - id; one label per value is a warning because the wording belongs to the - table the value was read from, and two tables of one publisher may word a - band or a total differently. A consumer target that spans both sees both, - so the bundle says where that is possible. + These are the publisher wordings listed in ``REVIEWED_VALUE_LABEL_VARIANTS``: + Chronicle keeps each as published, and a consumer target that spans both + packages sees both, so the bundle says where that is possible. Any other + value two packages label differently is an error (chronicle#376). """ return [ - BuildBundleIssue( - code="conflicting_value_label_across_packages", - message=( - f"Packages label {dimension_id!r} value {value_id!r} " - "differently: " - + "; ".join( - f"{label!r} in {sorted(sources)}" - for label, sources in sorted(labels.items()) - ) - + "." - ), - key=f"{dimension_id}={value_id}", - ) + _cross_package_value_label_issue(dimension_id, value_id, labels) for (dimension_id, value_id), labels in sorted(labels_by_value.items()) - if len(labels) > 1 and _spans_two_sources(labels) + if len(labels) > 1 + and _spans_two_sources(labels) + and is_reviewed_value_label_variant(dimension_id, value_id, labels) ] +def _cross_package_value_label_issue( + dimension_id: str, + value_id: str, + labels: dict[str, list[str]], + *, + remedy: str | None = None, +) -> BuildBundleIssue: + message = ( + f"Packages label {dimension_id!r} value {value_id!r} differently: " + + "; ".join( + f"{label!r} in {sorted(sources)}" + for label, sources in sorted(labels.items()) + ) + + "." + ) + return BuildBundleIssue( + code="conflicting_value_label_across_packages", + message=f"{message} {remedy}" if remedy else message, + key=f"{dimension_id}={value_id}", + ) + + def _cross_package_geography_name_warnings( names_by_area: dict[tuple[str, str], dict[str, list[str]]], ) -> list[BuildBundleIssue]: diff --git a/chronicle/dimension_labels.py b/chronicle/dimension_labels.py index f8713760..7fd90a45 100644 --- a/chronicle/dimension_labels.py +++ b/chronicle/dimension_labels.py @@ -31,6 +31,8 @@ Everything else must be declared. :func:`dimension_label_issues` checks rows the way Microcosm reads them and reports what is missing or labelled two ways. +A value labelled two ways is a bundle error unless it is a reviewed publisher +wording in :data:`REVIEWED_VALUE_LABEL_VARIANTS` (chronicle#376). """ from __future__ import annotations @@ -41,6 +43,7 @@ from collections.abc import Iterable, Mapping from dataclasses import asdict, dataclass, replace from decimal import Decimal +from types import MappingProxyType from typing import Any from chronicle.consumer_contract import _concept_leaf @@ -50,6 +53,207 @@ _ISO_DATE = re.compile(r"\d{4}-\d{2}-\d{2}") +# One dimension value id carries one label (chronicle#376). Microcosm copies a +# fact's labels into every target it compiles, and a release whose targets +# label one value two ways cannot be shown as one value. ``build-bundle`` +# therefore refuses a value labelled two ways, whether by two packages or by +# the groupby rows of one package, unless every label it carries is listed +# here for that value; those it reports as warnings. +# +# Each entry is publisher wording that a review kept as published before the +# rule. A new package takes the label the other packages give a value, even +# where its own table words it differently, and its guards check the +# publisher's text. An entry leaves this register once its packages agree: +# the merged-bundle test requires every entry to still warn. +REVIEWED_VALUE_LABEL_VARIANTS: Mapping[tuple[str, str], frozenset[str]] = ( + MappingProxyType( + { + # chronicle#265: Census Bureau tables word ages differently. + ("age", "85_plus"): frozenset({"Age 85 and over", "Age 85 plus"}), + ("age", "age_20"): frozenset({"Age 20", "Female 20 years"}), + ("age", "age_21"): frozenset({"Age 21", "Female 21 years"}), + # chronicle#302: ASHE writes '30 to 39' where NTS0601 writes + # 'Age 30 to 39'. + ("age_band", "30 to 39"): frozenset({"30 to 39", "Age 30 to 39"}), + ("age_band", "40 to 49"): frozenset({"40 to 49", "Age 40 to 49"}), + # chronicle#313: SLC Table 7C(ii) spells one footnote two ways + # (B75 and B111). + ( + "early_year_award_line", + "new_returning_total_returning_31_33_35", + ): frozenset( + {"Total returning [31[[33][35]", "Total returning [31][33][35]"} + ), + # chronicle#265: Groeipakket counts each supplement per child and + # per family. + ("groeipakket.component", "pleegzorgtoeslag"): frozenset( + { + "Foster care supplement child caseload", + "Foster care supplement family caseload", + } + ), + ("groeipakket.component", "sociale_toeslag"): frozenset( + { + "Social supplement child caseload", + "Social supplement family caseload", + } + ), + ("groeipakket.component", "wezentoeslag"): frozenset( + { + "Orphan supplement child caseload", + "Orphan supplement family caseload", + } + ), + ("groeipakket.component", "zorgtoeslag"): frozenset( + { + "Care supplement child caseload", + "Care supplement family caseload", + } + ), + # chronicle#265: two ONS tables number the same footnote + # differently. + ("household_type", "couple_3_plus_children_households"): frozenset( + { + "3 or more dependent children [note 3]", + "3 or more dependent children [note 5]", + } + ), + ("ons.household_type", "couple_3_plus_children_households"): frozenset( + { + "3 or more dependent children [note 3]", + "3 or more dependent children [note 5]", + } + ), + # chronicle#265: each Belgian package names its own country total. + ("measure", "country_total"): frozenset( + { + "Belgium PIT federal and local tax before withholding", + "Complete unemployment benefit recipients", + "Household disposable income gross", + "Worker-borne personal social security contributions", + } + ), + # chronicle#265: Eurostat's paid-direction and received-direction + # cells of one national-accounts item. + ("na_item", "belgium_household_disposable_income_gross"): frozenset( + { + "Disposable income, gross (B.6G), paid-direction cell", + "Disposable income, gross (B.6G), received-direction cell", + } + ), + ("na_item", "belgium_household_mixed_income_gross"): frozenset( + { + "Mixed income, gross (B.3G), paid-direction cell", + "Mixed income, gross (B.3G), received-direction cell", + } + ), + ("na_item", "belgium_household_operating_surplus_gross"): frozenset( + { + "Operating surplus, gross (B.2G), paid-direction cell", + "Operating surplus, gross (B.2G), received-direction cell", + } + ), + # chronicle#322: the ONS Consumer Trends overview and detailed + # worksheets name six divisions differently. + ("ons.coicop", "coicop_01"): frozenset( + {"Food and drink", "Food and non-alcoholic beverages"} + ), + ("ons.coicop", "coicop_02"): frozenset( + { + "Alcohol, tobacco and narcotics", + "Alcoholic beverages and tobacco [note 1]", + } + ), + ("ons.coicop", "coicop_04"): frozenset( + {"Housing", "Housing, water, electricity, gas and other fuel"} + ), + ("ons.coicop", "coicop_05"): frozenset( + { + "Furnishing, household equipment and routine HH maintenance", + "Furnishings, household equipment and routine maintenance " + "of the house", + } + ), + ("ons.coicop", "coicop_11"): frozenset( + {"Restaurant and hotels", "Restaurants and hotels"} + ), + ("ons.coicop", "coicop_12"): frozenset( + {"Miscellaneous", "Miscellaneous goods and services"} + ), + # chronicle#211: Stats NZ heads its oldest band '65+' where + # Statbel's rows write '65_plus'. + ("person.age_band", "65_plus"): frozenset({"65+", "65_plus"}), + # chronicle#313 and #343: DESNZ, ONS house-price and ONS rent + # tables word property types differently. + ("property_type", "detached"): frozenset( + {"Detached", "Detached houses", "detached"} + ), + ("property_type", "semi_detached"): frozenset( + {"Semi detached", "Semi-detached houses"} + ), + ("property_type", "terraced"): frozenset( + {"Terraced", "Terraced houses", "terraced"} + ), + # chronicle#265: Eurostat publishes a decile's income threshold and + # its income share under one income-quantile code. + ("quant_inc", "d1"): frozenset( + {"First income decile cut-point", "First income decile share"} + ), + ("quant_inc", "d2"): frozenset( + {"Second income decile cut-point", "Second income decile share"} + ), + ("quant_inc", "d3"): frozenset( + {"Third income decile cut-point", "Third income decile share"} + ), + ("quant_inc", "d4"): frozenset( + {"Fourth income decile cut-point", "Fourth income decile share"} + ), + ("quant_inc", "d5"): frozenset( + {"Fifth income decile cut-point", "Fifth income decile share"} + ), + ("quant_inc", "d6"): frozenset( + {"Sixth income decile cut-point", "Sixth income decile share"} + ), + ("quant_inc", "d7"): frozenset( + {"Seventh income decile cut-point", "Seventh income decile share"} + ), + ("quant_inc", "d8"): frozenset( + {"Eighth income decile cut-point", "Eighth income decile share"} + ), + ("quant_inc", "d9"): frozenset( + {"Ninth income decile cut-point", "Ninth income decile share"} + ), + # chronicle#265: the UK statistics offices and HMRC write 'Female' + # or 'Females'; ACS B01001's rows write 'female'. + ("sex", "female"): frozenset({"Female", "Females", "female"}), + ("sex", "male"): frozenset({"Male", "Males"}), + # chronicle#265: IRS SOI's total rows: 'All returns' in Historic + # Table 2 and Table 1.1, 'All returns, total' in Tables 1.2, 1.4, + # 2.1 and 4.3. + ("us:statutes/26/62#adjusted_gross_income", "all"): frozenset( + {"All returns", "All returns, total"} + ), + } + ) +) + + +def is_reviewed_value_label_variant( + dimension_id: str, + value_id: str | None, + labels: Iterable[str], +) -> bool: + """Whether every label of one value is a reviewed wording of it. + + A value labelled two ways is accepted only when its id is in + :data:`REVIEWED_VALUE_LABEL_VARIANTS` and each label is listed for it, so a + third wording of a reviewed value is refused like any other (chronicle#376). + """ + if value_id is None: + return False + reviewed = REVIEWED_VALUE_LABEL_VARIANTS.get((dimension_id, value_id)) + return reviewed is not None and set(labels) <= reviewed + def dimension_value_id(value: Any) -> str: """Return the ``dimension_value_labels`` key of one dimension value. @@ -494,10 +698,12 @@ def _count( __all__ = [ + "REVIEWED_VALUE_LABEL_VARIANTS", "DimensionLabelIssue", "dimension_label_issues", "dimension_labels_by_id", "dimension_value_id", + "is_reviewed_value_label_variant", "label_facts", "unused_label_declarations", ] diff --git a/docs/agent-source-package-harness.md b/docs/agent-source-package-harness.md index 2bf204b8..b314d3b9 100644 --- a/docs/agent-source-package-harness.md +++ b/docs/agent-source-package-harness.md @@ -852,9 +852,19 @@ dimension_value_labels: A groupby axis whose rows cross several fields takes the label of its leading field, the one its id names; the crossed fields carry their own labels as dimensions of the same fact. A declaration wins over every recovered label. `build-bundle` reports an error for any package fact with an unlabelled or doubly labelled dimension or value, and for a dimension id two packages label differently (chronicle#265 — the rule -covers every country, since a consumer reads one artifact); publisher row labels -that differ across one groupby value's rows are a warning, since Chronicle keeps -them as published. `build_facts` refuses a declaration no fact uses. +covers every country, since a consumer reads one artifact). One dimension value +id carries one label too (chronicle#376): `build-bundle` reports an error for a +value two packages label differently, and for publisher row labels that differ +across one groupby value's rows. Microcosm copies the labels into every target, +so a release would otherwise label one value two ways. A package whose table +words a band differently from the other packages takes their label in the row's +`label:` and keeps the publisher's text in its guards; the IRS filing-season +week-47 table heads its below-$1 column "No adjusted gross income and deficit", +and the package labels it "No adjusted gross income", as every other IRS SOI +package does. The exceptions are the publisher wordings Chronicle reviewed and +kept before that rule, listed with every label each value may carry in +`REVIEWED_VALUE_LABEL_VARIANTS` (`chronicle/dimension_labels.py`); those are +warnings. `build_facts` refuses a declaration no fact uses. Every fact also carries the publisher's name for its geography (chronicle#266), taken from the record set's or the row's `geography_name`. `build-bundle` diff --git a/packages/irs_soi/filing_season_week47_2024/source_package.yaml b/packages/irs_soi/filing_season_week47_2024/source_package.yaml index 1910f20e..b2ff8a04 100644 --- a/packages/irs_soi/filing_season_week47_2024/source_package.yaml +++ b/packages/irs_soi/filing_season_week47_2024/source_package.yaml @@ -145,8 +145,14 @@ record_sets: domain: all_individual_income_tax_returns groupby_dimension: us:statutes/26/62#adjusted_gross_income rows: + # The IRS heads column C "No adjusted gross income and deficit" (the + # guard below checks that text). It is the class every other IRS SOI + # package labels "No adjusted gross income": adjusted gross income + # below $1, deficits included (column C's AGI is -$120.2 billion, and + # the 16 columns add up to all returns). One value id carries one label + # (chronicle#376). - value_id: under_1 - label: No adjusted gross income and deficit + label: No adjusted gross income ordinal: 0 row_number: 115 column: C @@ -411,8 +417,9 @@ record_sets: domain: all_individual_income_tax_returns groupby_dimension: us:statutes/26/62#adjusted_gross_income rows: + # Labelled as in the count record set above (chronicle#376). - value_id: under_1 - label: No adjusted gross income and deficit + label: No adjusted gross income ordinal: 0 row_number: 116 column: C From 601b2195d65ee7cba9015ffa971d506a8448a63e Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:34:19 +0400 Subject: [PATCH 03/13] Test one Chronicle-declared label per dimension value (#376) Tests only. They fail until the next commit. The design changes from a register of accepted label variants to one label per (dimension id, value id), declared in Chronicle's register chronicle/dimension_values/canonical_labels.json, with each package's own wording kept in a new field, as chronicle#281 did for geography names. - test_issue_376_one_label_per_value.py (rewritten): - the register holds the three US labels the maintainer chose: adjusted gross income under_1 "No adjusted gross income", all "All returns, total", and age 85_plus "Age 85 plus"; - REVIEWED_VALUE_LABEL_VARIANTS and the warning path are gone; - built at 2024, week 47's under_1 facts carry the register's label in dimension_value_labels and layout.groupby_value_label, and the IRS wording in publisher_dimension_value_labels; the package's row label is the IRS text again, still checked by its column guard; - the 34 week-47 fact keys equal main's, and the two under_1 facts' record_set_spec_hash is main's again; - an exported row with the new field loads through build_consumer_artifact and load_consumer_artifact with its key; - packages built at different years (week 47 at 2024 with Table 1.4 at 2023; ACS S0101 at 2024 with B01001 female at 2023) give each value one label; - a value two packages label differently is an error with no exception, and the message names the register and the new field; - a value one package labels two ways is refused by validate-package, build-suite and build-bundle, and a package declaration settles it. - Consumer contract tests: rows are emitted as chronicle.consumer_fact.v5, v4 plus the optional publisher_dimension_value_labels; the v5 schema's sha256 is pinned. - test_issue_265, test_issue_261 and the merged-bundle pins in test_chronicle_bundle.py: no value-label warnings remain (121 warnings become 83) and no value-label error is expected. On this commit the six changed files give 27 failed, 136 passed: the register, canonical_value_label, the new fact field and the v5 schema do not exist yet; the week-47 row label and record-set hash are still the previous commit's; the S0101 and B01001 age values still differ; the old exception list still exists; validate-package and build-suite have no value-label check; and rows are still v4. The keys assertion passes, as it should. The merged-bundle test was not run locally. Co-Authored-By: Claude Opus 5.5 --- tests/test_chronicle_bundle.py | 56 +-- tests/test_chronicle_consumer.py | 9 +- tests/test_chronicle_consumer_contract.py | 4 +- tests/test_issue_261_dimension_labels.py | 6 +- tests/test_issue_265_non_uk_labels.py | 10 +- tests/test_issue_376_one_label_per_value.py | 464 ++++++++++++++------ tests/test_policyengine_chronicle_schema.py | 46 +- 7 files changed, 388 insertions(+), 207 deletions(-) diff --git a/tests/test_chronicle_bundle.py b/tests/test_chronicle_bundle.py index caa8bbe0..7101628d 100644 --- a/tests/test_chronicle_bundle.py +++ b/tests/test_chronicle_bundle.py @@ -470,11 +470,12 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): # chronicle#356 adds two: DESNZ's LSOA workbooks name data zones # S01019676 and S01019677 'Newtown St Boswells', where NRS writes # 'Newton St Boswells'. - # chronicle#376 removes one: the week-47 filing-season rows now label - # adjusted gross income under_1 as the other IRS SOI packages do. Since - # then every value wording kept here is listed in the reviewed register - # (REVIEWED_VALUE_LABEL_VARIANTS); any other is a bundle error. - "warning_count": 121, + # chronicle#376 removes 39: every dimension value two packages emit + # takes its label from chronicle/dimension_values/canonical_labels.json, + # so the 16 values two packages labelled differently and the 23 values + # one package's rows labelled two ways now carry one label each, and a + # value labelled two ways is an error rather than a warning. + "warning_count": 83, } row_count = 0 provenance_classes = set() @@ -1985,8 +1986,6 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): assert len(coverage["duplicates"]["semantic_fact_keys"]) == 2324 assert Counter(warning["code"] for warning in summary["warnings"]) == { "conflicting_geography_name_across_packages": 82, - "conflicting_groupby_value_label": 23, - "conflicting_value_label_across_packages": 15, "duplicate_semantic_fact_key": 1, } assert [ @@ -2025,44 +2024,19 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): for warning in geography_names if warning["key"].startswith("local_authority:") ) == ["local_authority:nz-ta-018", "local_authority:nz-ta-027"] - assert sorted( - warning["key"] - for warning in summary["warnings"] - if warning["code"] == "conflicting_value_label_across_packages" - ) == [ - "age=85_plus", - "age=age_20", - "age=age_21", - "age_band=30 to 39", - "age_band=40 to 49", - "household_type=couple_3_plus_children_households", - "measure=country_total", - "ons.household_type=couple_3_plus_children_households", - "person.age_band=65_plus", - "property_type=detached", - "property_type=semi_detached", - "property_type=terraced", - "sex=female", - "sex=male", - "us:statutes/26/62#adjusted_gross_income=all", - ] - # A value may carry two labels only as a reviewed publisher wording - # (chronicle#376), and the register lists exactly the ones this bundle - # still warns about: an entry leaves it once its packages agree. - from chronicle.dimension_labels import REVIEWED_VALUE_LABEL_VARIANTS - - assert { - warning["key"] - for warning in summary["warnings"] - if warning["code"] + # One label per dimension value (chronicle#376): nothing about value labels + # is left as a warning, and the bundle reports no value label error. + assert [ + issue["key"] + for issue in (*summary["errors"], *summary["warnings"]) + if issue["code"] in { + "conflicting_value_label", "conflicting_value_label_across_packages", "conflicting_groupby_value_label", + "conflicting_dimension_value_label", } - } == { - f"{dimension_id}={value_id}" - for dimension_id, value_id in REVIEWED_VALUE_LABEL_VARIANTS - } + ] == [] # E12000003 is the case chronicle#281 was opened on: six packages wrote # "Yorkshire and The Humber" and five "Yorkshire and the Humber". The # register carries the identifier, so the export names it once and the diff --git a/tests/test_chronicle_consumer.py b/tests/test_chronicle_consumer.py index fc4cecac..3ae89044 100644 --- a/tests/test_chronicle_consumer.py +++ b/tests/test_chronicle_consumer.py @@ -131,7 +131,7 @@ def test_artifact_build_load_round_trip_is_facts_only(tmp_path): } assert manifest == { "schema_version": "policyengine_ledger.consumer_artifact.v2", - "consumer_fact_schema_versions": ["chronicle.consumer_fact.v4"], + "consumer_fact_schema_versions": ["chronicle.consumer_fact.v5"], "consumer_fact_schema_sha256": CONSUMER_FACT_SCHEMA_SHA256, "fact_row_count": 2, "facts_sha256": hashlib.sha256( @@ -140,7 +140,7 @@ def test_artifact_build_load_round_trip_is_facts_only(tmp_path): } assert ( CONSUMER_FACT_SCHEMA_SHA256 - == (CONSUMER_FACT_SCHEMA_SHA256_BY_VERSION["chronicle.consumer_fact.v4"]) + == (CONSUMER_FACT_SCHEMA_SHA256_BY_VERSION["chronicle.consumer_fact.v5"]) ) assert {path.name for path in out_dir.iterdir()} == { "consumer_facts.jsonl", @@ -258,14 +258,14 @@ def test_mixed_epoch_rows_are_emitted_ledger_keyed_in_one_artifact(tmp_path): artifact = load_consumer_artifact(out_dir) assert artifact.manifest["consumer_fact_schema_versions"] == [ - "chronicle.consumer_fact.v4" + "chronicle.consumer_fact.v5" ] assert len(artifact.rows) == 2 validator = Draft202012Validator(consumer_fact_schema()) for line in (out_dir / "consumer_facts.jsonl").read_text().splitlines(): row = json.loads(line) validator.validate(row) - assert row["schema_version"] == "chronicle.consumer_fact.v4" + assert row["schema_version"] == "chronicle.consumer_fact.v5" for field_name in _KEY_FIELDS: assert row[field_name].startswith("ledger.") assert all( @@ -400,6 +400,7 @@ def test_artifact_load_unknown_row_schema_names_both_accepted_forms(tmp_path): assert "chronicle.consumer_fact.v2" in message assert "chronicle.consumer_fact.v3" in message assert "chronicle.consumer_fact.v4" in message + assert "chronicle.consumer_fact.v5" in message def test_artifact_load_rejects_declared_row_schema_drift(tmp_path): diff --git a/tests/test_chronicle_consumer_contract.py b/tests/test_chronicle_consumer_contract.py index ba47ffe3..7943746a 100644 --- a/tests/test_chronicle_consumer_contract.py +++ b/tests/test_chronicle_consumer_contract.py @@ -44,7 +44,7 @@ ) CONSUMER_FACT_SCHEMA_PATH = ( - Path(__file__).parents[1] / "docs" / "schemas" / "consumer_fact.v4.schema.json" + Path(__file__).parents[1] / "docs" / "schemas" / "consumer_fact.v5.schema.json" ) CONSUMER_FACT_SAMPLE_PATH = ( Path(__file__).parents[1] / "chronicle" / "fixtures" / "consumer_facts.jsonl" @@ -331,7 +331,7 @@ def test_write_consumer_facts_jsonl_accepts_the_ledger_epoch_string(tmp_path): [_soi_agi_fact()], facts_path, emit_epoch="ledger" ) - assert report.schema_version == "chronicle.consumer_fact.v4" + assert report.schema_version == "chronicle.consumer_fact.v5" assert facts_path.exists() diff --git a/tests/test_issue_261_dimension_labels.py b/tests/test_issue_261_dimension_labels.py index 1443c960..c72df502 100644 --- a/tests/test_issue_261_dimension_labels.py +++ b/tests/test_issue_261_dimension_labels.py @@ -328,7 +328,7 @@ def test_labels_never_move_a_fact_key_or_value(): **plain["layout"], "groupby_dimension_label": "Carer Entitlement", } - assert rich["schema_version"] == "chronicle.consumer_fact.v4" + assert rich["schema_version"] == "chronicle.consumer_fact.v5" validate_consumer_fact_row(rich, 1, "labelled.jsonl") @@ -430,8 +430,8 @@ def test_bundle_requires_labels_for_uk_packages_only(): UK_BUNDLE_SOURCES[0], [unlabelled, _row(), drifting] ) - # Row labels that drift across one groupby value are refused too, unless - # the value is a reviewed publisher wording (chronicle#376). + # Row labels that drift across one groupby value are refused too: a value + # carries one label (chronicle#376). assert {error.code for error in errors} == { "missing_dimension_label", "missing_dimension_value_label", diff --git a/tests/test_issue_265_non_uk_labels.py b/tests/test_issue_265_non_uk_labels.py index 72d95b37..2b96bf98 100644 --- a/tests/test_issue_265_non_uk_labels.py +++ b/tests/test_issue_265_non_uk_labels.py @@ -20,7 +20,6 @@ from chronicle.bundle import ( UK_BUNDLE_SOURCES, _cross_package_dimension_label_errors, - _cross_package_value_label_warnings, _dimension_label_reports, _value_labels, ) @@ -154,8 +153,8 @@ def test_the_packages_microcosm_targets_outside_the_uk_declare_their_labels(): def test_a_value_two_packages_word_differently_is_an_error(): """One value id carries one label across packages (chronicle#376). - Only the publisher wordings Chronicle has reviewed and kept stay warnings; - test_issue_376_one_label_per_value.py covers those. + There is no list of accepted wordings: the remedy is one register entry, + which test_issue_376_one_label_per_value.py covers. """ labels_by_value = { ("income_range", "under_1"): { @@ -173,7 +172,6 @@ def test_a_value_two_packages_word_differently_is_an_error(): ("conflicting_value_label_across_packages", "income_range=under_1") ] assert "'No adjusted gross income' in ['soi-table-1-1']" in errors[0].message - assert _cross_package_value_label_warnings(labels_by_value) == [] def test_an_area_name_is_left_to_the_geography_warning(): @@ -196,7 +194,7 @@ def test_an_area_name_is_left_to_the_geography_warning(): def test_one_package_disagreeing_with_itself_is_left_to_its_own_report(): """The package-level check owns single-source drift; this one would repeat it.""" - warnings = _cross_package_value_label_warnings( + errors = bundle._cross_package_value_label_errors( { ("groeipakket.component", "sociale_toeslag"): { "Social supplement child caseload": [ @@ -209,4 +207,4 @@ def test_one_package_disagreeing_with_itself_is_left_to_its_own_report(): } ) - assert warnings == [] + assert errors == [] diff --git a/tests/test_issue_376_one_label_per_value.py b/tests/test_issue_376_one_label_per_value.py index 1155c56f..7a83cc30 100644 --- a/tests/test_issue_376_one_label_per_value.py +++ b/tests/test_issue_376_one_label_per_value.py @@ -1,88 +1,170 @@ -"""One dimension value carries one label (chronicle#376). +"""One Chronicle-declared label per dimension value (chronicle#376). Microcosm copies every fact's dimension and value labels into the targets it compiles, and the calibration-diagnostics dashboard refuses a release whose -targets label one value two ways. ``build-bundle`` reported a value that two -packages label differently only as a warning (chronicle#265), so IRS SOI's -filing-season week-47 package labelled ``under_1`` of the adjusted-gross-income -dimension "No adjusted gross income and deficit" while every other IRS SOI -package writes "No adjusted gross income", and US release -``populace-us-2024-0fb05b6-4b57d15a287c-20260930T150401Z`` carried both. - -``build-bundle`` now refuses a value labelled two ways, inside one package or -across packages, unless every label is a publisher wording Chronicle has -reviewed and kept (``REVIEWED_VALUE_LABEL_VARIANTS``); those stay warnings. +targets label one value two ways. IRS SOI's filing-season week-47 package +labelled ``under_1`` of the adjusted-gross-income dimension "No adjusted gross +income and deficit", the IRS's column header, while every other IRS SOI package +writes "No adjusted gross income", and US release +``populace-us-2024-0fb05b6-4b57d15a287c-20260930T150401Z`` carried both. The +bundle check only compared packages built for one year, and Microcosm merges +packages built for different years into one feed. + +Each (dimension id, value id) that more than one package emits now has one +label in Chronicle's register, ``chronicle/dimension_values/canonical_labels.json``. +Every fact takes that label when it is built, at any build year, in +``dimension_value_labels`` and ``layout.groupby_value_label``. A package's own +wording, where it differs, is kept in ``publisher_dimension_value_labels`` +(``chronicle.consumer_fact.v5``), as chronicle#281 did for geography names. A +value no register entry covers takes its package's label, and two labels for it +are an error wherever Chronicle can see both: inside one package in +``validate-package``, ``build-suite`` and ``build-bundle``, and across packages +in ``build-bundle``. There is no list of accepted exceptions. """ from __future__ import annotations +import hashlib +import json from pathlib import Path import yaml from chronicle import bundle, dimension_labels -from chronicle.source_package import SOURCE_PACKAGE_ALIASES, load_source_package +from chronicle.consumer_contract import consumer_fact_rows, write_consumer_facts_jsonl +from chronicle.source_package import ( + SOURCE_PACKAGE_ALIASES, + load_source_package, + validate_source_package, +) +from chronicle.suite import build_source_suite +from policyengine_chronicle.consumer import ( + build_consumer_artifact, + load_consumer_artifact, +) ADJUSTED_GROSS_INCOME = "us:statutes/26/62#adjusted_gross_income" WEEK47 = "soi-filing-season-week47-2024-eitc-total" IRS_SOI_LABEL = "No adjusted gross income" +WEEK47_WORDING = "No adjusted gross income and deficit" WEEK47_COLUMN_HEADER = "No adjusted gross\nincome and deficit" +FILING_SEASON_LINE = "irs_soi.filing_season_line" _PACKAGES = Path(__file__).parents[1] / "packages" +_REGISTER = ( + Path(__file__).parents[1] + / "chronicle" + / "dimension_values" + / "canonical_labels.json" +) -def _groupby_row(*, value_label: str) -> dict: - """A consumer row whose groupby value carries the given row label.""" - return { - "aggregate_fact_key": f"ledger.aggregate_fact.v2:{value_label}", - "dimensions": {"income_range": "under_1"}, - "dimension_labels": { - "income_range": "Income range", - ADJUSTED_GROSS_INCOME: "Adjusted gross income", - }, - "dimension_value_labels": { - "income_range": {"under_1": IRS_SOI_LABEL}, - ADJUSTED_GROSS_INCOME: {"under_1": value_label}, - }, - "layout": { - "groupby_dimension": ADJUSTED_GROSS_INCOME, - "groupby_dimension_label": "Adjusted gross income", - "groupby_value_id": "under_1", - "groupby_value_label": value_label, - }, - } +def _week47_facts(): + package = load_source_package(WEEK47) + return package.build_facts(2024, cells=package.build_source_cells(2024)) -def test_the_week47_band_below_one_dollar_takes_the_irs_soi_label(): - package = load_source_package(WEEK47) - facts = package.build_facts(2024, cells=package.build_source_cells(2024)) - below_one_dollar = [ +def _below_one_dollar(facts): + return [ fact for fact in facts if fact.layout.groupby_dimension == ADJUSTED_GROSS_INCOME and fact.layout.groupby_value_id == "under_1" ] - assert sorted(fact.source_record_id for fact in below_one_dollar) == [ - "irs_soi.ty2024.filing_season_week47.eitc_by_agi.under_1." - "total_earned_income_credit_amount", - "irs_soi.ty2024.filing_season_week47.eitc_by_agi.under_1." - "total_earned_income_credit_returns", - ] + +def _package_rows(source: str, year: int) -> list[dict]: + package = load_source_package(source) + facts = package.build_facts(year, cells=package.build_source_cells(year)) + return consumer_fact_rows(facts) + + +def _week47_copy_with_drifting_line_label(tmp_path: Path) -> Path: + """A copy of week 47 whose EITC amount row names its line differently.""" + source = _PACKAGES / SOURCE_PACKAGE_ALIASES[WEEK47] / "source_package.yaml" + package_dir = tmp_path / "week47_drift" + package_dir.mkdir() + payload = yaml.safe_load(source.read_text()) + amount = next( + record_set + for record_set in payload["record_sets"] + if record_set["record_set_id"].endswith("eitc_all_returns.amount") + ) + amount["rows"][0]["label"] = "Earned income credit amount" + (package_dir / "source_package.yaml").write_text( + yaml.safe_dump(payload, sort_keys=False, allow_unicode=True) + ) + return package_dir + + +# The register and the three US labels the maintainer chose. + + +def test_the_register_holds_the_three_chosen_us_labels(): + assert ( + dimension_labels.canonical_value_label(ADJUSTED_GROSS_INCOME, "under_1") + == "No adjusted gross income" + ) + assert ( + dimension_labels.canonical_value_label(ADJUSTED_GROSS_INCOME, "all") + == "All returns, total" + ) + assert dimension_labels.canonical_value_label("age", "85_plus") == "Age 85 plus" + + +def test_the_register_is_one_file_with_one_label_per_value(): + payload = json.loads(_REGISTER.read_text(encoding="utf-8")) + + assert payload["schema_version"] == "chronicle.dimension_value_labels.v1" + assert payload["labels"] + for dimension_id, value_labels in payload["labels"].items(): + assert dimension_id.strip() == dimension_id and dimension_id + for value_id, label in value_labels.items(): + assert isinstance(label, str) and label.strip() == label and label, ( + dimension_id, + value_id, + ) + assert dimension_labels.REGISTER_PATH == _REGISTER + + +def test_there_is_no_list_of_accepted_label_variants(): + assert not hasattr(dimension_labels, "REVIEWED_VALUE_LABEL_VARIANTS") + assert not hasattr(dimension_labels, "is_reviewed_value_label_variant") + assert not hasattr(bundle, "_cross_package_value_label_warnings") + + +# The week-47 package: the register's label, and the IRS wording beside it. + + +def test_week47_facts_carry_the_register_label_and_keep_the_irs_wording(): + below_one_dollar = _below_one_dollar(_week47_facts()) + + assert len(below_one_dollar) == 2 for fact in below_one_dollar: - assert fact.layout.groupby_value_label == IRS_SOI_LABEL assert fact.dimension_value_labels[ADJUSTED_GROSS_INCOME] == { "under_1": IRS_SOI_LABEL } - assert fact.dimension_value_labels["income_range"] == {"under_1": IRS_SOI_LABEL} - # The band itself is unchanged: adjusted gross income below $1. + assert fact.layout.groupby_value_label == IRS_SOI_LABEL + assert fact.publisher_dimension_value_labels == { + ADJUSTED_GROSS_INCOME: {"under_1": WEEK47_WORDING} + } assert [ (constraint.variable, constraint.operator, constraint.value) for constraint in fact.constraints ] == [(ADJUSTED_GROSS_INCOME, "<", 1)] + for row in consumer_fact_rows(below_one_dollar): + assert row["schema_version"] == "chronicle.consumer_fact.v5" + assert row["dimension_value_labels"][ADJUSTED_GROSS_INCOME] == { + "under_1": IRS_SOI_LABEL + } + assert row["layout"]["groupby_value_label"] == IRS_SOI_LABEL + assert row["publisher_dimension_value_labels"] == { + ADJUSTED_GROSS_INCOME: {"under_1": WEEK47_WORDING} + } + -def test_the_week47_column_guard_keeps_the_irs_wording(): - """The label follows the other IRS SOI packages; the guard still reads the IRS.""" +def test_week47_rows_keep_the_irs_wording_checked_against_its_column(): + """The package's row label is the IRS's text, which its column guard checks.""" package_yaml = _PACKAGES / SOURCE_PACKAGE_ALIASES[WEEK47] / "source_package.yaml" package = yaml.safe_load(package_yaml.read_text()) rows = [ @@ -94,36 +176,112 @@ def test_the_week47_column_guard_keeps_the_irs_wording(): ] assert [(row["label"], row["expected_column_header"]) for row in rows] == [ - (IRS_SOI_LABEL, WEEK47_COLUMN_HEADER), - (IRS_SOI_LABEL, WEEK47_COLUMN_HEADER), + (WEEK47_WORDING, WEEK47_COLUMN_HEADER), + (WEEK47_WORDING, WEEK47_COLUMN_HEADER), ] -def test_irs_soi_packages_label_the_band_below_one_dollar_one_way(tmp_path): - report = bundle.build_bundle( - tmp_path / "bundle", - year=2023, - sources=[ - WEEK47, - "soi-historic-table-2", - "soi-table-2-5-eitc-agi-children-2023", - ], +def test_week47_fact_keys_and_record_set_hashes_are_unchanged(): + """Labels are display metadata: no key moves, and the row text is main's.""" + rows = sorted( + consumer_fact_rows(_week47_facts()), + key=lambda row: row["lineage"]["source_record_id"], ) + key_fields = [ + [ + row["lineage"]["source_record_id"], + row["aggregate_fact_key"], + row["semantic_fact_key"], + row["legacy_fact_key"], + row["dimension_set_key"], + ] + for row in rows + ] - assert report.valid, [issue.to_dict() for issue in report.errors] - assert [ - issue.to_dict() - for issue in (*report.errors, *report.warnings) - if issue.key == f"{ADJUSTED_GROSS_INCOME}=under_1" - ] == [] + assert len(rows) == 34 + # The 34 facts' keys as built on main at aa288c3. + assert ( + hashlib.sha256(json.dumps(key_fields).encode()).hexdigest() + == "ddbdbaca7cbfe0f0a035db899d944e60cf2f1077a89339d3bc54564dbbea28ee" + ) + assert { + row["lineage"]["source_record_id"].rsplit(".", 1)[-1]: row["layout"][ + "record_set_spec_hash" + ] + for row in rows + if row["layout"].get("groupby_value_id") == "under_1" + } == { + "total_earned_income_credit_returns": "5baf962c82f213018a03f6c3", + "total_earned_income_credit_amount": "164f9b82a4305158d1a57b85", + } + + +def test_a_row_with_the_publisher_wording_loads_with_its_fact_key(tmp_path): + """The validator's recomputed key ignores the new display field (cf. #284).""" + facts = _below_one_dollar(_week47_facts()) + facts_path = tmp_path / "consumer_facts.jsonl" + write_consumer_facts_jsonl(facts, facts_path) + + build_consumer_artifact(tmp_path / "artifact", facts_path=facts_path) + artifact = load_consumer_artifact(tmp_path / "artifact") + + assert artifact.manifest["consumer_fact_schema_versions"] == [ + "chronicle.consumer_fact.v5" + ] + assert sorted(row["aggregate_fact_key"] for row in artifact.rows) == [ + "ledger.aggregate_fact.v2:d7be16a1db07493be1500058", + "ledger.aggregate_fact.v2:fba2311e623134c71d8c78cd", + ] + assert all( + row["publisher_dimension_value_labels"] + == {ADJUSTED_GROSS_INCOME: {"under_1": WEEK47_WORDING}} + for row in artifact.rows + ) + + +# One label at every build year. + + +def test_a_value_carries_one_label_across_packages_built_at_different_years(): + """Microcosm merges packages built for different years into one feed. + + Week 47 builds only at 2024 and Publication 1304 Table 1.4 at 2023; ACS + S0101 builds only at 2024 and the B01001 female table only at 2023. No + single-year bundle holds both packages of either pair, so the facts must + agree on their own. + """ + labels_by_value: dict[tuple[str, str], dict[str, list[str]]] = {} + for source, year in ( + (WEEK47, 2024), + ("soi-table-1-4", 2023), + ("census-acs-s0101-national-age-2024", 2024), + ("census-b01001-female-age-2023", 2023), + ): + for value, labels in bundle._value_labels(_package_rows(source, year)).items(): + for label in labels: + labels_by_value.setdefault(value, {}).setdefault(label, []).append( + source + ) + + assert bundle._cross_package_value_label_errors(labels_by_value) == [] + assert labels_by_value[(ADJUSTED_GROSS_INCOME, "under_1")] == { + IRS_SOI_LABEL: [WEEK47, "soi-table-1-4"] + } + for value_id in ("age_25_to_29", "age_30_to_34", "age_35_to_39", "age_40_to_44"): + assert list(labels_by_value[("age", value_id)]) == [ + dimension_labels.canonical_value_label("age", value_id) + ] -def test_a_value_two_packages_label_differently_is_refused(): +def test_a_value_two_packages_label_differently_is_refused_with_its_remedy(): labels_by_value = { - (ADJUSTED_GROSS_INCOME, "under_1"): { - IRS_SOI_LABEL: ["soi-table-1-4"], - "No adjusted gross income and deficit": [WEEK47], + ("filing_status", "head_of_household"): { + "Head of household": ["soi-table-1-2"], + "Heads of household": ["a-new-package"], }, + # A registered value is refused too if two labels reach the bundle: + # nothing is an accepted exception. + ("sex", "male"): {"Male": ["ons-a"], "Males": ["nrs-b"]}, ("filing_status", "all"): {"All filing statuses": ["soi-table-1-1"]}, } @@ -132,102 +290,124 @@ def test_a_value_two_packages_label_differently_is_refused(): assert [(error.code, error.key) for error in errors] == [ ( "conflicting_value_label_across_packages", - f"{ADJUSTED_GROSS_INCOME}=under_1", - ) + "filing_status=head_of_household", + ), + ("conflicting_value_label_across_packages", "sex=male"), ] - assert f"'No adjusted gross income and deficit' in ['{WEEK47}']" in ( - errors[0].message - ) - assert bundle._cross_package_value_label_warnings(labels_by_value) == [] + message = errors[0].message + assert "'Heads of household' in ['a-new-package']" in message + assert "chronicle/dimension_values/canonical_labels.json" in message + assert "publisher_dimension_value_labels" in message -def test_a_reviewed_publisher_wording_across_packages_stays_a_warning(): - labels_by_value = { - ("property_type", "semi_detached"): { - "Semi-detached houses": ["ons-hpssa-pcon24-year-ending-march-2026"], - "Semi detached": ["desnz-need-england-wales-2024"], - } - } +# One label inside one package, refused as early as validate-package. - assert bundle._cross_package_value_label_errors(labels_by_value) == [] - assert [ - (warning.code, warning.key) - for warning in bundle._cross_package_value_label_warnings(labels_by_value) - ] == [("conflicting_value_label_across_packages", "property_type=semi_detached")] +def test_validate_package_refuses_a_value_one_package_labels_two_ways(tmp_path): + package_dir = _week47_copy_with_drifting_line_label(tmp_path) -def test_a_new_wording_of_a_reviewed_value_is_refused(): - labels_by_value = { - ("property_type", "semi_detached"): { - "Semi-detached houses": ["ons-hpssa-pcon24-year-ending-march-2026"], - "Semi detached": ["desnz-need-england-wales-2024"], - "Semi-detached": ["a-new-package"], - } - } + report = validate_source_package(package_dir, year=2024) - errors = bundle._cross_package_value_label_errors(labels_by_value) + issues = [ + issue for issue in report.errors if issue.code == "conflicting_value_label" + ] + assert len(issues) == 1 + message = issues[0].message + assert f"{FILING_SEASON_LINE!r} value 'earned_income_credit'" in message + assert "'Earned income credit'" in message + assert "'Earned income credit amount'" in message + assert "dimension_value_labels" in message + assert "chronicle/dimension_values/canonical_labels.json" in message - assert [error.key for error in errors] == ["property_type=semi_detached"] - assert bundle._cross_package_value_label_warnings(labels_by_value) == [] +def test_build_suite_refuses_a_value_one_package_labels_two_ways(tmp_path): + package_dir = _week47_copy_with_drifting_line_label(tmp_path) -def test_one_package_labelling_a_groupby_value_two_ways_is_refused(): - errors, warnings = bundle._dimension_label_reports( - WEEK47, - [ - _groupby_row(value_label=IRS_SOI_LABEL), - _groupby_row(value_label="No adjusted gross income and deficit"), - ], - ) + report = build_source_suite(package_dir, tmp_path / "suite", year=2024) - assert [(error.code, error.key) for error in errors] == [ - ("conflicting_groupby_value_label", f"{ADJUSTED_GROSS_INCOME}=under_1") - ] - assert errors[0].source == WEEK47 - assert warnings == [] + assert not report.valid + assert [ + issue.code + for issue in report.agent_acceptance.errors + if issue.code == "conflicting_value_label" + ] == ["conflicting_value_label"] + + +def test_a_package_declaration_settles_its_own_value_and_keeps_the_rows(tmp_path): + """A value only one package emits needs no register entry: it declares one.""" + package_dir = _week47_copy_with_drifting_line_label(tmp_path) + package_yaml = package_dir / "source_package.yaml" + payload = yaml.safe_load(package_yaml.read_text()) + payload["dimension_value_labels"][FILING_SEASON_LINE] = { + "earned_income_credit": "Earned income credit" + } + package_yaml.write_text(yaml.safe_dump(payload, sort_keys=False)) + + assert [ + issue.code + for issue in validate_source_package(package_dir, year=2024).errors + if issue.code == "conflicting_value_label" + ] == [] + package = load_source_package(package_dir) + facts = package.build_facts(2024, cells=package.build_source_cells(2024)) + amount = next( + fact + for fact in facts + if fact.source_record_id.endswith( + "eitc_all_returns.total_earned_income_credit_amount" + ) + ) + assert amount.layout.groupby_value_label == "Earned income credit" + assert amount.publisher_dimension_value_labels == { + FILING_SEASON_LINE: {"earned_income_credit": "Earned income credit amount"} + } -def test_a_reviewed_publisher_wording_inside_one_package_stays_a_warning(): - """SLC prints two footnote spellings for one row (Table 7C(ii)!B75, B111).""" - axis = "early_year_award_line" - axis_label = "Early-in-year support line" - returning_total_id = "new_returning_total_returning_31_33_35" +def test_the_bundle_refuses_a_value_one_package_labels_two_ways(): rows = [ { - "aggregate_fact_key": f"ledger.aggregate_fact.v2:{value_label}", - "dimensions": {axis: returning_total_id}, - "dimension_labels": {axis: axis_label}, - "dimension_value_labels": {axis: {returning_total_id: value_label}}, + "aggregate_fact_key": f"ledger.aggregate_fact.v2:{label}", + "dimensions": {}, + "dimension_labels": {FILING_SEASON_LINE: "Filing season line"}, + "dimension_value_labels": { + FILING_SEASON_LINE: {"earned_income_credit": label} + }, "layout": { - "groupby_dimension": axis, - "groupby_dimension_label": axis_label, - "groupby_value_id": returning_total_id, - "groupby_value_label": value_label, + "groupby_dimension": FILING_SEASON_LINE, + "groupby_dimension_label": "Filing season line", + "groupby_value_id": "earned_income_credit", + "groupby_value_label": label, }, } - for value_label in ( - "Total returning [31][33][35]", - "Total returning [31[[33][35]", - ) + for label in ("Earned income credit", "Earned income credit amount") ] - errors, warnings = bundle._dimension_label_reports( - "slc-student-support-england-2025-provisional-2025-26", rows - ) + errors, warnings = bundle._dimension_label_reports(WEEK47, rows) - assert errors == [] - assert [(warning.code, warning.key) for warning in warnings] == [ - ("conflicting_groupby_value_label", f"{axis}={returning_total_id}") + assert [(error.code, error.key) for error in errors] == [ + ( + "conflicting_groupby_value_label", + f"{FILING_SEASON_LINE}=earned_income_credit", + ) ] + assert "chronicle/dimension_values/canonical_labels.json" in errors[0].message + assert warnings == [] -def test_every_reviewed_variant_names_two_or_more_labels(): - register = dimension_labels.REVIEWED_VALUE_LABEL_VARIANTS +def test_irs_soi_packages_label_the_band_below_one_dollar_one_way(tmp_path): + report = bundle.build_bundle( + tmp_path / "bundle", + year=2023, + sources=[ + WEEK47, + "soi-historic-table-2", + "soi-table-2-5-eitc-agi-children-2023", + ], + ) - assert register - for (dimension_id, value_id), labels in register.items(): - assert dimension_id.strip() == dimension_id and dimension_id - assert value_id.strip() == value_id and value_id - assert len(labels) >= 2, (dimension_id, value_id) - assert all(label.strip() == label and label for label in labels) - assert (ADJUSTED_GROSS_INCOME, "under_1") not in register + assert report.valid, [issue.to_dict() for issue in report.errors] + assert [ + issue.to_dict() + for issue in (*report.errors, *report.warnings) + if issue.key == f"{ADJUSTED_GROSS_INCOME}=under_1" + ] == [] diff --git a/tests/test_policyengine_chronicle_schema.py b/tests/test_policyengine_chronicle_schema.py index 1e15afa2..5113ef98 100644 --- a/tests/test_policyengine_chronicle_schema.py +++ b/tests/test_policyengine_chronicle_schema.py @@ -31,11 +31,13 @@ _V2 = SCHEMA_IDS["consumer_fact"].chronicle _V3 = "chronicle.consumer_fact.v3" _V4 = "chronicle.consumer_fact.v4" +_V5 = "chronicle.consumer_fact.v5" _SCHEMA_FILES = { _V1: "consumer_fact.v1.schema.json", _V2: "consumer_fact.v2.schema.json", _V3: "consumer_fact.v3.schema.json", _V4: "consumer_fact.v4.schema.json", + _V5: "consumer_fact.v5.schema.json", } _FROZEN_SCHEMA_SHA256 = { _V1: "72ad3149564f8aab3e9bb6de5ea25950c8e19a3e8402e2cdcfc52d250bc8ee82", @@ -43,6 +45,8 @@ _V3: "bdb51e2a8115634633ba7448c4005930fd9c0bfbade5e1b079b6bc24da485d3d", # chronicle#356 adds gas_year to period.type's vocabulary description. _V4: "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + # chronicle#376: v4 plus the source package's own wording of a value. + _V5: "c4d2579cdf0e4a8ea0b43ec74c75f198ea12916f443073af29adcd2e670eb7a6", } _TOP_LEVEL_KEY_DOMAINS = { @@ -106,7 +110,7 @@ def _v1_row(index=0): return row -@pytest.mark.parametrize("schema_version", [_V1, _V2, _V3, _V4]) +@pytest.mark.parametrize("schema_version", [_V1, _V2, _V3, _V4, _V5]) def test_packaged_schema_is_byte_identical_to_docs_schema(schema_version): filename = _SCHEMA_FILES[schema_version] docs_bytes = (_REPO_ROOT / "docs" / "schemas" / filename).read_bytes() @@ -125,11 +129,35 @@ def test_packaged_schema_is_byte_identical_to_docs_schema(schema_version): ) -def test_emitted_schema_is_the_v4_contract(): - assert CONSUMER_FACT_SCHEMA_SHA256 == _FROZEN_SCHEMA_SHA256[_V4] +def test_emitted_schema_is_the_v5_contract(): + assert CONSUMER_FACT_SCHEMA_SHA256 == _FROZEN_SCHEMA_SHA256[_V5] assert dict(CONSUMER_FACT_SCHEMA_SHA256_BY_VERSION) == _FROZEN_SCHEMA_SHA256 - assert consumer_fact_schema() == consumer_fact_schema(_V4) - assert tuple(CONSUMER_FACT_ROW_CONTRACTS) == (_V1, _V2, _V3, _V4) + assert consumer_fact_schema() == consumer_fact_schema(_V5) + assert tuple(CONSUMER_FACT_ROW_CONTRACTS) == (_V1, _V2, _V3, _V4, _V5) + + +def test_v5_schema_is_v4_plus_the_packages_own_value_wording(): + v4 = copy.deepcopy(consumer_fact_schema(_V4)) + v5 = copy.deepcopy(consumer_fact_schema(_V5)) + + assert v5["properties"]["schema_version"] == {"const": _V5} + assert v5["required"] == v4["required"] + assert set(v5["properties"]) - set(v4["properties"]) == { + "publisher_dimension_value_labels" + } + publisher_labels = v5["properties"]["publisher_dimension_value_labels"] + assert publisher_labels["type"] == "object" + assert publisher_labels["additionalProperties"] == { + "type": "object", + "additionalProperties": {"$ref": "#/$defs/label"}, + } + for schema in (v4, v5): + for key in ("$id", "title", "description"): + schema.pop(key, None) + schema["properties"].pop("schema_version") + schema["properties"].pop("publisher_dimension_value_labels", None) + schema["properties"]["dimension_value_labels"].pop("description") + assert v5 == v4 def test_v4_schema_is_v3_plus_the_publishers_own_geography_name(): @@ -200,12 +228,12 @@ def test_valid_fixture_rows_pass_validation(): ] assert len(rows) == 3 - assert {row["schema_version"] for row in rows} == {_V4} + assert {row["schema_version"] for row in rows} == {_V5} for line_number, row in enumerate(rows, start=1): validate_consumer_fact_row(row, line_number, _SAMPLE_PATH) -@pytest.mark.parametrize("schema_version", [_V2, _V3, _V4]) +@pytest.mark.parametrize("schema_version", [_V2, _V3, _V4, _V5]) def test_a_v1_row_validates_as_v1_and_restamped_under_each_successor(schema_version): row = _v1_row() @@ -263,8 +291,8 @@ def test_all_chronicle_epoch_identifiers_pass_without_mutating_row(): assert row == original normalized = normalize_consumer_fact_row_epochs(row, 2, _SAMPLE_PATH) # The row contract is not an epoch alias: a row keeps the contract it - # declares, here the v4 one the fixture was published under. - assert normalized["schema_version"] == _V4 + # declares, here the v5 one the fixture was published under. + assert normalized["schema_version"] == _V5 for field_name, domain_name in _TOP_LEVEL_KEY_DOMAINS.items(): assert normalized[field_name].startswith(HASH_DOMAINS[domain_name].ledger + ":") assert normalized["concept_alignment"]["concept_alignment_key"].startswith( From e29da32e867f91bf033c3e46186f4a5fb41a27bf Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:59:56 +0400 Subject: [PATCH 04/13] Give each dimension value one Chronicle-declared label (#376) Replaces the register of accepted label variants with one label per (dimension id, value id), as chronicle#281 did for geography names. - chronicle/dimension_values/canonical_labels.json is the register: 4,824 values in 147 dimensions. It lists every value that two or more default-bundle packages emit (4,801) and every value its packages word more than one way (43, of which 23 only one package emits). It was generated from every default-bundle package built at its own year: the 405 that build at 2023, and the ten that do not, at the year each validates. Values with one wording keep it; the 43 take the label in the PR's table. The three US labels are the maintainer's: adjusted gross income under_1 "No adjusted gross income", all "All returns, total", age 85_plus "Age 85 plus". The other 40 are proposals for review. - label_facts gives a listed value the register's label in dimension_value_labels and layout.groupby_value_label, at every build year, and keeps the package's own wording (its row label, else its declaration, else its source-row text) in the new AggregateFact.publisher_dimension_value_labels where it differs. A value the register does not list takes its package's label as before. - REVIEWED_VALUE_LABEL_VARIANTS, is_reviewed_value_label_variant and _cross_package_value_label_warnings are gone. A value labelled two ways is an error: validate-package reports conflicting_value_label for a groupby value the package's rows word two ways (from the package file, at any year), build-suite reports it for any value the facts label two ways, and build-bundle reports it inside a package and across packages. Each message names the register and the package declaration as the two places to fix it. - consumer_fact.v5: v4 plus the optional top-level publisher_dimension_value_labels, in docs/schemas and the package, with CONSUMER_FACT_ROW_CONTRACTS, the wheel smoke check and the checked-in sample rows updated. The validator's recomputed aggregate key never reads labels, so keys do not move; a test exports and reloads a row carrying the field. - Packages: week 47's two under_1 rows go back to the IRS header "No adjusted gross income and deficit", which their facts now carry as the package's wording, so their record_set_spec_hash is main's again. The rows for age 85_plus in Census PEP national and adjusted gross income all in Historic Table 2 take the register's label, because neither file has text there and the old labels were Chronicle's own. - Docs: the harness guide, the README and the changelog fragment describe the rule; stale docstrings that described warnings are rewritten. Test changes in this commit, beyond the previous commit's: - one record id in the new declaration test was wrong; - the schema test's v2 helper drops the new field; - two test_issue_261 tests used values the register now labels: one switches to an unlisted value id, and one no longer expects missing value labels for a package whose values other ISC packages emit; - test_chronicle_core pins the re-pinned sample's sha256; - the SLC test in test_chronicle_uk_national_2025 expects one label and the footnote typo in publisher_dimension_value_labels, not a warning. The six files from the previous commit give 163 passed. The related test files (bundle except the merged-bundle test, source packages, consumer contract, schema, boundaries, source cells, imports, governance, core, geography names, SOI targets, the label and NZ files) give 4,197 passed, 1 deselected. Applying the register to the stored consumer rows of all 415 default-bundle packages, each built at a year it validates, and running the bundle's value label checks over their union gives 0 errors and 0 warnings. ruff check passes. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 18 +- README.md | 31 +- changelog.d/376.fixed.md | 2 +- chronicle/bundle.py | 111 +- chronicle/consumer_contract.py | 8 + chronicle/core.py | 6 + chronicle/database.py | 11 +- chronicle/dimension_labels.py | 415 +- .../dimension_values/canonical_labels.json | 5124 +++++++++++++++++ chronicle/epoch.py | 15 +- chronicle/fixtures/consumer_facts.jsonl | 6 +- chronicle/source_package.py | 81 +- chronicle/store.py | 12 +- chronicle/suite.py | 53 + docs/agent-source-package-harness.md | 56 +- docs/schemas/consumer_fact.v5.schema.json | 502 ++ .../source_package.yaml | 4 +- .../source_package.yaml | 18 +- .../historic_table_2/source_package.yaml | 5 +- policyengine_chronicle/consumer.py | 8 +- policyengine_chronicle/schema.py | 5 +- .../schemas/consumer_fact.v5.schema.json | 502 ++ tests/test_chronicle_core.py | 11 +- tests/test_chronicle_uk_national_2025.py | 25 +- tests/test_issue_261_dimension_labels.py | 20 +- tests/test_issue_376_one_label_per_value.py | 2 +- tests/test_policyengine_chronicle_schema.py | 4 +- 27 files changed, 6651 insertions(+), 404 deletions(-) create mode 100644 chronicle/dimension_values/canonical_labels.json create mode 100644 docs/schemas/consumer_fact.v5.schema.json create mode 100644 policyengine_chronicle/schemas/consumer_fact.v5.schema.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc9cd420..d6aab5f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -190,27 +190,39 @@ jobs: assert policyengine_chronicle.__name__ == "policyengine_chronicle" # Every consumer-fact contract must ship inside the wheel: frozen v1, - # the v2 labels contract, the v3 geography-name contract, and the v4 - # contract rows are emitted under. + # the v2 labels contract, the v3 geography-name contract, the v4 + # geography-register contract, and the v5 contract rows are emitted + # under. assert ( consumer_fact_schema("ledger.consumer_fact.v1")["title"] == "Ledger consumer fact contract row" ) assert consumer_fact_schema()["title"] == "Chronicle consumer fact contract row" assert consumer_fact_schema()["properties"]["schema_version"] == { - "const": "chronicle.consumer_fact.v4" + "const": "chronicle.consumer_fact.v5" } assert set(CONSUMER_FACT_SCHEMA_SHA256_BY_VERSION) == { "ledger.consumer_fact.v1", "chronicle.consumer_fact.v2", "chronicle.consumer_fact.v3", "chronicle.consumer_fact.v4", + "chronicle.consumer_fact.v5", } # v4 rows take their UK area names from the register, which the # export reads from package data, so the wheel must carry it. from chronicle.geography_names import canonical_geography_name assert canonical_geography_name("E06000001") == "Hartlepool" + # v5 rows take shared dimension value labels from the register, which + # facts read from package data at build time. + from chronicle.dimension_labels import canonical_value_label + + assert ( + canonical_value_label( + "us:statutes/26/62#adjusted_gross_income", "under_1" + ) + == "No adjusted gross income" + ) assert len(CONSUMER_FACT_SCHEMA_SHA256) == 64 PY /tmp/chronicle-wheel-smoke/bin/chronicle --help >/dev/null diff --git a/README.md b/README.md index 8b5007f6..dd552a3b 100644 --- a/README.md +++ b/README.md @@ -289,9 +289,16 @@ carry canonical constraints explicitly in `universe_constraints`; source-layout `dimensions` are metadata and are not target constraints. Rows also carry Chronicle-owned labels for every dimension and dimension value (`dimension_labels`, `dimension_value_labels`, `layout.groupby_dimension_label`; -chronicle#261), which Microcosm's calibration hierarchy displays. Every UK -package must label all of them, and `build-bundle` reports a gap as an error; -see `docs/agent-source-package-harness.md` for where labels come from. +chronicle#261), which Microcosm's calibration hierarchy displays. Every package +must label all of them, and `build-bundle` reports a gap as an error +(chronicle#265). One dimension value carries one label (chronicle#376): a value +two or more packages emit takes the label in +`chronicle/dimension_values/canonical_labels.json` at every build year, and the +package's own wording, where it differs, rides in +`publisher_dimension_value_labels`. A value labelled two ways is an error, never +a warning: `validate-package` and `build-suite` refuse one a package's rows label +two ways, and `build-bundle` also refuses one two packages label differently. +See `docs/agent-source-package-harness.md` for where labels come from. For the UK source-package feed, build the curated UK suite and then a facts-only consumer artifact: @@ -580,15 +587,19 @@ payload as its Ledger key; only the prefix differs. - **The row contract moves on its own.** Each contract in `CONSUMER_FACT_ROW_CONTRACTS` adds to the one before it, so a row restamped with a later id stays valid: `chronicle.consumer_fact.v2` (chronicle#261) is - the frozen v1 row plus optional dimension and value labels, and + the frozen v1 row plus optional dimension and value labels, `chronicle.consumer_fact.v3` (chronicle#266) is v2 plus the publisher's name - for the fact's geography. Rows are emitted as v3 with Ledger-named keys, and - each row validates against the schema of the contract it names. Only v1 and - v2 name a naming epoch: the epoch table renames identifiers, it does not - version the row. + for the fact's geography, `chronicle.consumer_fact.v4` (chronicle#281) is v3 + with one name per UK geography identifier and the publisher's text in an + optional `geography.publisher_name`, and `chronicle.consumer_fact.v5` + (chronicle#376) is v4 with one label per dimension value and the package's + own wording in an optional `publisher_dimension_value_labels`. Rows are + emitted as v5 with Ledger-named keys, and each row validates against the + schema of the contract it names. Only v1 and v2 name a naming epoch: the + epoch table renames identifiers, it does not version the row. - **Artifacts canonicalize on emit.** The consumer artifact pins the sha256 of - the v3 consumer-fact schema, so `build_consumer_artifact` rewrites every row - it read to Ledger-named keys and the v3 contract before writing it; an + the v5 consumer-fact schema, so `build_consumer_artifact` rewrites every row + it read to Ledger-named keys and the v5 contract before writing it; an artifact built from mixed-epoch rows is byte-identical to one built from the same rows written Ledger-named. Loads accept an artifact pinned to any packaged schema whose rows use that contract. Asking an artifact boundary to diff --git a/changelog.d/376.fixed.md b/changelog.d/376.fixed.md index 17df81b2..ddbd6ba6 100644 --- a/changelog.d/376.fixed.md +++ b/changelog.d/376.fixed.md @@ -1 +1 @@ -Label the IRS filing-season week-47 rows for adjusted gross income `under_1` "No adjusted gross income", as every other IRS SOI package does. The IRS heads that column "No adjusted gross income and deficit", and the package's guard still checks that text. It is the same class, adjusted gross income below $1 with deficits included. The two facts' value labels change, and so does `layout.record_set_spec_hash` on the 32 facts of the two `eitc_by_agi` record sets, since that hash covers row labels; no value, value id, constraint or fact key moves. `build-bundle` now reports an error for a dimension value labelled two ways, whether by two packages or by one package's groupby rows, unless every label is a reviewed publisher wording listed in `REVIEWED_VALUE_LABEL_VARIANTS`; those 38 values stay warnings (chronicle#376). +One dimension value now carries one label, whichever packages and build years a consumer combines. Every value that two or more packages emit, or that its packages word more than one way, has one label in Chronicle's register, `chronicle/dimension_values/canonical_labels.json` (4,824 values). Facts take that label in `dimension_value_labels` and `layout.groupby_value_label` when they are built, and keep the package's own wording, where it differs, in the new optional `publisher_dimension_value_labels`; rows are emitted as `chronicle.consumer_fact.v5`, v4 plus that field. The IRS filing-season week-47 rows for adjusted gross income `under_1` keep the IRS header "No adjusted gross income and deficit" as their label and in that field, and their facts carry "No adjusted gross income", as every other IRS SOI package does. A value labelled two ways is an error, never a warning: `validate-package` and `build-suite` refuse one that a package labels two ways, and `build-bundle` also refuses one that two packages label differently; the list of accepted label variants is gone. No fact key or value moves (chronicle#376). diff --git a/chronicle/bundle.py b/chronicle/bundle.py index 14e8377f..a660cfd0 100644 --- a/chronicle/bundle.py +++ b/chronicle/bundle.py @@ -10,9 +10,9 @@ from typing import Any, Callable, Sequence from chronicle.dimension_labels import ( + ONE_LABEL_PER_VALUE, dimension_label_issues, dimension_labels_by_id, - is_reviewed_value_label_variant, ) from chronicle.epoch import canonicalize_key, schema_id from chronicle.source_package import ( @@ -679,7 +679,6 @@ def build_bundle( errors.extend(_cross_package_dimension_label_errors(dimension_labels)) errors.extend(_cross_package_value_label_errors(value_labels)) warnings.extend(_cross_package_geography_name_warnings(geography_names)) - warnings.extend(_cross_package_value_label_warnings(value_labels)) aggregate_duplicates = _duplicate_key_reports( consumer_rows, "aggregate_fact_key", @@ -756,13 +755,6 @@ def build_bundle( return report -_ONE_LABEL_PER_VALUE = ( - "One dimension value carries one label (chronicle#376): use the label the " - "other facts give this value. Only reviewed publisher wordings, listed in " - "REVIEWED_VALUE_LABEL_VARIANTS, may differ." -) - - def _dimension_label_reports( source: str, rows: list[dict[str, Any]], @@ -771,27 +763,17 @@ def _dimension_label_reports( Every package must label every dimension and value it emits, whichever country it publishes for: the row contract is one contract, and a consumer - reads the labels the same way everywhere. Publisher row labels that differ - across the rows of one groupby value are an error too, since a consumer - then sees one value under two names, unless every one of them is a - reviewed wording Chronicle keeps as published - (``REVIEWED_VALUE_LABEL_VARIANTS``), which is a warning. + reads the labels the same way everywhere. A value one package labels two + ways, publisher row labels that differ across one groupby value's rows + included, is an error too: a consumer would see one value under two names + (chronicle#376). Nothing about labels is only a warning. """ errors: list[BuildBundleIssue] = [] warnings: list[BuildBundleIssue] = [] for issue in dimension_label_issues(rows): - reviewed_wording = ( - issue.code == "conflicting_groupby_value_label" - and is_reviewed_value_label_variant( - issue.dimension_id, issue.value_id, issue.labels - ) - ) - message = issue.message - if issue.code == "conflicting_groupby_value_label" and not reviewed_wording: - message = f"{message} {_ONE_LABEL_PER_VALUE}" report = BuildBundleIssue( code=issue.code, - message=message, + message=issue.message, source=source, key=( issue.dimension_id @@ -799,10 +781,7 @@ def _dimension_label_reports( else f"{issue.dimension_id}={issue.value_id}" ), ) - if reviewed_wording: - warnings.append(report) - else: - errors.append(report) + errors.append(report) return errors, warnings @@ -923,67 +902,35 @@ def _cross_package_value_label_errors( """Refuse a dimension value two packages label differently (chronicle#376). Microcosm copies each fact's labels into every target it compiles, so a - release built from both packages labels one value two ways, and a consumer - that groups targets by value cannot show it as one (the IRS filing-season - week-47 table labelled adjusted gross income ``under_1`` "No adjusted gross - income and deficit" where every other IRS SOI package writes "No adjusted - gross income"). Two tables may still word one band differently; the - package then takes the label the others use, and its guards keep the - publisher's text. Reviewed wordings Chronicle keeps as published are the - warnings of :func:`_cross_package_value_label_warnings` instead. + release built from both packages would label one value two ways, and a + consumer that groups targets by value cannot show it as one (the IRS + filing-season week-47 table labelled adjusted gross income ``under_1`` "No + adjusted gross income and deficit" where every other IRS SOI package writes + "No adjusted gross income"). Every fact takes the label Chronicle's + register gives a value it lists, so this finds values the register does not + list yet. There are no accepted exceptions: the remedy is one register + entry, and each package's own wording is then kept in + ``publisher_dimension_value_labels``. """ return [ - _cross_package_value_label_issue( - dimension_id, value_id, labels, remedy=_ONE_LABEL_PER_VALUE + BuildBundleIssue( + code="conflicting_value_label_across_packages", + message=( + f"Packages label {dimension_id!r} value {value_id!r} " + "differently: " + + "; ".join( + f"{label!r} in {sorted(sources)}" + for label, sources in sorted(labels.items()) + ) + + f". {ONE_LABEL_PER_VALUE}" + ), + key=f"{dimension_id}={value_id}", ) for (dimension_id, value_id), labels in sorted(labels_by_value.items()) - if len(labels) > 1 - and _spans_two_sources(labels) - and not is_reviewed_value_label_variant(dimension_id, value_id, labels) - ] - - -def _cross_package_value_label_warnings( - labels_by_value: dict[tuple[str, str], dict[str, list[str]]], -) -> list[BuildBundleIssue]: - """Report a reviewed wording two packages keep for one value (chronicle#265). - - These are the publisher wordings listed in ``REVIEWED_VALUE_LABEL_VARIANTS``: - Chronicle keeps each as published, and a consumer target that spans both - packages sees both, so the bundle says where that is possible. Any other - value two packages label differently is an error (chronicle#376). - """ - return [ - _cross_package_value_label_issue(dimension_id, value_id, labels) - for (dimension_id, value_id), labels in sorted(labels_by_value.items()) - if len(labels) > 1 - and _spans_two_sources(labels) - and is_reviewed_value_label_variant(dimension_id, value_id, labels) + if len(labels) > 1 and _spans_two_sources(labels) ] -def _cross_package_value_label_issue( - dimension_id: str, - value_id: str, - labels: dict[str, list[str]], - *, - remedy: str | None = None, -) -> BuildBundleIssue: - message = ( - f"Packages label {dimension_id!r} value {value_id!r} differently: " - + "; ".join( - f"{label!r} in {sorted(sources)}" - for label, sources in sorted(labels.items()) - ) - + "." - ) - return BuildBundleIssue( - code="conflicting_value_label_across_packages", - message=f"{message} {remedy}" if remedy else message, - key=f"{dimension_id}={value_id}", - ) - - def _cross_package_geography_name_warnings( names_by_area: dict[tuple[str, str], dict[str, list[str]]], ) -> list[BuildBundleIssue]: diff --git a/chronicle/consumer_contract.py b/chronicle/consumer_contract.py index 170a0b3b..2c60e446 100644 --- a/chronicle/consumer_contract.py +++ b/chronicle/consumer_contract.py @@ -689,6 +689,14 @@ def _consumer_fact_row( dimension_id: dict(value_labels) for dimension_id, value_labels in fact.dimension_value_labels.items() }, + # The package's own wording where Chronicle's register relabels a value + # (chronicle#376); display metadata, like the labels, never in a key. + "publisher_dimension_value_labels": { + dimension_id: dict(value_labels) + for dimension_id, value_labels in ( + fact.publisher_dimension_value_labels.items() + ) + }, "universe_constraints": _universe_constraint_set_payload(fact), "source": _clean(fact_to_mapping(fact)["source"]), "lineage": { diff --git a/chronicle/core.py b/chronicle/core.py index 8ab671f2..da1c4bfc 100644 --- a/chronicle/core.py +++ b/chronicle/core.py @@ -290,6 +290,12 @@ class AggregateFact: # keyed by ``consumer_contract.dimension_value_id``. dimension_labels: dict[str, str] = field(default_factory=dict) dimension_value_labels: dict[str, dict[str, str]] = field(default_factory=dict) + # The package's own wording of a value, keyed like dimension_value_labels, + # where it differs from the label Chronicle's register gives the value + # (chronicle#376). Display metadata too. + publisher_dimension_value_labels: dict[str, dict[str, str]] = field( + default_factory=dict + ) @dataclass(frozen=True) diff --git a/chronicle/database.py b/chronicle/database.py index 41d2dab5..065753de 100644 --- a/chronicle/database.py +++ b/chronicle/database.py @@ -1058,9 +1058,14 @@ def _build_id( def _canonical_fact_mapping(fact: AggregateFact) -> dict[str, Any]: """Return a build-hash payload independent of accepted key epochs.""" mapping = asdict(fact) - # Labels (chronicle#261) enter the hash only when present, so a fact - # without them keeps the build hash it had before they existed. - for label_field in ("dimension_labels", "dimension_value_labels"): + # Labels (chronicle#261) and the package's own value wording (chronicle#376) + # enter the hash only when present, so a fact without them keeps the build + # hash it had before they existed. + for label_field in ( + "dimension_labels", + "dimension_value_labels", + "publisher_dimension_value_labels", + ): if not mapping[label_field]: mapping.pop(label_field) if mapping["layout"] and mapping["layout"]["groupby_dimension_label"] is None: diff --git a/chronicle/dimension_labels.py b/chronicle/dimension_labels.py index 7fd90a45..ddb26060 100644 --- a/chronicle/dimension_labels.py +++ b/chronicle/dimension_labels.py @@ -8,31 +8,43 @@ ``filters`` and, when it has one, its ``layout.groupby_dimension``, whose label is also stamped on ``layout.groupby_dimension_label``. -Labels resolve per package; the first source that has one wins. - -Dimension ids: +Dimension ids take the first label one of these gives: 1. the package's ``dimension_labels`` declaration; 2. the publisher's field label, for a Stat-Xplore field the parser read; 3. the label on the package's ``==`` constraints for that filter, when they agree on one label that is not the identifier itself. -Dimension values: - -1. the package's ``dimension_value_labels`` declaration; -2. the fact's publisher row label (``layout.groupby_value_label``) when the +One dimension value carries one label (chronicle#376). Microcosm merges facts +from packages built for different years into one feed, so the label cannot +depend on which packages one build happens to compare. A value takes the first +label one of these gives: + +1. Chronicle's register, :data:`REGISTER_PATH` + (``chronicle/dimension_values/canonical_labels.json``), which lists every + value that two or more packages emit, or that its packages word more than + one way, with one label; +2. the package's ``dimension_value_labels`` declaration; +3. the fact's publisher row label (``layout.groupby_value_label``) when the dimension is the record set's groupby axis (the same id, or the same leaf name source-filter canonicalization matches on) and the value is the row's value id; -3. the publisher's text in the matching column of the fact's source row, when +4. the publisher's text in the matching column of the fact's source row, when the row-semantic acceptance matches the filter to it (for ``all``, the publisher's aggregate item, such as Stat-Xplore's ``Total``); -4. a number or an ISO date, which labels itself. - -Everything else must be declared. :func:`dimension_label_issues` checks rows -the way Microcosm reads them and reports what is missing or labelled two ways. -A value labelled two ways is a bundle error unless it is a reviewed publisher -wording in :data:`REVIEWED_VALUE_LABEL_VARIANTS` (chronicle#376). +5. a number or an ISO date, which labels itself. + +That label goes on ``dimension_value_labels`` and, for the groupby value, on +``layout.groupby_value_label``. The package's own wording of the value (its row +label, else its declaration, else its source-row text) is kept in +``publisher_dimension_value_labels`` where it differs, as +``geography.publisher_name`` keeps a publisher's area name (chronicle#281). + +A value the register does not list is one that only one package emits, so its +package's label is its one label. :func:`dimension_label_issues` reports such a +value labelled two ways inside one package, and ``build-bundle`` reports it +labelled two ways by two packages; both are errors, with no exceptions. +Everything else that is unlabelled must be declared. """ from __future__ import annotations @@ -43,6 +55,8 @@ from collections.abc import Iterable, Mapping from dataclasses import asdict, dataclass, replace from decimal import Decimal +from functools import cache +from pathlib import Path from types import MappingProxyType from typing import Any @@ -53,206 +67,46 @@ _ISO_DATE = re.compile(r"\d{4}-\d{2}-\d{2}") -# One dimension value id carries one label (chronicle#376). Microcosm copies a -# fact's labels into every target it compiles, and a release whose targets -# label one value two ways cannot be shown as one value. ``build-bundle`` -# therefore refuses a value labelled two ways, whether by two packages or by -# the groupby rows of one package, unless every label it carries is listed -# here for that value; those it reports as warnings. -# -# Each entry is publisher wording that a review kept as published before the -# rule. A new package takes the label the other packages give a value, even -# where its own table words it differently, and its guards check the -# publisher's text. An entry leaves this register once its packages agree: -# the merged-bundle test requires every entry to still warn. -REVIEWED_VALUE_LABEL_VARIANTS: Mapping[tuple[str, str], frozenset[str]] = ( - MappingProxyType( +#: Chronicle's register of dimension value labels (chronicle#376). +REGISTER_PATH = Path(__file__).with_name("dimension_values") / "canonical_labels.json" +#: The register's path as error messages name it, relative to the repository. +REGISTER_DISPLAY_PATH = "chronicle/dimension_values/canonical_labels.json" +#: The remedy every one-label-per-value error states. +ONE_LABEL_PER_VALUE = ( + "One dimension value carries one label (chronicle#376). If other packages " + f"emit this value, add it to {REGISTER_DISPLAY_PATH} with one label; if " + "only this package does, declare one label for it in the package's " + "dimension_value_labels. Each package's own wording is then kept in " + "publisher_dimension_value_labels, and its guards still check the " + "publisher's text." +) + + +@cache +def _register() -> Mapping[str, Mapping[str, str]]: + payload = json.loads(REGISTER_PATH.read_text(encoding="utf-8")) + return MappingProxyType( { - # chronicle#265: Census Bureau tables word ages differently. - ("age", "85_plus"): frozenset({"Age 85 and over", "Age 85 plus"}), - ("age", "age_20"): frozenset({"Age 20", "Female 20 years"}), - ("age", "age_21"): frozenset({"Age 21", "Female 21 years"}), - # chronicle#302: ASHE writes '30 to 39' where NTS0601 writes - # 'Age 30 to 39'. - ("age_band", "30 to 39"): frozenset({"30 to 39", "Age 30 to 39"}), - ("age_band", "40 to 49"): frozenset({"40 to 49", "Age 40 to 49"}), - # chronicle#313: SLC Table 7C(ii) spells one footnote two ways - # (B75 and B111). - ( - "early_year_award_line", - "new_returning_total_returning_31_33_35", - ): frozenset( - {"Total returning [31[[33][35]", "Total returning [31][33][35]"} - ), - # chronicle#265: Groeipakket counts each supplement per child and - # per family. - ("groeipakket.component", "pleegzorgtoeslag"): frozenset( - { - "Foster care supplement child caseload", - "Foster care supplement family caseload", - } - ), - ("groeipakket.component", "sociale_toeslag"): frozenset( - { - "Social supplement child caseload", - "Social supplement family caseload", - } - ), - ("groeipakket.component", "wezentoeslag"): frozenset( - { - "Orphan supplement child caseload", - "Orphan supplement family caseload", - } - ), - ("groeipakket.component", "zorgtoeslag"): frozenset( - { - "Care supplement child caseload", - "Care supplement family caseload", - } - ), - # chronicle#265: two ONS tables number the same footnote - # differently. - ("household_type", "couple_3_plus_children_households"): frozenset( - { - "3 or more dependent children [note 3]", - "3 or more dependent children [note 5]", - } - ), - ("ons.household_type", "couple_3_plus_children_households"): frozenset( - { - "3 or more dependent children [note 3]", - "3 or more dependent children [note 5]", - } - ), - # chronicle#265: each Belgian package names its own country total. - ("measure", "country_total"): frozenset( - { - "Belgium PIT federal and local tax before withholding", - "Complete unemployment benefit recipients", - "Household disposable income gross", - "Worker-borne personal social security contributions", - } - ), - # chronicle#265: Eurostat's paid-direction and received-direction - # cells of one national-accounts item. - ("na_item", "belgium_household_disposable_income_gross"): frozenset( - { - "Disposable income, gross (B.6G), paid-direction cell", - "Disposable income, gross (B.6G), received-direction cell", - } - ), - ("na_item", "belgium_household_mixed_income_gross"): frozenset( - { - "Mixed income, gross (B.3G), paid-direction cell", - "Mixed income, gross (B.3G), received-direction cell", - } - ), - ("na_item", "belgium_household_operating_surplus_gross"): frozenset( - { - "Operating surplus, gross (B.2G), paid-direction cell", - "Operating surplus, gross (B.2G), received-direction cell", - } - ), - # chronicle#322: the ONS Consumer Trends overview and detailed - # worksheets name six divisions differently. - ("ons.coicop", "coicop_01"): frozenset( - {"Food and drink", "Food and non-alcoholic beverages"} - ), - ("ons.coicop", "coicop_02"): frozenset( - { - "Alcohol, tobacco and narcotics", - "Alcoholic beverages and tobacco [note 1]", - } - ), - ("ons.coicop", "coicop_04"): frozenset( - {"Housing", "Housing, water, electricity, gas and other fuel"} - ), - ("ons.coicop", "coicop_05"): frozenset( - { - "Furnishing, household equipment and routine HH maintenance", - "Furnishings, household equipment and routine maintenance " - "of the house", - } - ), - ("ons.coicop", "coicop_11"): frozenset( - {"Restaurant and hotels", "Restaurants and hotels"} - ), - ("ons.coicop", "coicop_12"): frozenset( - {"Miscellaneous", "Miscellaneous goods and services"} - ), - # chronicle#211: Stats NZ heads its oldest band '65+' where - # Statbel's rows write '65_plus'. - ("person.age_band", "65_plus"): frozenset({"65+", "65_plus"}), - # chronicle#313 and #343: DESNZ, ONS house-price and ONS rent - # tables word property types differently. - ("property_type", "detached"): frozenset( - {"Detached", "Detached houses", "detached"} - ), - ("property_type", "semi_detached"): frozenset( - {"Semi detached", "Semi-detached houses"} - ), - ("property_type", "terraced"): frozenset( - {"Terraced", "Terraced houses", "terraced"} - ), - # chronicle#265: Eurostat publishes a decile's income threshold and - # its income share under one income-quantile code. - ("quant_inc", "d1"): frozenset( - {"First income decile cut-point", "First income decile share"} - ), - ("quant_inc", "d2"): frozenset( - {"Second income decile cut-point", "Second income decile share"} - ), - ("quant_inc", "d3"): frozenset( - {"Third income decile cut-point", "Third income decile share"} - ), - ("quant_inc", "d4"): frozenset( - {"Fourth income decile cut-point", "Fourth income decile share"} - ), - ("quant_inc", "d5"): frozenset( - {"Fifth income decile cut-point", "Fifth income decile share"} - ), - ("quant_inc", "d6"): frozenset( - {"Sixth income decile cut-point", "Sixth income decile share"} - ), - ("quant_inc", "d7"): frozenset( - {"Seventh income decile cut-point", "Seventh income decile share"} - ), - ("quant_inc", "d8"): frozenset( - {"Eighth income decile cut-point", "Eighth income decile share"} - ), - ("quant_inc", "d9"): frozenset( - {"Ninth income decile cut-point", "Ninth income decile share"} - ), - # chronicle#265: the UK statistics offices and HMRC write 'Female' - # or 'Females'; ACS B01001's rows write 'female'. - ("sex", "female"): frozenset({"Female", "Females", "female"}), - ("sex", "male"): frozenset({"Male", "Males"}), - # chronicle#265: IRS SOI's total rows: 'All returns' in Historic - # Table 2 and Table 1.1, 'All returns, total' in Tables 1.2, 1.4, - # 2.1 and 4.3. - ("us:statutes/26/62#adjusted_gross_income", "all"): frozenset( - {"All returns", "All returns, total"} - ), + dimension_id: MappingProxyType(dict(value_labels)) + for dimension_id, value_labels in payload["labels"].items() } ) -) -def is_reviewed_value_label_variant( - dimension_id: str, - value_id: str | None, - labels: Iterable[str], -) -> bool: - """Whether every label of one value is a reviewed wording of it. - - A value labelled two ways is accepted only when its id is in - :data:`REVIEWED_VALUE_LABEL_VARIANTS` and each label is listed for it, so a - third wording of a reviewed value is refused like any other (chronicle#376). +def canonical_value_label(dimension_id: str, value_id: str) -> str | None: + """Return the register's label for one dimension value, or None. + + *value_id* is the value's :func:`dimension_value_id` form. A value the + register does not list takes its package's label. """ - if value_id is None: - return False - reviewed = REVIEWED_VALUE_LABEL_VARIANTS.get((dimension_id, value_id)) - return reviewed is not None and set(labels) <= reviewed + + return _register().get(str(dimension_id), {}).get(str(value_id)) + + +def register_size() -> int: + """Return how many dimension values the register labels.""" + + return sum(len(value_labels) for value_labels in _register().values()) def dimension_value_id(value: Any) -> str: @@ -284,8 +138,11 @@ def label_facts( ``dimension_labels`` and ``dimension_value_labels`` are the package's declarations, ``field_labels`` the publisher's labels for parsed source-row columns, and ``source_rows`` the parsed rows the facts' lineage points at. - A dimension or value no source labels is left unlabelled for - :func:`dimension_label_issues` to report. + A value the register lists takes the register's label, in + ``dimension_value_labels`` and ``layout.groupby_value_label`` alike, and + the package's own wording goes to ``publisher_dimension_value_labels`` + where it differs. A dimension or value no source labels is left + unlabelled for :func:`dimension_label_issues` to report. """ facts = list(facts) declared = dict(dimension_labels or {}) @@ -322,43 +179,105 @@ def label_facts( if key in rows_by_key ] value_labels: dict[str, dict[str, str]] = defaultdict(dict) + publisher_labels: dict[str, dict[str, str]] = defaultdict(dict) + + def resolve( + dimension_id: str, + value_id: str, + *, + row_label: str | None, + source_text: str | None = None, + self_label: str | None = None, + ) -> None: + declared_label = declared_values.get(dimension_id, {}).get(value_id) + label = ( + canonical_value_label(dimension_id, value_id) + or declared_label + or row_label + or source_text + or self_label + ) + if not label: + return + value_labels[dimension_id][value_id] = label + # The package's own wording: its row's label (the publisher's text + # its guards check), else its declaration, else the source text. + package_wording = row_label or declared_label or source_text + if package_wording and package_wording != label: + publisher_labels[dimension_id][value_id] = package_wording + for dimension_id, value in dimensions.items(): value_id = dimension_value_id(value) - label = ( - declared_values.get(dimension_id, {}).get(value_id) - or _groupby_row_label(layout, dimension_id, value_id) - or _source_row_label(rows, dimension_id, value) - or _self_label(value) + resolve( + dimension_id, + value_id, + row_label=_groupby_row_label(layout, dimension_id, value_id), + source_text=_source_row_label(rows, dimension_id, value), + self_label=_self_label(value), ) - if label: - value_labels[dimension_id][value_id] = label - if ( - groupby - and groupby not in dimensions - and layout.groupby_value_id is not None - ): - value_id = dimension_value_id(layout.groupby_value_id) - label = declared_values.get(groupby, {}).get(value_id) or ( - _groupby_row_label(layout, groupby, value_id) + groupby_value_id = ( + dimension_value_id(layout.groupby_value_id) + if groupby and layout.groupby_value_id is not None + else None + ) + if groupby_value_id is not None and groupby not in dimensions: + resolve( + groupby, + groupby_value_id, + row_label=_groupby_row_label(layout, groupby, groupby_value_id), ) - if label: - value_labels[groupby][value_id] = label + if layout is not None: + layout = replace(layout, groupby_dimension_label=labels.get(groupby)) + if groupby_value_id is not None: + layout = replace( + layout, + groupby_value_label=_groupby_value_label( + layout, value_labels, publisher_labels, declared_values + ), + ) labelled.append( replace( fact, dimension_labels=labels, dimension_value_labels=dict(value_labels), - layout=( - replace(layout, groupby_dimension_label=labels.get(groupby)) - if layout - else layout - ), + publisher_dimension_value_labels=dict(publisher_labels), + layout=layout, ) ) return labelled +def _groupby_value_label( + layout: SourceRecordLayout, + value_labels: Mapping[str, Mapping[str, str]], + publisher_labels: dict[str, dict[str, str]], + declared_values: Mapping[str, Mapping[str, str]], +) -> str | None: + """Return the one label of a fact's groupby value (chronicle#376). + + The groupby value is usually in ``value_labels`` already. When the groupby + axis is also a filter whose value differs from the row's, the row's value + is not, so its label is resolved here the same way, and the row's own + wording is kept with the others. + """ + + groupby = layout.groupby_dimension + value_id = dimension_value_id(layout.groupby_value_id) + row_label = (layout.groupby_value_label or "").strip() or None + resolved = value_labels.get(groupby, {}).get(value_id) + if resolved is not None: + return resolved + label = ( + canonical_value_label(groupby, value_id) + or declared_values.get(groupby, {}).get(value_id) + or row_label + ) + if row_label and label and row_label != label: + publisher_labels.setdefault(groupby, {})[value_id] = row_label + return label + + def unused_label_declarations( facts: Iterable[AggregateFact], *, @@ -416,7 +335,13 @@ def message(self) -> str: return ( f"{target} carries publisher row labels that differ across " f"facts: {list(self.labels)!r}; a consumer selecting facts " - "from more than one of those rows sees conflicting labels." + "from more than one of those rows sees conflicting labels. " + f"{ONE_LABEL_PER_VALUE}" + ) + if self.code == "conflicting_dimension_value_label": + return ( + f"{target} carries more than one label: {list(self.labels)!r}. " + f"{ONE_LABEL_PER_VALUE}" ) if self.code.startswith("conflicting"): return f"{target} carries more than one label: {list(self.labels)!r}." @@ -450,13 +375,16 @@ def dimension_label_issues( dimension, ``layout.groupby_dimension_label``; each value likewise, from ``dimension_value_labels`` or, for the groupby value, ``layout.groupby_value_label``. Across *rows*, every dimension id and every - value must carry a single label. Values that only ever appear as a groupby - value, whose publisher row labels differ across rows, are reported - separately as ``conflicting_groupby_value_label``: that text is the - publisher's, kept as published — including the row value of a groupby axis - that is also a filter, whose own value the row names and the filter does - not. Issues are aggregated per dimension or value, with a fact count and an - example fact key. + value must carry a single label (chronicle#376). A value labelled two ways + that only ever appears as a groupby value is reported as + ``conflicting_groupby_value_label`` (including the row value of a groupby + axis that is also a filter, whose own value the row names and the filter + does not), and any other as ``conflicting_dimension_value_label``. Facts a + package builds take the register's label for every value it lists, so + either code means a value the register does not list, labelled two ways + by the package's rows or declarations; both are errors. Issues are + aggregated per dimension or value, with a fact count and an example fact + key. """ missing: dict[tuple[str, str, str | None], list[Any]] = {} row_conflicts: dict[tuple[str, str | None], set[str]] = defaultdict(set) @@ -698,12 +626,15 @@ def _count( __all__ = [ - "REVIEWED_VALUE_LABEL_VARIANTS", + "ONE_LABEL_PER_VALUE", + "REGISTER_DISPLAY_PATH", + "REGISTER_PATH", "DimensionLabelIssue", + "canonical_value_label", "dimension_label_issues", "dimension_labels_by_id", "dimension_value_id", - "is_reviewed_value_label_variant", "label_facts", + "register_size", "unused_label_declarations", ] diff --git a/chronicle/dimension_values/canonical_labels.json b/chronicle/dimension_values/canonical_labels.json new file mode 100644 index 00000000..fb1bb166 --- /dev/null +++ b/chronicle/dimension_values/canonical_labels.json @@ -0,0 +1,5124 @@ +{ + "schema_version": "chronicle.dimension_value_labels.v1", + "description": "One label per dimension value (chronicle#376). Every fact built from a source package carries the label listed here for a value in dimension_value_labels and, for its groupby value, layout.groupby_value_label; the package's own wording, where it differs, is kept in publisher_dimension_value_labels. A value enters this register when two or more packages emit it or when its packages word it more than one way. A value no entry covers takes its package's label, and build-bundle refuses it if two packages label it differently.", + "labels": { + "Age": { + "0 - 4": "0 - 4", + "10 - 14": "10 - 14", + "15": "15", + "16 - 17": "16 - 17", + "18 - 19": "18 - 19", + "20 - 24": "20 - 24", + "25 - 29": "25 - 29", + "30 - 34": "30 - 34", + "35 - 39": "35 - 39", + "40 - 44": "40 - 44", + "45 - 49": "45 - 49", + "5 - 9": "5 - 9", + "50 - 54": "50 - 54", + "55 - 59": "55 - 59", + "60 - 64": "60 - 64", + "65 - 69": "65 - 69", + "70 - 74": "70 - 74", + "75 - 79": "75 - 79", + "80 - 84": "80 - 84", + "85 and over": "85 and over", + "Total": "Total" + }, + "Age Label": { + "0 years": "0 years", + "1 year": "1 year", + "10 years": "10 years", + "100+ years": "100+ years", + "11 years": "11 years", + "12 years": "12 years", + "13 years": "13 years", + "14 years": "14 years", + "15 years": "15 years", + "16 years": "16 years", + "17 years": "17 years", + "18 years": "18 years", + "19 years": "19 years", + "2 years": "2 years", + "20 years": "20 years", + "21 years": "21 years", + "22 years": "22 years", + "23 years": "23 years", + "24 years": "24 years", + "25 years": "25 years", + "26 years": "26 years", + "27 years": "27 years", + "28 years": "28 years", + "29 years": "29 years", + "3 years": "3 years", + "30 years": "30 years", + "31 years": "31 years", + "32 years": "32 years", + "33 years": "33 years", + "34 years": "34 years", + "35 years": "35 years", + "36 years": "36 years", + "37 years": "37 years", + "38 years": "38 years", + "39 years": "39 years", + "4 years": "4 years", + "40 years": "40 years", + "41 years": "41 years", + "42 years": "42 years", + "43 years": "43 years", + "44 years": "44 years", + "45 years": "45 years", + "46 years": "46 years", + "47 years": "47 years", + "48 years": "48 years", + "49 years": "49 years", + "5 years": "5 years", + "50 years": "50 years", + "51 years": "51 years", + "52 years": "52 years", + "53 years": "53 years", + "54 years": "54 years", + "55 years": "55 years", + "56 years": "56 years", + "57 years": "57 years", + "58 years": "58 years", + "59 years": "59 years", + "6 years": "6 years", + "60 years": "60 years", + "61 years": "61 years", + "62 years": "62 years", + "63 years": "63 years", + "64 years": "64 years", + "65 years": "65 years", + "66 years": "66 years", + "67 years": "67 years", + "68 years": "68 years", + "69 years": "69 years", + "7 years": "7 years", + "70 years": "70 years", + "71 years": "71 years", + "72 years": "72 years", + "73 years": "73 years", + "74 years": "74 years", + "75 years": "75 years", + "76 years": "76 years", + "77 years": "77 years", + "78 years": "78 years", + "79 years": "79 years", + "8 years": "8 years", + "80 years": "80 years", + "81 years": "81 years", + "82 years": "82 years", + "83 years": "83 years", + "84 years": "84 years", + "85 years": "85 years", + "86 years": "86 years", + "87 years": "87 years", + "88 years": "88 years", + "89 years": "89 years", + "9 years": "9 years", + "90 years": "90 years", + "91 years": "91 years", + "92 years": "92 years", + "93 years": "93 years", + "94 years": "94 years", + "95 years": "95 years", + "96 years": "96 years", + "97 years": "97 years", + "98 years": "98 years", + "99 years": "99 years", + "age_0": "0 years", + "age_1": "1 year", + "age_10": "10 years", + "age_100": "100+ years", + "age_11": "11 years", + "age_12": "12 years", + "age_13": "13 years", + "age_14": "14 years", + "age_15": "15 years", + "age_16": "16 years", + "age_17": "17 years", + "age_18": "18 years", + "age_19": "19 years", + "age_2": "2 years", + "age_20": "20 years", + "age_21": "21 years", + "age_22": "22 years", + "age_23": "23 years", + "age_24": "24 years", + "age_25": "25 years", + "age_26": "26 years", + "age_27": "27 years", + "age_28": "28 years", + "age_29": "29 years", + "age_3": "3 years", + "age_30": "30 years", + "age_31": "31 years", + "age_32": "32 years", + "age_33": "33 years", + "age_34": "34 years", + "age_35": "35 years", + "age_36": "36 years", + "age_37": "37 years", + "age_38": "38 years", + "age_39": "39 years", + "age_4": "4 years", + "age_40": "40 years", + "age_41": "41 years", + "age_42": "42 years", + "age_43": "43 years", + "age_44": "44 years", + "age_45": "45 years", + "age_46": "46 years", + "age_47": "47 years", + "age_48": "48 years", + "age_49": "49 years", + "age_5": "5 years", + "age_50": "50 years", + "age_51": "51 years", + "age_52": "52 years", + "age_53": "53 years", + "age_54": "54 years", + "age_55": "55 years", + "age_56": "56 years", + "age_57": "57 years", + "age_58": "58 years", + "age_59": "59 years", + "age_6": "6 years", + "age_60": "60 years", + "age_61": "61 years", + "age_62": "62 years", + "age_63": "63 years", + "age_64": "64 years", + "age_65": "65 years", + "age_66": "66 years", + "age_67": "67 years", + "age_68": "68 years", + "age_69": "69 years", + "age_7": "7 years", + "age_70": "70 years", + "age_71": "71 years", + "age_72": "72 years", + "age_73": "73 years", + "age_74": "74 years", + "age_75": "75 years", + "age_76": "76 years", + "age_77": "77 years", + "age_78": "78 years", + "age_79": "79 years", + "age_8": "8 years", + "age_80": "80 years", + "age_81": "81 years", + "age_82": "82 years", + "age_83": "83 years", + "age_84": "84 years", + "age_85": "85 years", + "age_86": "86 years", + "age_87": "87 years", + "age_88": "88 years", + "age_89": "89 years", + "age_9": "9 years", + "age_90": "90 years", + "age_91": "91 years", + "age_92": "92 years", + "age_93": "93 years", + "age_94": "94 years", + "age_95": "95 years", + "age_96": "96 years", + "age_97": "97 years", + "age_98": "98 years", + "age_99": "99 years", + "n05000001__age_0": "0 years", + "n05000001__age_1": "1 year", + "n05000001__age_10": "10 years", + "n05000001__age_100": "100+ years", + "n05000001__age_11": "11 years", + "n05000001__age_12": "12 years", + "n05000001__age_13": "13 years", + "n05000001__age_14": "14 years", + "n05000001__age_15": "15 years", + "n05000001__age_16": "16 years", + "n05000001__age_17": "17 years", + "n05000001__age_18": "18 years", + "n05000001__age_19": "19 years", + "n05000001__age_2": "2 years", + "n05000001__age_20": "20 years", + "n05000001__age_21": "21 years", + "n05000001__age_22": "22 years", + "n05000001__age_23": "23 years", + "n05000001__age_24": "24 years", + "n05000001__age_25": "25 years", + "n05000001__age_26": "26 years", + "n05000001__age_27": "27 years", + "n05000001__age_28": "28 years", + "n05000001__age_29": "29 years", + "n05000001__age_3": "3 years", + "n05000001__age_30": "30 years", + "n05000001__age_31": "31 years", + "n05000001__age_32": "32 years", + "n05000001__age_33": "33 years", + "n05000001__age_34": "34 years", + "n05000001__age_35": "35 years", + "n05000001__age_36": "36 years", + "n05000001__age_37": "37 years", + "n05000001__age_38": "38 years", + "n05000001__age_39": "39 years", + "n05000001__age_4": "4 years", + "n05000001__age_40": "40 years", + "n05000001__age_41": "41 years", + "n05000001__age_42": "42 years", + "n05000001__age_43": "43 years", + "n05000001__age_44": "44 years", + "n05000001__age_45": "45 years", + "n05000001__age_46": "46 years", + "n05000001__age_47": "47 years", + "n05000001__age_48": "48 years", + "n05000001__age_49": "49 years", + "n05000001__age_5": "5 years", + "n05000001__age_50": "50 years", + "n05000001__age_51": "51 years", + "n05000001__age_52": "52 years", + "n05000001__age_53": "53 years", + "n05000001__age_54": "54 years", + "n05000001__age_55": "55 years", + "n05000001__age_56": "56 years", + "n05000001__age_57": "57 years", + "n05000001__age_58": "58 years", + "n05000001__age_59": "59 years", + "n05000001__age_6": "6 years", + "n05000001__age_60": "60 years", + "n05000001__age_61": "61 years", + "n05000001__age_62": "62 years", + "n05000001__age_63": "63 years", + "n05000001__age_64": "64 years", + "n05000001__age_65": "65 years", + "n05000001__age_66": "66 years", + "n05000001__age_67": "67 years", + "n05000001__age_68": "68 years", + "n05000001__age_69": "69 years", + "n05000001__age_7": "7 years", + "n05000001__age_70": "70 years", + "n05000001__age_71": "71 years", + "n05000001__age_72": "72 years", + "n05000001__age_73": "73 years", + "n05000001__age_74": "74 years", + "n05000001__age_75": "75 years", + "n05000001__age_76": "76 years", + "n05000001__age_77": "77 years", + "n05000001__age_78": "78 years", + "n05000001__age_79": "79 years", + "n05000001__age_8": "8 years", + "n05000001__age_80": "80 years", + "n05000001__age_81": "81 years", + "n05000001__age_82": "82 years", + "n05000001__age_83": "83 years", + "n05000001__age_84": "84 years", + "n05000001__age_85": "85 years", + "n05000001__age_86": "86 years", + "n05000001__age_87": "87 years", + "n05000001__age_88": "88 years", + "n05000001__age_89": "89 years", + "n05000001__age_9": "9 years", + "n05000001__age_90": "90 years", + "n05000001__age_91": "91 years", + "n05000001__age_92": "92 years", + "n05000001__age_93": "93 years", + "n05000001__age_94": "94 years", + "n05000001__age_95": "95 years", + "n05000001__age_96": "96 years", + "n05000001__age_97": "97 years", + "n05000001__age_98": "98 years", + "n05000001__age_99": "99 years", + "n05000002__age_0": "0 years", + "n05000002__age_1": "1 year", + "n05000002__age_10": "10 years", + "n05000002__age_100": "100+ years", + "n05000002__age_11": "11 years", + "n05000002__age_12": "12 years", + "n05000002__age_13": "13 years", + "n05000002__age_14": "14 years", + "n05000002__age_15": "15 years", + "n05000002__age_16": "16 years", + "n05000002__age_17": "17 years", + "n05000002__age_18": "18 years", + "n05000002__age_19": "19 years", + "n05000002__age_2": "2 years", + "n05000002__age_20": "20 years", + "n05000002__age_21": "21 years", + "n05000002__age_22": "22 years", + "n05000002__age_23": "23 years", + "n05000002__age_24": "24 years", + "n05000002__age_25": "25 years", + "n05000002__age_26": "26 years", + "n05000002__age_27": "27 years", + "n05000002__age_28": "28 years", + "n05000002__age_29": "29 years", + "n05000002__age_3": "3 years", + "n05000002__age_30": "30 years", + "n05000002__age_31": "31 years", + "n05000002__age_32": "32 years", + "n05000002__age_33": "33 years", + "n05000002__age_34": "34 years", + "n05000002__age_35": "35 years", + "n05000002__age_36": "36 years", + "n05000002__age_37": "37 years", + "n05000002__age_38": "38 years", + "n05000002__age_39": "39 years", + "n05000002__age_4": "4 years", + "n05000002__age_40": "40 years", + "n05000002__age_41": "41 years", + "n05000002__age_42": "42 years", + "n05000002__age_43": "43 years", + "n05000002__age_44": "44 years", + "n05000002__age_45": "45 years", + "n05000002__age_46": "46 years", + "n05000002__age_47": "47 years", + "n05000002__age_48": "48 years", + "n05000002__age_49": "49 years", + "n05000002__age_5": "5 years", + "n05000002__age_50": "50 years", + "n05000002__age_51": "51 years", + "n05000002__age_52": "52 years", + "n05000002__age_53": "53 years", + "n05000002__age_54": "54 years", + "n05000002__age_55": "55 years", + "n05000002__age_56": "56 years", + "n05000002__age_57": "57 years", + "n05000002__age_58": "58 years", + "n05000002__age_59": "59 years", + "n05000002__age_6": "6 years", + "n05000002__age_60": "60 years", + "n05000002__age_61": "61 years", + "n05000002__age_62": "62 years", + "n05000002__age_63": "63 years", + "n05000002__age_64": "64 years", + "n05000002__age_65": "65 years", + "n05000002__age_66": "66 years", + "n05000002__age_67": "67 years", + "n05000002__age_68": "68 years", + "n05000002__age_69": "69 years", + "n05000002__age_7": "7 years", + "n05000002__age_70": "70 years", + "n05000002__age_71": "71 years", + "n05000002__age_72": "72 years", + "n05000002__age_73": "73 years", + "n05000002__age_74": "74 years", + "n05000002__age_75": "75 years", + "n05000002__age_76": "76 years", + "n05000002__age_77": "77 years", + "n05000002__age_78": "78 years", + "n05000002__age_79": "79 years", + "n05000002__age_8": "8 years", + "n05000002__age_80": "80 years", + "n05000002__age_81": "81 years", + "n05000002__age_82": "82 years", + "n05000002__age_83": "83 years", + "n05000002__age_84": "84 years", + "n05000002__age_85": "85 years", + "n05000002__age_86": "86 years", + "n05000002__age_87": "87 years", + "n05000002__age_88": "88 years", + "n05000002__age_89": "89 years", + "n05000002__age_9": "9 years", + "n05000002__age_90": "90 years", + "n05000002__age_91": "91 years", + "n05000002__age_92": "92 years", + "n05000002__age_93": "93 years", + "n05000002__age_94": "94 years", + "n05000002__age_95": "95 years", + "n05000002__age_96": "96 years", + "n05000002__age_97": "97 years", + "n05000002__age_98": "98 years", + "n05000002__age_99": "99 years", + "n05000003__age_0": "0 years", + "n05000003__age_1": "1 year", + "n05000003__age_10": "10 years", + "n05000003__age_100": "100+ years", + "n05000003__age_11": "11 years", + "n05000003__age_12": "12 years", + "n05000003__age_13": "13 years", + "n05000003__age_14": "14 years", + "n05000003__age_15": "15 years", + "n05000003__age_16": "16 years", + "n05000003__age_17": "17 years", + "n05000003__age_18": "18 years", + "n05000003__age_19": "19 years", + "n05000003__age_2": "2 years", + "n05000003__age_20": "20 years", + "n05000003__age_21": "21 years", + "n05000003__age_22": "22 years", + "n05000003__age_23": "23 years", + "n05000003__age_24": "24 years", + "n05000003__age_25": "25 years", + "n05000003__age_26": "26 years", + "n05000003__age_27": "27 years", + "n05000003__age_28": "28 years", + "n05000003__age_29": "29 years", + "n05000003__age_3": "3 years", + "n05000003__age_30": "30 years", + "n05000003__age_31": "31 years", + "n05000003__age_32": "32 years", + "n05000003__age_33": "33 years", + "n05000003__age_34": "34 years", + "n05000003__age_35": "35 years", + "n05000003__age_36": "36 years", + "n05000003__age_37": "37 years", + "n05000003__age_38": "38 years", + "n05000003__age_39": "39 years", + "n05000003__age_4": "4 years", + "n05000003__age_40": "40 years", + "n05000003__age_41": "41 years", + "n05000003__age_42": "42 years", + "n05000003__age_43": "43 years", + "n05000003__age_44": "44 years", + "n05000003__age_45": "45 years", + "n05000003__age_46": "46 years", + "n05000003__age_47": "47 years", + "n05000003__age_48": "48 years", + "n05000003__age_49": "49 years", + "n05000003__age_5": "5 years", + "n05000003__age_50": "50 years", + "n05000003__age_51": "51 years", + "n05000003__age_52": "52 years", + "n05000003__age_53": "53 years", + "n05000003__age_54": "54 years", + "n05000003__age_55": "55 years", + "n05000003__age_56": "56 years", + "n05000003__age_57": "57 years", + "n05000003__age_58": "58 years", + "n05000003__age_59": "59 years", + "n05000003__age_6": "6 years", + "n05000003__age_60": "60 years", + "n05000003__age_61": "61 years", + "n05000003__age_62": "62 years", + "n05000003__age_63": "63 years", + "n05000003__age_64": "64 years", + "n05000003__age_65": "65 years", + "n05000003__age_66": "66 years", + "n05000003__age_67": "67 years", + "n05000003__age_68": "68 years", + "n05000003__age_69": "69 years", + "n05000003__age_7": "7 years", + "n05000003__age_70": "70 years", + "n05000003__age_71": "71 years", + "n05000003__age_72": "72 years", + "n05000003__age_73": "73 years", + "n05000003__age_74": "74 years", + "n05000003__age_75": "75 years", + "n05000003__age_76": "76 years", + "n05000003__age_77": "77 years", + "n05000003__age_78": "78 years", + "n05000003__age_79": "79 years", + "n05000003__age_8": "8 years", + "n05000003__age_80": "80 years", + "n05000003__age_81": "81 years", + "n05000003__age_82": "82 years", + "n05000003__age_83": "83 years", + "n05000003__age_84": "84 years", + "n05000003__age_85": "85 years", + "n05000003__age_86": "86 years", + "n05000003__age_87": "87 years", + "n05000003__age_88": "88 years", + "n05000003__age_89": "89 years", + "n05000003__age_9": "9 years", + "n05000003__age_90": "90 years", + "n05000003__age_91": "91 years", + "n05000003__age_92": "92 years", + "n05000003__age_93": "93 years", + "n05000003__age_94": "94 years", + "n05000003__age_95": "95 years", + "n05000003__age_96": "96 years", + "n05000003__age_97": "97 years", + "n05000003__age_98": "98 years", + "n05000003__age_99": "99 years", + "n05000004__age_0": "0 years", + "n05000004__age_1": "1 year", + "n05000004__age_10": "10 years", + "n05000004__age_100": "100+ years", + "n05000004__age_11": "11 years", + "n05000004__age_12": "12 years", + "n05000004__age_13": "13 years", + "n05000004__age_14": "14 years", + "n05000004__age_15": "15 years", + "n05000004__age_16": "16 years", + "n05000004__age_17": "17 years", + "n05000004__age_18": "18 years", + "n05000004__age_19": "19 years", + "n05000004__age_2": "2 years", + "n05000004__age_20": "20 years", + "n05000004__age_21": "21 years", + "n05000004__age_22": "22 years", + "n05000004__age_23": "23 years", + "n05000004__age_24": "24 years", + "n05000004__age_25": "25 years", + "n05000004__age_26": "26 years", + "n05000004__age_27": "27 years", + "n05000004__age_28": "28 years", + "n05000004__age_29": "29 years", + "n05000004__age_3": "3 years", + "n05000004__age_30": "30 years", + "n05000004__age_31": "31 years", + "n05000004__age_32": "32 years", + "n05000004__age_33": "33 years", + "n05000004__age_34": "34 years", + "n05000004__age_35": "35 years", + "n05000004__age_36": "36 years", + "n05000004__age_37": "37 years", + "n05000004__age_38": "38 years", + "n05000004__age_39": "39 years", + "n05000004__age_4": "4 years", + "n05000004__age_40": "40 years", + "n05000004__age_41": "41 years", + "n05000004__age_42": "42 years", + "n05000004__age_43": "43 years", + "n05000004__age_44": "44 years", + "n05000004__age_45": "45 years", + "n05000004__age_46": "46 years", + "n05000004__age_47": "47 years", + "n05000004__age_48": "48 years", + "n05000004__age_49": "49 years", + "n05000004__age_5": "5 years", + "n05000004__age_50": "50 years", + "n05000004__age_51": "51 years", + "n05000004__age_52": "52 years", + "n05000004__age_53": "53 years", + "n05000004__age_54": "54 years", + "n05000004__age_55": "55 years", + "n05000004__age_56": "56 years", + "n05000004__age_57": "57 years", + "n05000004__age_58": "58 years", + "n05000004__age_59": "59 years", + "n05000004__age_6": "6 years", + "n05000004__age_60": "60 years", + "n05000004__age_61": "61 years", + "n05000004__age_62": "62 years", + "n05000004__age_63": "63 years", + "n05000004__age_64": "64 years", + "n05000004__age_65": "65 years", + "n05000004__age_66": "66 years", + "n05000004__age_67": "67 years", + "n05000004__age_68": "68 years", + "n05000004__age_69": "69 years", + "n05000004__age_7": "7 years", + "n05000004__age_70": "70 years", + "n05000004__age_71": "71 years", + "n05000004__age_72": "72 years", + "n05000004__age_73": "73 years", + "n05000004__age_74": "74 years", + "n05000004__age_75": "75 years", + "n05000004__age_76": "76 years", + "n05000004__age_77": "77 years", + "n05000004__age_78": "78 years", + "n05000004__age_79": "79 years", + "n05000004__age_8": "8 years", + "n05000004__age_80": "80 years", + "n05000004__age_81": "81 years", + "n05000004__age_82": "82 years", + "n05000004__age_83": "83 years", + "n05000004__age_84": "84 years", + "n05000004__age_85": "85 years", + "n05000004__age_86": "86 years", + "n05000004__age_87": "87 years", + "n05000004__age_88": "88 years", + "n05000004__age_89": "89 years", + "n05000004__age_9": "9 years", + "n05000004__age_90": "90 years", + "n05000004__age_91": "91 years", + "n05000004__age_92": "92 years", + "n05000004__age_93": "93 years", + "n05000004__age_94": "94 years", + "n05000004__age_95": "95 years", + "n05000004__age_96": "96 years", + "n05000004__age_97": "97 years", + "n05000004__age_98": "98 years", + "n05000004__age_99": "99 years", + "n05000005__age_0": "0 years", + "n05000005__age_1": "1 year", + "n05000005__age_10": "10 years", + "n05000005__age_100": "100+ years", + "n05000005__age_11": "11 years", + "n05000005__age_12": "12 years", + "n05000005__age_13": "13 years", + "n05000005__age_14": "14 years", + "n05000005__age_15": "15 years", + "n05000005__age_16": "16 years", + "n05000005__age_17": "17 years", + "n05000005__age_18": "18 years", + "n05000005__age_19": "19 years", + "n05000005__age_2": "2 years", + "n05000005__age_20": "20 years", + "n05000005__age_21": "21 years", + "n05000005__age_22": "22 years", + "n05000005__age_23": "23 years", + "n05000005__age_24": "24 years", + "n05000005__age_25": "25 years", + "n05000005__age_26": "26 years", + "n05000005__age_27": "27 years", + "n05000005__age_28": "28 years", + "n05000005__age_29": "29 years", + "n05000005__age_3": "3 years", + "n05000005__age_30": "30 years", + "n05000005__age_31": "31 years", + "n05000005__age_32": "32 years", + "n05000005__age_33": "33 years", + "n05000005__age_34": "34 years", + "n05000005__age_35": "35 years", + "n05000005__age_36": "36 years", + "n05000005__age_37": "37 years", + "n05000005__age_38": "38 years", + "n05000005__age_39": "39 years", + "n05000005__age_4": "4 years", + "n05000005__age_40": "40 years", + "n05000005__age_41": "41 years", + "n05000005__age_42": "42 years", + "n05000005__age_43": "43 years", + "n05000005__age_44": "44 years", + "n05000005__age_45": "45 years", + "n05000005__age_46": "46 years", + "n05000005__age_47": "47 years", + "n05000005__age_48": "48 years", + "n05000005__age_49": "49 years", + "n05000005__age_5": "5 years", + "n05000005__age_50": "50 years", + "n05000005__age_51": "51 years", + "n05000005__age_52": "52 years", + "n05000005__age_53": "53 years", + "n05000005__age_54": "54 years", + "n05000005__age_55": "55 years", + "n05000005__age_56": "56 years", + "n05000005__age_57": "57 years", + "n05000005__age_58": "58 years", + "n05000005__age_59": "59 years", + "n05000005__age_6": "6 years", + "n05000005__age_60": "60 years", + "n05000005__age_61": "61 years", + "n05000005__age_62": "62 years", + "n05000005__age_63": "63 years", + "n05000005__age_64": "64 years", + "n05000005__age_65": "65 years", + "n05000005__age_66": "66 years", + "n05000005__age_67": "67 years", + "n05000005__age_68": "68 years", + "n05000005__age_69": "69 years", + "n05000005__age_7": "7 years", + "n05000005__age_70": "70 years", + "n05000005__age_71": "71 years", + "n05000005__age_72": "72 years", + "n05000005__age_73": "73 years", + "n05000005__age_74": "74 years", + "n05000005__age_75": "75 years", + "n05000005__age_76": "76 years", + "n05000005__age_77": "77 years", + "n05000005__age_78": "78 years", + "n05000005__age_79": "79 years", + "n05000005__age_8": "8 years", + "n05000005__age_80": "80 years", + "n05000005__age_81": "81 years", + "n05000005__age_82": "82 years", + "n05000005__age_83": "83 years", + "n05000005__age_84": "84 years", + "n05000005__age_85": "85 years", + "n05000005__age_86": "86 years", + "n05000005__age_87": "87 years", + "n05000005__age_88": "88 years", + "n05000005__age_89": "89 years", + "n05000005__age_9": "9 years", + "n05000005__age_90": "90 years", + "n05000005__age_91": "91 years", + "n05000005__age_92": "92 years", + "n05000005__age_93": "93 years", + "n05000005__age_94": "94 years", + "n05000005__age_95": "95 years", + "n05000005__age_96": "96 years", + "n05000005__age_97": "97 years", + "n05000005__age_98": "98 years", + "n05000005__age_99": "99 years", + "n05000006__age_0": "0 years", + "n05000006__age_1": "1 year", + "n05000006__age_10": "10 years", + "n05000006__age_100": "100+ years", + "n05000006__age_11": "11 years", + "n05000006__age_12": "12 years", + "n05000006__age_13": "13 years", + "n05000006__age_14": "14 years", + "n05000006__age_15": "15 years", + "n05000006__age_16": "16 years", + "n05000006__age_17": "17 years", + "n05000006__age_18": "18 years", + "n05000006__age_19": "19 years", + "n05000006__age_2": "2 years", + "n05000006__age_20": "20 years", + "n05000006__age_21": "21 years", + "n05000006__age_22": "22 years", + "n05000006__age_23": "23 years", + "n05000006__age_24": "24 years", + "n05000006__age_25": "25 years", + "n05000006__age_26": "26 years", + "n05000006__age_27": "27 years", + "n05000006__age_28": "28 years", + "n05000006__age_29": "29 years", + "n05000006__age_3": "3 years", + "n05000006__age_30": "30 years", + "n05000006__age_31": "31 years", + "n05000006__age_32": "32 years", + "n05000006__age_33": "33 years", + "n05000006__age_34": "34 years", + "n05000006__age_35": "35 years", + "n05000006__age_36": "36 years", + "n05000006__age_37": "37 years", + "n05000006__age_38": "38 years", + "n05000006__age_39": "39 years", + "n05000006__age_4": "4 years", + "n05000006__age_40": "40 years", + "n05000006__age_41": "41 years", + "n05000006__age_42": "42 years", + "n05000006__age_43": "43 years", + "n05000006__age_44": "44 years", + "n05000006__age_45": "45 years", + "n05000006__age_46": "46 years", + "n05000006__age_47": "47 years", + "n05000006__age_48": "48 years", + "n05000006__age_49": "49 years", + "n05000006__age_5": "5 years", + "n05000006__age_50": "50 years", + "n05000006__age_51": "51 years", + "n05000006__age_52": "52 years", + "n05000006__age_53": "53 years", + "n05000006__age_54": "54 years", + "n05000006__age_55": "55 years", + "n05000006__age_56": "56 years", + "n05000006__age_57": "57 years", + "n05000006__age_58": "58 years", + "n05000006__age_59": "59 years", + "n05000006__age_6": "6 years", + "n05000006__age_60": "60 years", + "n05000006__age_61": "61 years", + "n05000006__age_62": "62 years", + "n05000006__age_63": "63 years", + "n05000006__age_64": "64 years", + "n05000006__age_65": "65 years", + "n05000006__age_66": "66 years", + "n05000006__age_67": "67 years", + "n05000006__age_68": "68 years", + "n05000006__age_69": "69 years", + "n05000006__age_7": "7 years", + "n05000006__age_70": "70 years", + "n05000006__age_71": "71 years", + "n05000006__age_72": "72 years", + "n05000006__age_73": "73 years", + "n05000006__age_74": "74 years", + "n05000006__age_75": "75 years", + "n05000006__age_76": "76 years", + "n05000006__age_77": "77 years", + "n05000006__age_78": "78 years", + "n05000006__age_79": "79 years", + "n05000006__age_8": "8 years", + "n05000006__age_80": "80 years", + "n05000006__age_81": "81 years", + "n05000006__age_82": "82 years", + "n05000006__age_83": "83 years", + "n05000006__age_84": "84 years", + "n05000006__age_85": "85 years", + "n05000006__age_86": "86 years", + "n05000006__age_87": "87 years", + "n05000006__age_88": "88 years", + "n05000006__age_89": "89 years", + "n05000006__age_9": "9 years", + "n05000006__age_90": "90 years", + "n05000006__age_91": "91 years", + "n05000006__age_92": "92 years", + "n05000006__age_93": "93 years", + "n05000006__age_94": "94 years", + "n05000006__age_95": "95 years", + "n05000006__age_96": "96 years", + "n05000006__age_97": "97 years", + "n05000006__age_98": "98 years", + "n05000006__age_99": "99 years", + "n05000007__age_0": "0 years", + "n05000007__age_1": "1 year", + "n05000007__age_10": "10 years", + "n05000007__age_100": "100+ years", + "n05000007__age_11": "11 years", + "n05000007__age_12": "12 years", + "n05000007__age_13": "13 years", + "n05000007__age_14": "14 years", + "n05000007__age_15": "15 years", + "n05000007__age_16": "16 years", + "n05000007__age_17": "17 years", + "n05000007__age_18": "18 years", + "n05000007__age_19": "19 years", + "n05000007__age_2": "2 years", + "n05000007__age_20": "20 years", + "n05000007__age_21": "21 years", + "n05000007__age_22": "22 years", + "n05000007__age_23": "23 years", + "n05000007__age_24": "24 years", + "n05000007__age_25": "25 years", + "n05000007__age_26": "26 years", + "n05000007__age_27": "27 years", + "n05000007__age_28": "28 years", + "n05000007__age_29": "29 years", + "n05000007__age_3": "3 years", + "n05000007__age_30": "30 years", + "n05000007__age_31": "31 years", + "n05000007__age_32": "32 years", + "n05000007__age_33": "33 years", + "n05000007__age_34": "34 years", + "n05000007__age_35": "35 years", + "n05000007__age_36": "36 years", + "n05000007__age_37": "37 years", + "n05000007__age_38": "38 years", + "n05000007__age_39": "39 years", + "n05000007__age_4": "4 years", + "n05000007__age_40": "40 years", + "n05000007__age_41": "41 years", + "n05000007__age_42": "42 years", + "n05000007__age_43": "43 years", + "n05000007__age_44": "44 years", + "n05000007__age_45": "45 years", + "n05000007__age_46": "46 years", + "n05000007__age_47": "47 years", + "n05000007__age_48": "48 years", + "n05000007__age_49": "49 years", + "n05000007__age_5": "5 years", + "n05000007__age_50": "50 years", + "n05000007__age_51": "51 years", + "n05000007__age_52": "52 years", + "n05000007__age_53": "53 years", + "n05000007__age_54": "54 years", + "n05000007__age_55": "55 years", + "n05000007__age_56": "56 years", + "n05000007__age_57": "57 years", + "n05000007__age_58": "58 years", + "n05000007__age_59": "59 years", + "n05000007__age_6": "6 years", + "n05000007__age_60": "60 years", + "n05000007__age_61": "61 years", + "n05000007__age_62": "62 years", + "n05000007__age_63": "63 years", + "n05000007__age_64": "64 years", + "n05000007__age_65": "65 years", + "n05000007__age_66": "66 years", + "n05000007__age_67": "67 years", + "n05000007__age_68": "68 years", + "n05000007__age_69": "69 years", + "n05000007__age_7": "7 years", + "n05000007__age_70": "70 years", + "n05000007__age_71": "71 years", + "n05000007__age_72": "72 years", + "n05000007__age_73": "73 years", + "n05000007__age_74": "74 years", + "n05000007__age_75": "75 years", + "n05000007__age_76": "76 years", + "n05000007__age_77": "77 years", + "n05000007__age_78": "78 years", + "n05000007__age_79": "79 years", + "n05000007__age_8": "8 years", + "n05000007__age_80": "80 years", + "n05000007__age_81": "81 years", + "n05000007__age_82": "82 years", + "n05000007__age_83": "83 years", + "n05000007__age_84": "84 years", + "n05000007__age_85": "85 years", + "n05000007__age_86": "86 years", + "n05000007__age_87": "87 years", + "n05000007__age_88": "88 years", + "n05000007__age_89": "89 years", + "n05000007__age_9": "9 years", + "n05000007__age_90": "90 years", + "n05000007__age_91": "91 years", + "n05000007__age_92": "92 years", + "n05000007__age_93": "93 years", + "n05000007__age_94": "94 years", + "n05000007__age_95": "95 years", + "n05000007__age_96": "96 years", + "n05000007__age_97": "97 years", + "n05000007__age_98": "98 years", + "n05000007__age_99": "99 years", + "n05000008__age_0": "0 years", + "n05000008__age_1": "1 year", + "n05000008__age_10": "10 years", + "n05000008__age_100": "100+ years", + "n05000008__age_11": "11 years", + "n05000008__age_12": "12 years", + "n05000008__age_13": "13 years", + "n05000008__age_14": "14 years", + "n05000008__age_15": "15 years", + "n05000008__age_16": "16 years", + "n05000008__age_17": "17 years", + "n05000008__age_18": "18 years", + "n05000008__age_19": "19 years", + "n05000008__age_2": "2 years", + "n05000008__age_20": "20 years", + "n05000008__age_21": "21 years", + "n05000008__age_22": "22 years", + "n05000008__age_23": "23 years", + "n05000008__age_24": "24 years", + "n05000008__age_25": "25 years", + "n05000008__age_26": "26 years", + "n05000008__age_27": "27 years", + "n05000008__age_28": "28 years", + "n05000008__age_29": "29 years", + "n05000008__age_3": "3 years", + "n05000008__age_30": "30 years", + "n05000008__age_31": "31 years", + "n05000008__age_32": "32 years", + "n05000008__age_33": "33 years", + "n05000008__age_34": "34 years", + "n05000008__age_35": "35 years", + "n05000008__age_36": "36 years", + "n05000008__age_37": "37 years", + "n05000008__age_38": "38 years", + "n05000008__age_39": "39 years", + "n05000008__age_4": "4 years", + "n05000008__age_40": "40 years", + "n05000008__age_41": "41 years", + "n05000008__age_42": "42 years", + "n05000008__age_43": "43 years", + "n05000008__age_44": "44 years", + "n05000008__age_45": "45 years", + "n05000008__age_46": "46 years", + "n05000008__age_47": "47 years", + "n05000008__age_48": "48 years", + "n05000008__age_49": "49 years", + "n05000008__age_5": "5 years", + "n05000008__age_50": "50 years", + "n05000008__age_51": "51 years", + "n05000008__age_52": "52 years", + "n05000008__age_53": "53 years", + "n05000008__age_54": "54 years", + "n05000008__age_55": "55 years", + "n05000008__age_56": "56 years", + "n05000008__age_57": "57 years", + "n05000008__age_58": "58 years", + "n05000008__age_59": "59 years", + "n05000008__age_6": "6 years", + "n05000008__age_60": "60 years", + "n05000008__age_61": "61 years", + "n05000008__age_62": "62 years", + "n05000008__age_63": "63 years", + "n05000008__age_64": "64 years", + "n05000008__age_65": "65 years", + "n05000008__age_66": "66 years", + "n05000008__age_67": "67 years", + "n05000008__age_68": "68 years", + "n05000008__age_69": "69 years", + "n05000008__age_7": "7 years", + "n05000008__age_70": "70 years", + "n05000008__age_71": "71 years", + "n05000008__age_72": "72 years", + "n05000008__age_73": "73 years", + "n05000008__age_74": "74 years", + "n05000008__age_75": "75 years", + "n05000008__age_76": "76 years", + "n05000008__age_77": "77 years", + "n05000008__age_78": "78 years", + "n05000008__age_79": "79 years", + "n05000008__age_8": "8 years", + "n05000008__age_80": "80 years", + "n05000008__age_81": "81 years", + "n05000008__age_82": "82 years", + "n05000008__age_83": "83 years", + "n05000008__age_84": "84 years", + "n05000008__age_85": "85 years", + "n05000008__age_86": "86 years", + "n05000008__age_87": "87 years", + "n05000008__age_88": "88 years", + "n05000008__age_89": "89 years", + "n05000008__age_9": "9 years", + "n05000008__age_90": "90 years", + "n05000008__age_91": "91 years", + "n05000008__age_92": "92 years", + "n05000008__age_93": "93 years", + "n05000008__age_94": "94 years", + "n05000008__age_95": "95 years", + "n05000008__age_96": "96 years", + "n05000008__age_97": "97 years", + "n05000008__age_98": "98 years", + "n05000008__age_99": "99 years", + "n05000009__age_0": "0 years", + "n05000009__age_1": "1 year", + "n05000009__age_10": "10 years", + "n05000009__age_100": "100+ years", + "n05000009__age_11": "11 years", + "n05000009__age_12": "12 years", + "n05000009__age_13": "13 years", + "n05000009__age_14": "14 years", + "n05000009__age_15": "15 years", + "n05000009__age_16": "16 years", + "n05000009__age_17": "17 years", + "n05000009__age_18": "18 years", + "n05000009__age_19": "19 years", + "n05000009__age_2": "2 years", + "n05000009__age_20": "20 years", + "n05000009__age_21": "21 years", + "n05000009__age_22": "22 years", + "n05000009__age_23": "23 years", + "n05000009__age_24": "24 years", + "n05000009__age_25": "25 years", + "n05000009__age_26": "26 years", + "n05000009__age_27": "27 years", + "n05000009__age_28": "28 years", + "n05000009__age_29": "29 years", + "n05000009__age_3": "3 years", + "n05000009__age_30": "30 years", + "n05000009__age_31": "31 years", + "n05000009__age_32": "32 years", + "n05000009__age_33": "33 years", + "n05000009__age_34": "34 years", + "n05000009__age_35": "35 years", + "n05000009__age_36": "36 years", + "n05000009__age_37": "37 years", + "n05000009__age_38": "38 years", + "n05000009__age_39": "39 years", + "n05000009__age_4": "4 years", + "n05000009__age_40": "40 years", + "n05000009__age_41": "41 years", + "n05000009__age_42": "42 years", + "n05000009__age_43": "43 years", + "n05000009__age_44": "44 years", + "n05000009__age_45": "45 years", + "n05000009__age_46": "46 years", + "n05000009__age_47": "47 years", + "n05000009__age_48": "48 years", + "n05000009__age_49": "49 years", + "n05000009__age_5": "5 years", + "n05000009__age_50": "50 years", + "n05000009__age_51": "51 years", + "n05000009__age_52": "52 years", + "n05000009__age_53": "53 years", + "n05000009__age_54": "54 years", + "n05000009__age_55": "55 years", + "n05000009__age_56": "56 years", + "n05000009__age_57": "57 years", + "n05000009__age_58": "58 years", + "n05000009__age_59": "59 years", + "n05000009__age_6": "6 years", + "n05000009__age_60": "60 years", + "n05000009__age_61": "61 years", + "n05000009__age_62": "62 years", + "n05000009__age_63": "63 years", + "n05000009__age_64": "64 years", + "n05000009__age_65": "65 years", + "n05000009__age_66": "66 years", + "n05000009__age_67": "67 years", + "n05000009__age_68": "68 years", + "n05000009__age_69": "69 years", + "n05000009__age_7": "7 years", + "n05000009__age_70": "70 years", + "n05000009__age_71": "71 years", + "n05000009__age_72": "72 years", + "n05000009__age_73": "73 years", + "n05000009__age_74": "74 years", + "n05000009__age_75": "75 years", + "n05000009__age_76": "76 years", + "n05000009__age_77": "77 years", + "n05000009__age_78": "78 years", + "n05000009__age_79": "79 years", + "n05000009__age_8": "8 years", + "n05000009__age_80": "80 years", + "n05000009__age_81": "81 years", + "n05000009__age_82": "82 years", + "n05000009__age_83": "83 years", + "n05000009__age_84": "84 years", + "n05000009__age_85": "85 years", + "n05000009__age_86": "86 years", + "n05000009__age_87": "87 years", + "n05000009__age_88": "88 years", + "n05000009__age_89": "89 years", + "n05000009__age_9": "9 years", + "n05000009__age_90": "90 years", + "n05000009__age_91": "91 years", + "n05000009__age_92": "92 years", + "n05000009__age_93": "93 years", + "n05000009__age_94": "94 years", + "n05000009__age_95": "95 years", + "n05000009__age_96": "96 years", + "n05000009__age_97": "97 years", + "n05000009__age_98": "98 years", + "n05000009__age_99": "99 years", + "n05000010__age_0": "0 years", + "n05000010__age_1": "1 year", + "n05000010__age_10": "10 years", + "n05000010__age_100": "100+ years", + "n05000010__age_11": "11 years", + "n05000010__age_12": "12 years", + "n05000010__age_13": "13 years", + "n05000010__age_14": "14 years", + "n05000010__age_15": "15 years", + "n05000010__age_16": "16 years", + "n05000010__age_17": "17 years", + "n05000010__age_18": "18 years", + "n05000010__age_19": "19 years", + "n05000010__age_2": "2 years", + "n05000010__age_20": "20 years", + "n05000010__age_21": "21 years", + "n05000010__age_22": "22 years", + "n05000010__age_23": "23 years", + "n05000010__age_24": "24 years", + "n05000010__age_25": "25 years", + "n05000010__age_26": "26 years", + "n05000010__age_27": "27 years", + "n05000010__age_28": "28 years", + "n05000010__age_29": "29 years", + "n05000010__age_3": "3 years", + "n05000010__age_30": "30 years", + "n05000010__age_31": "31 years", + "n05000010__age_32": "32 years", + "n05000010__age_33": "33 years", + "n05000010__age_34": "34 years", + "n05000010__age_35": "35 years", + "n05000010__age_36": "36 years", + "n05000010__age_37": "37 years", + "n05000010__age_38": "38 years", + "n05000010__age_39": "39 years", + "n05000010__age_4": "4 years", + "n05000010__age_40": "40 years", + "n05000010__age_41": "41 years", + "n05000010__age_42": "42 years", + "n05000010__age_43": "43 years", + "n05000010__age_44": "44 years", + "n05000010__age_45": "45 years", + "n05000010__age_46": "46 years", + "n05000010__age_47": "47 years", + "n05000010__age_48": "48 years", + "n05000010__age_49": "49 years", + "n05000010__age_5": "5 years", + "n05000010__age_50": "50 years", + "n05000010__age_51": "51 years", + "n05000010__age_52": "52 years", + "n05000010__age_53": "53 years", + "n05000010__age_54": "54 years", + "n05000010__age_55": "55 years", + "n05000010__age_56": "56 years", + "n05000010__age_57": "57 years", + "n05000010__age_58": "58 years", + "n05000010__age_59": "59 years", + "n05000010__age_6": "6 years", + "n05000010__age_60": "60 years", + "n05000010__age_61": "61 years", + "n05000010__age_62": "62 years", + "n05000010__age_63": "63 years", + "n05000010__age_64": "64 years", + "n05000010__age_65": "65 years", + "n05000010__age_66": "66 years", + "n05000010__age_67": "67 years", + "n05000010__age_68": "68 years", + "n05000010__age_69": "69 years", + "n05000010__age_7": "7 years", + "n05000010__age_70": "70 years", + "n05000010__age_71": "71 years", + "n05000010__age_72": "72 years", + "n05000010__age_73": "73 years", + "n05000010__age_74": "74 years", + "n05000010__age_75": "75 years", + "n05000010__age_76": "76 years", + "n05000010__age_77": "77 years", + "n05000010__age_78": "78 years", + "n05000010__age_79": "79 years", + "n05000010__age_8": "8 years", + "n05000010__age_80": "80 years", + "n05000010__age_81": "81 years", + "n05000010__age_82": "82 years", + "n05000010__age_83": "83 years", + "n05000010__age_84": "84 years", + "n05000010__age_85": "85 years", + "n05000010__age_86": "86 years", + "n05000010__age_87": "87 years", + "n05000010__age_88": "88 years", + "n05000010__age_89": "89 years", + "n05000010__age_9": "9 years", + "n05000010__age_90": "90 years", + "n05000010__age_91": "91 years", + "n05000010__age_92": "92 years", + "n05000010__age_93": "93 years", + "n05000010__age_94": "94 years", + "n05000010__age_95": "95 years", + "n05000010__age_96": "96 years", + "n05000010__age_97": "97 years", + "n05000010__age_98": "98 years", + "n05000010__age_99": "99 years", + "n05000011__age_0": "0 years", + "n05000011__age_1": "1 year", + "n05000011__age_10": "10 years", + "n05000011__age_100": "100+ years", + "n05000011__age_11": "11 years", + "n05000011__age_12": "12 years", + "n05000011__age_13": "13 years", + "n05000011__age_14": "14 years", + "n05000011__age_15": "15 years", + "n05000011__age_16": "16 years", + "n05000011__age_17": "17 years", + "n05000011__age_18": "18 years", + "n05000011__age_19": "19 years", + "n05000011__age_2": "2 years", + "n05000011__age_20": "20 years", + "n05000011__age_21": "21 years", + "n05000011__age_22": "22 years", + "n05000011__age_23": "23 years", + "n05000011__age_24": "24 years", + "n05000011__age_25": "25 years", + "n05000011__age_26": "26 years", + "n05000011__age_27": "27 years", + "n05000011__age_28": "28 years", + "n05000011__age_29": "29 years", + "n05000011__age_3": "3 years", + "n05000011__age_30": "30 years", + "n05000011__age_31": "31 years", + "n05000011__age_32": "32 years", + "n05000011__age_33": "33 years", + "n05000011__age_34": "34 years", + "n05000011__age_35": "35 years", + "n05000011__age_36": "36 years", + "n05000011__age_37": "37 years", + "n05000011__age_38": "38 years", + "n05000011__age_39": "39 years", + "n05000011__age_4": "4 years", + "n05000011__age_40": "40 years", + "n05000011__age_41": "41 years", + "n05000011__age_42": "42 years", + "n05000011__age_43": "43 years", + "n05000011__age_44": "44 years", + "n05000011__age_45": "45 years", + "n05000011__age_46": "46 years", + "n05000011__age_47": "47 years", + "n05000011__age_48": "48 years", + "n05000011__age_49": "49 years", + "n05000011__age_5": "5 years", + "n05000011__age_50": "50 years", + "n05000011__age_51": "51 years", + "n05000011__age_52": "52 years", + "n05000011__age_53": "53 years", + "n05000011__age_54": "54 years", + "n05000011__age_55": "55 years", + "n05000011__age_56": "56 years", + "n05000011__age_57": "57 years", + "n05000011__age_58": "58 years", + "n05000011__age_59": "59 years", + "n05000011__age_6": "6 years", + "n05000011__age_60": "60 years", + "n05000011__age_61": "61 years", + "n05000011__age_62": "62 years", + "n05000011__age_63": "63 years", + "n05000011__age_64": "64 years", + "n05000011__age_65": "65 years", + "n05000011__age_66": "66 years", + "n05000011__age_67": "67 years", + "n05000011__age_68": "68 years", + "n05000011__age_69": "69 years", + "n05000011__age_7": "7 years", + "n05000011__age_70": "70 years", + "n05000011__age_71": "71 years", + "n05000011__age_72": "72 years", + "n05000011__age_73": "73 years", + "n05000011__age_74": "74 years", + "n05000011__age_75": "75 years", + "n05000011__age_76": "76 years", + "n05000011__age_77": "77 years", + "n05000011__age_78": "78 years", + "n05000011__age_79": "79 years", + "n05000011__age_8": "8 years", + "n05000011__age_80": "80 years", + "n05000011__age_81": "81 years", + "n05000011__age_82": "82 years", + "n05000011__age_83": "83 years", + "n05000011__age_84": "84 years", + "n05000011__age_85": "85 years", + "n05000011__age_86": "86 years", + "n05000011__age_87": "87 years", + "n05000011__age_88": "88 years", + "n05000011__age_89": "89 years", + "n05000011__age_9": "9 years", + "n05000011__age_90": "90 years", + "n05000011__age_91": "91 years", + "n05000011__age_92": "92 years", + "n05000011__age_93": "93 years", + "n05000011__age_94": "94 years", + "n05000011__age_95": "95 years", + "n05000011__age_96": "96 years", + "n05000011__age_97": "97 years", + "n05000011__age_98": "98 years", + "n05000011__age_99": "99 years", + "n05000012__age_0": "0 years", + "n05000012__age_1": "1 year", + "n05000012__age_10": "10 years", + "n05000012__age_100": "100+ years", + "n05000012__age_11": "11 years", + "n05000012__age_12": "12 years", + "n05000012__age_13": "13 years", + "n05000012__age_14": "14 years", + "n05000012__age_15": "15 years", + "n05000012__age_16": "16 years", + "n05000012__age_17": "17 years", + "n05000012__age_18": "18 years", + "n05000012__age_19": "19 years", + "n05000012__age_2": "2 years", + "n05000012__age_20": "20 years", + "n05000012__age_21": "21 years", + "n05000012__age_22": "22 years", + "n05000012__age_23": "23 years", + "n05000012__age_24": "24 years", + "n05000012__age_25": "25 years", + "n05000012__age_26": "26 years", + "n05000012__age_27": "27 years", + "n05000012__age_28": "28 years", + "n05000012__age_29": "29 years", + "n05000012__age_3": "3 years", + "n05000012__age_30": "30 years", + "n05000012__age_31": "31 years", + "n05000012__age_32": "32 years", + "n05000012__age_33": "33 years", + "n05000012__age_34": "34 years", + "n05000012__age_35": "35 years", + "n05000012__age_36": "36 years", + "n05000012__age_37": "37 years", + "n05000012__age_38": "38 years", + "n05000012__age_39": "39 years", + "n05000012__age_4": "4 years", + "n05000012__age_40": "40 years", + "n05000012__age_41": "41 years", + "n05000012__age_42": "42 years", + "n05000012__age_43": "43 years", + "n05000012__age_44": "44 years", + "n05000012__age_45": "45 years", + "n05000012__age_46": "46 years", + "n05000012__age_47": "47 years", + "n05000012__age_48": "48 years", + "n05000012__age_49": "49 years", + "n05000012__age_5": "5 years", + "n05000012__age_50": "50 years", + "n05000012__age_51": "51 years", + "n05000012__age_52": "52 years", + "n05000012__age_53": "53 years", + "n05000012__age_54": "54 years", + "n05000012__age_55": "55 years", + "n05000012__age_56": "56 years", + "n05000012__age_57": "57 years", + "n05000012__age_58": "58 years", + "n05000012__age_59": "59 years", + "n05000012__age_6": "6 years", + "n05000012__age_60": "60 years", + "n05000012__age_61": "61 years", + "n05000012__age_62": "62 years", + "n05000012__age_63": "63 years", + "n05000012__age_64": "64 years", + "n05000012__age_65": "65 years", + "n05000012__age_66": "66 years", + "n05000012__age_67": "67 years", + "n05000012__age_68": "68 years", + "n05000012__age_69": "69 years", + "n05000012__age_7": "7 years", + "n05000012__age_70": "70 years", + "n05000012__age_71": "71 years", + "n05000012__age_72": "72 years", + "n05000012__age_73": "73 years", + "n05000012__age_74": "74 years", + "n05000012__age_75": "75 years", + "n05000012__age_76": "76 years", + "n05000012__age_77": "77 years", + "n05000012__age_78": "78 years", + "n05000012__age_79": "79 years", + "n05000012__age_8": "8 years", + "n05000012__age_80": "80 years", + "n05000012__age_81": "81 years", + "n05000012__age_82": "82 years", + "n05000012__age_83": "83 years", + "n05000012__age_84": "84 years", + "n05000012__age_85": "85 years", + "n05000012__age_86": "86 years", + "n05000012__age_87": "87 years", + "n05000012__age_88": "88 years", + "n05000012__age_89": "89 years", + "n05000012__age_9": "9 years", + "n05000012__age_90": "90 years", + "n05000012__age_91": "91 years", + "n05000012__age_92": "92 years", + "n05000012__age_93": "93 years", + "n05000012__age_94": "94 years", + "n05000012__age_95": "95 years", + "n05000012__age_96": "96 years", + "n05000012__age_97": "97 years", + "n05000012__age_98": "98 years", + "n05000012__age_99": "99 years", + "n05000013__age_0": "0 years", + "n05000013__age_1": "1 year", + "n05000013__age_10": "10 years", + "n05000013__age_100": "100+ years", + "n05000013__age_11": "11 years", + "n05000013__age_12": "12 years", + "n05000013__age_13": "13 years", + "n05000013__age_14": "14 years", + "n05000013__age_15": "15 years", + "n05000013__age_16": "16 years", + "n05000013__age_17": "17 years", + "n05000013__age_18": "18 years", + "n05000013__age_19": "19 years", + "n05000013__age_2": "2 years", + "n05000013__age_20": "20 years", + "n05000013__age_21": "21 years", + "n05000013__age_22": "22 years", + "n05000013__age_23": "23 years", + "n05000013__age_24": "24 years", + "n05000013__age_25": "25 years", + "n05000013__age_26": "26 years", + "n05000013__age_27": "27 years", + "n05000013__age_28": "28 years", + "n05000013__age_29": "29 years", + "n05000013__age_3": "3 years", + "n05000013__age_30": "30 years", + "n05000013__age_31": "31 years", + "n05000013__age_32": "32 years", + "n05000013__age_33": "33 years", + "n05000013__age_34": "34 years", + "n05000013__age_35": "35 years", + "n05000013__age_36": "36 years", + "n05000013__age_37": "37 years", + "n05000013__age_38": "38 years", + "n05000013__age_39": "39 years", + "n05000013__age_4": "4 years", + "n05000013__age_40": "40 years", + "n05000013__age_41": "41 years", + "n05000013__age_42": "42 years", + "n05000013__age_43": "43 years", + "n05000013__age_44": "44 years", + "n05000013__age_45": "45 years", + "n05000013__age_46": "46 years", + "n05000013__age_47": "47 years", + "n05000013__age_48": "48 years", + "n05000013__age_49": "49 years", + "n05000013__age_5": "5 years", + "n05000013__age_50": "50 years", + "n05000013__age_51": "51 years", + "n05000013__age_52": "52 years", + "n05000013__age_53": "53 years", + "n05000013__age_54": "54 years", + "n05000013__age_55": "55 years", + "n05000013__age_56": "56 years", + "n05000013__age_57": "57 years", + "n05000013__age_58": "58 years", + "n05000013__age_59": "59 years", + "n05000013__age_6": "6 years", + "n05000013__age_60": "60 years", + "n05000013__age_61": "61 years", + "n05000013__age_62": "62 years", + "n05000013__age_63": "63 years", + "n05000013__age_64": "64 years", + "n05000013__age_65": "65 years", + "n05000013__age_66": "66 years", + "n05000013__age_67": "67 years", + "n05000013__age_68": "68 years", + "n05000013__age_69": "69 years", + "n05000013__age_7": "7 years", + "n05000013__age_70": "70 years", + "n05000013__age_71": "71 years", + "n05000013__age_72": "72 years", + "n05000013__age_73": "73 years", + "n05000013__age_74": "74 years", + "n05000013__age_75": "75 years", + "n05000013__age_76": "76 years", + "n05000013__age_77": "77 years", + "n05000013__age_78": "78 years", + "n05000013__age_79": "79 years", + "n05000013__age_8": "8 years", + "n05000013__age_80": "80 years", + "n05000013__age_81": "81 years", + "n05000013__age_82": "82 years", + "n05000013__age_83": "83 years", + "n05000013__age_84": "84 years", + "n05000013__age_85": "85 years", + "n05000013__age_86": "86 years", + "n05000013__age_87": "87 years", + "n05000013__age_88": "88 years", + "n05000013__age_89": "89 years", + "n05000013__age_9": "9 years", + "n05000013__age_90": "90 years", + "n05000013__age_91": "91 years", + "n05000013__age_92": "92 years", + "n05000013__age_93": "93 years", + "n05000013__age_94": "94 years", + "n05000013__age_95": "95 years", + "n05000013__age_96": "96 years", + "n05000013__age_97": "97 years", + "n05000013__age_98": "98 years", + "n05000013__age_99": "99 years", + "n05000014__age_0": "0 years", + "n05000014__age_1": "1 year", + "n05000014__age_10": "10 years", + "n05000014__age_100": "100+ years", + "n05000014__age_11": "11 years", + "n05000014__age_12": "12 years", + "n05000014__age_13": "13 years", + "n05000014__age_14": "14 years", + "n05000014__age_15": "15 years", + "n05000014__age_16": "16 years", + "n05000014__age_17": "17 years", + "n05000014__age_18": "18 years", + "n05000014__age_19": "19 years", + "n05000014__age_2": "2 years", + "n05000014__age_20": "20 years", + "n05000014__age_21": "21 years", + "n05000014__age_22": "22 years", + "n05000014__age_23": "23 years", + "n05000014__age_24": "24 years", + "n05000014__age_25": "25 years", + "n05000014__age_26": "26 years", + "n05000014__age_27": "27 years", + "n05000014__age_28": "28 years", + "n05000014__age_29": "29 years", + "n05000014__age_3": "3 years", + "n05000014__age_30": "30 years", + "n05000014__age_31": "31 years", + "n05000014__age_32": "32 years", + "n05000014__age_33": "33 years", + "n05000014__age_34": "34 years", + "n05000014__age_35": "35 years", + "n05000014__age_36": "36 years", + "n05000014__age_37": "37 years", + "n05000014__age_38": "38 years", + "n05000014__age_39": "39 years", + "n05000014__age_4": "4 years", + "n05000014__age_40": "40 years", + "n05000014__age_41": "41 years", + "n05000014__age_42": "42 years", + "n05000014__age_43": "43 years", + "n05000014__age_44": "44 years", + "n05000014__age_45": "45 years", + "n05000014__age_46": "46 years", + "n05000014__age_47": "47 years", + "n05000014__age_48": "48 years", + "n05000014__age_49": "49 years", + "n05000014__age_5": "5 years", + "n05000014__age_50": "50 years", + "n05000014__age_51": "51 years", + "n05000014__age_52": "52 years", + "n05000014__age_53": "53 years", + "n05000014__age_54": "54 years", + "n05000014__age_55": "55 years", + "n05000014__age_56": "56 years", + "n05000014__age_57": "57 years", + "n05000014__age_58": "58 years", + "n05000014__age_59": "59 years", + "n05000014__age_6": "6 years", + "n05000014__age_60": "60 years", + "n05000014__age_61": "61 years", + "n05000014__age_62": "62 years", + "n05000014__age_63": "63 years", + "n05000014__age_64": "64 years", + "n05000014__age_65": "65 years", + "n05000014__age_66": "66 years", + "n05000014__age_67": "67 years", + "n05000014__age_68": "68 years", + "n05000014__age_69": "69 years", + "n05000014__age_7": "7 years", + "n05000014__age_70": "70 years", + "n05000014__age_71": "71 years", + "n05000014__age_72": "72 years", + "n05000014__age_73": "73 years", + "n05000014__age_74": "74 years", + "n05000014__age_75": "75 years", + "n05000014__age_76": "76 years", + "n05000014__age_77": "77 years", + "n05000014__age_78": "78 years", + "n05000014__age_79": "79 years", + "n05000014__age_8": "8 years", + "n05000014__age_80": "80 years", + "n05000014__age_81": "81 years", + "n05000014__age_82": "82 years", + "n05000014__age_83": "83 years", + "n05000014__age_84": "84 years", + "n05000014__age_85": "85 years", + "n05000014__age_86": "86 years", + "n05000014__age_87": "87 years", + "n05000014__age_88": "88 years", + "n05000014__age_89": "89 years", + "n05000014__age_9": "9 years", + "n05000014__age_90": "90 years", + "n05000014__age_91": "91 years", + "n05000014__age_92": "92 years", + "n05000014__age_93": "93 years", + "n05000014__age_94": "94 years", + "n05000014__age_95": "95 years", + "n05000014__age_96": "96 years", + "n05000014__age_97": "97 years", + "n05000014__age_98": "98 years", + "n05000014__age_99": "99 years", + "n05000015__age_0": "0 years", + "n05000015__age_1": "1 year", + "n05000015__age_10": "10 years", + "n05000015__age_100": "100+ years", + "n05000015__age_11": "11 years", + "n05000015__age_12": "12 years", + "n05000015__age_13": "13 years", + "n05000015__age_14": "14 years", + "n05000015__age_15": "15 years", + "n05000015__age_16": "16 years", + "n05000015__age_17": "17 years", + "n05000015__age_18": "18 years", + "n05000015__age_19": "19 years", + "n05000015__age_2": "2 years", + "n05000015__age_20": "20 years", + "n05000015__age_21": "21 years", + "n05000015__age_22": "22 years", + "n05000015__age_23": "23 years", + "n05000015__age_24": "24 years", + "n05000015__age_25": "25 years", + "n05000015__age_26": "26 years", + "n05000015__age_27": "27 years", + "n05000015__age_28": "28 years", + "n05000015__age_29": "29 years", + "n05000015__age_3": "3 years", + "n05000015__age_30": "30 years", + "n05000015__age_31": "31 years", + "n05000015__age_32": "32 years", + "n05000015__age_33": "33 years", + "n05000015__age_34": "34 years", + "n05000015__age_35": "35 years", + "n05000015__age_36": "36 years", + "n05000015__age_37": "37 years", + "n05000015__age_38": "38 years", + "n05000015__age_39": "39 years", + "n05000015__age_4": "4 years", + "n05000015__age_40": "40 years", + "n05000015__age_41": "41 years", + "n05000015__age_42": "42 years", + "n05000015__age_43": "43 years", + "n05000015__age_44": "44 years", + "n05000015__age_45": "45 years", + "n05000015__age_46": "46 years", + "n05000015__age_47": "47 years", + "n05000015__age_48": "48 years", + "n05000015__age_49": "49 years", + "n05000015__age_5": "5 years", + "n05000015__age_50": "50 years", + "n05000015__age_51": "51 years", + "n05000015__age_52": "52 years", + "n05000015__age_53": "53 years", + "n05000015__age_54": "54 years", + "n05000015__age_55": "55 years", + "n05000015__age_56": "56 years", + "n05000015__age_57": "57 years", + "n05000015__age_58": "58 years", + "n05000015__age_59": "59 years", + "n05000015__age_6": "6 years", + "n05000015__age_60": "60 years", + "n05000015__age_61": "61 years", + "n05000015__age_62": "62 years", + "n05000015__age_63": "63 years", + "n05000015__age_64": "64 years", + "n05000015__age_65": "65 years", + "n05000015__age_66": "66 years", + "n05000015__age_67": "67 years", + "n05000015__age_68": "68 years", + "n05000015__age_69": "69 years", + "n05000015__age_7": "7 years", + "n05000015__age_70": "70 years", + "n05000015__age_71": "71 years", + "n05000015__age_72": "72 years", + "n05000015__age_73": "73 years", + "n05000015__age_74": "74 years", + "n05000015__age_75": "75 years", + "n05000015__age_76": "76 years", + "n05000015__age_77": "77 years", + "n05000015__age_78": "78 years", + "n05000015__age_79": "79 years", + "n05000015__age_8": "8 years", + "n05000015__age_80": "80 years", + "n05000015__age_81": "81 years", + "n05000015__age_82": "82 years", + "n05000015__age_83": "83 years", + "n05000015__age_84": "84 years", + "n05000015__age_85": "85 years", + "n05000015__age_86": "86 years", + "n05000015__age_87": "87 years", + "n05000015__age_88": "88 years", + "n05000015__age_89": "89 years", + "n05000015__age_9": "9 years", + "n05000015__age_90": "90 years", + "n05000015__age_91": "91 years", + "n05000015__age_92": "92 years", + "n05000015__age_93": "93 years", + "n05000015__age_94": "94 years", + "n05000015__age_95": "95 years", + "n05000015__age_96": "96 years", + "n05000015__age_97": "97 years", + "n05000015__age_98": "98 years", + "n05000015__age_99": "99 years", + "n05000016__age_0": "0 years", + "n05000016__age_1": "1 year", + "n05000016__age_10": "10 years", + "n05000016__age_100": "100+ years", + "n05000016__age_11": "11 years", + "n05000016__age_12": "12 years", + "n05000016__age_13": "13 years", + "n05000016__age_14": "14 years", + "n05000016__age_15": "15 years", + "n05000016__age_16": "16 years", + "n05000016__age_17": "17 years", + "n05000016__age_18": "18 years", + "n05000016__age_19": "19 years", + "n05000016__age_2": "2 years", + "n05000016__age_20": "20 years", + "n05000016__age_21": "21 years", + "n05000016__age_22": "22 years", + "n05000016__age_23": "23 years", + "n05000016__age_24": "24 years", + "n05000016__age_25": "25 years", + "n05000016__age_26": "26 years", + "n05000016__age_27": "27 years", + "n05000016__age_28": "28 years", + "n05000016__age_29": "29 years", + "n05000016__age_3": "3 years", + "n05000016__age_30": "30 years", + "n05000016__age_31": "31 years", + "n05000016__age_32": "32 years", + "n05000016__age_33": "33 years", + "n05000016__age_34": "34 years", + "n05000016__age_35": "35 years", + "n05000016__age_36": "36 years", + "n05000016__age_37": "37 years", + "n05000016__age_38": "38 years", + "n05000016__age_39": "39 years", + "n05000016__age_4": "4 years", + "n05000016__age_40": "40 years", + "n05000016__age_41": "41 years", + "n05000016__age_42": "42 years", + "n05000016__age_43": "43 years", + "n05000016__age_44": "44 years", + "n05000016__age_45": "45 years", + "n05000016__age_46": "46 years", + "n05000016__age_47": "47 years", + "n05000016__age_48": "48 years", + "n05000016__age_49": "49 years", + "n05000016__age_5": "5 years", + "n05000016__age_50": "50 years", + "n05000016__age_51": "51 years", + "n05000016__age_52": "52 years", + "n05000016__age_53": "53 years", + "n05000016__age_54": "54 years", + "n05000016__age_55": "55 years", + "n05000016__age_56": "56 years", + "n05000016__age_57": "57 years", + "n05000016__age_58": "58 years", + "n05000016__age_59": "59 years", + "n05000016__age_6": "6 years", + "n05000016__age_60": "60 years", + "n05000016__age_61": "61 years", + "n05000016__age_62": "62 years", + "n05000016__age_63": "63 years", + "n05000016__age_64": "64 years", + "n05000016__age_65": "65 years", + "n05000016__age_66": "66 years", + "n05000016__age_67": "67 years", + "n05000016__age_68": "68 years", + "n05000016__age_69": "69 years", + "n05000016__age_7": "7 years", + "n05000016__age_70": "70 years", + "n05000016__age_71": "71 years", + "n05000016__age_72": "72 years", + "n05000016__age_73": "73 years", + "n05000016__age_74": "74 years", + "n05000016__age_75": "75 years", + "n05000016__age_76": "76 years", + "n05000016__age_77": "77 years", + "n05000016__age_78": "78 years", + "n05000016__age_79": "79 years", + "n05000016__age_8": "8 years", + "n05000016__age_80": "80 years", + "n05000016__age_81": "81 years", + "n05000016__age_82": "82 years", + "n05000016__age_83": "83 years", + "n05000016__age_84": "84 years", + "n05000016__age_85": "85 years", + "n05000016__age_86": "86 years", + "n05000016__age_87": "87 years", + "n05000016__age_88": "88 years", + "n05000016__age_89": "89 years", + "n05000016__age_9": "9 years", + "n05000016__age_90": "90 years", + "n05000016__age_91": "91 years", + "n05000016__age_92": "92 years", + "n05000016__age_93": "93 years", + "n05000016__age_94": "94 years", + "n05000016__age_95": "95 years", + "n05000016__age_96": "96 years", + "n05000016__age_97": "97 years", + "n05000016__age_98": "98 years", + "n05000016__age_99": "99 years", + "n05000017__age_0": "0 years", + "n05000017__age_1": "1 year", + "n05000017__age_10": "10 years", + "n05000017__age_100": "100+ years", + "n05000017__age_11": "11 years", + "n05000017__age_12": "12 years", + "n05000017__age_13": "13 years", + "n05000017__age_14": "14 years", + "n05000017__age_15": "15 years", + "n05000017__age_16": "16 years", + "n05000017__age_17": "17 years", + "n05000017__age_18": "18 years", + "n05000017__age_19": "19 years", + "n05000017__age_2": "2 years", + "n05000017__age_20": "20 years", + "n05000017__age_21": "21 years", + "n05000017__age_22": "22 years", + "n05000017__age_23": "23 years", + "n05000017__age_24": "24 years", + "n05000017__age_25": "25 years", + "n05000017__age_26": "26 years", + "n05000017__age_27": "27 years", + "n05000017__age_28": "28 years", + "n05000017__age_29": "29 years", + "n05000017__age_3": "3 years", + "n05000017__age_30": "30 years", + "n05000017__age_31": "31 years", + "n05000017__age_32": "32 years", + "n05000017__age_33": "33 years", + "n05000017__age_34": "34 years", + "n05000017__age_35": "35 years", + "n05000017__age_36": "36 years", + "n05000017__age_37": "37 years", + "n05000017__age_38": "38 years", + "n05000017__age_39": "39 years", + "n05000017__age_4": "4 years", + "n05000017__age_40": "40 years", + "n05000017__age_41": "41 years", + "n05000017__age_42": "42 years", + "n05000017__age_43": "43 years", + "n05000017__age_44": "44 years", + "n05000017__age_45": "45 years", + "n05000017__age_46": "46 years", + "n05000017__age_47": "47 years", + "n05000017__age_48": "48 years", + "n05000017__age_49": "49 years", + "n05000017__age_5": "5 years", + "n05000017__age_50": "50 years", + "n05000017__age_51": "51 years", + "n05000017__age_52": "52 years", + "n05000017__age_53": "53 years", + "n05000017__age_54": "54 years", + "n05000017__age_55": "55 years", + "n05000017__age_56": "56 years", + "n05000017__age_57": "57 years", + "n05000017__age_58": "58 years", + "n05000017__age_59": "59 years", + "n05000017__age_6": "6 years", + "n05000017__age_60": "60 years", + "n05000017__age_61": "61 years", + "n05000017__age_62": "62 years", + "n05000017__age_63": "63 years", + "n05000017__age_64": "64 years", + "n05000017__age_65": "65 years", + "n05000017__age_66": "66 years", + "n05000017__age_67": "67 years", + "n05000017__age_68": "68 years", + "n05000017__age_69": "69 years", + "n05000017__age_7": "7 years", + "n05000017__age_70": "70 years", + "n05000017__age_71": "71 years", + "n05000017__age_72": "72 years", + "n05000017__age_73": "73 years", + "n05000017__age_74": "74 years", + "n05000017__age_75": "75 years", + "n05000017__age_76": "76 years", + "n05000017__age_77": "77 years", + "n05000017__age_78": "78 years", + "n05000017__age_79": "79 years", + "n05000017__age_8": "8 years", + "n05000017__age_80": "80 years", + "n05000017__age_81": "81 years", + "n05000017__age_82": "82 years", + "n05000017__age_83": "83 years", + "n05000017__age_84": "84 years", + "n05000017__age_85": "85 years", + "n05000017__age_86": "86 years", + "n05000017__age_87": "87 years", + "n05000017__age_88": "88 years", + "n05000017__age_89": "89 years", + "n05000017__age_9": "9 years", + "n05000017__age_90": "90 years", + "n05000017__age_91": "91 years", + "n05000017__age_92": "92 years", + "n05000017__age_93": "93 years", + "n05000017__age_94": "94 years", + "n05000017__age_95": "95 years", + "n05000017__age_96": "96 years", + "n05000017__age_97": "97 years", + "n05000017__age_98": "98 years", + "n05000017__age_99": "99 years", + "n05000018__age_0": "0 years", + "n05000018__age_1": "1 year", + "n05000018__age_10": "10 years", + "n05000018__age_100": "100+ years", + "n05000018__age_11": "11 years", + "n05000018__age_12": "12 years", + "n05000018__age_13": "13 years", + "n05000018__age_14": "14 years", + "n05000018__age_15": "15 years", + "n05000018__age_16": "16 years", + "n05000018__age_17": "17 years", + "n05000018__age_18": "18 years", + "n05000018__age_19": "19 years", + "n05000018__age_2": "2 years", + "n05000018__age_20": "20 years", + "n05000018__age_21": "21 years", + "n05000018__age_22": "22 years", + "n05000018__age_23": "23 years", + "n05000018__age_24": "24 years", + "n05000018__age_25": "25 years", + "n05000018__age_26": "26 years", + "n05000018__age_27": "27 years", + "n05000018__age_28": "28 years", + "n05000018__age_29": "29 years", + "n05000018__age_3": "3 years", + "n05000018__age_30": "30 years", + "n05000018__age_31": "31 years", + "n05000018__age_32": "32 years", + "n05000018__age_33": "33 years", + "n05000018__age_34": "34 years", + "n05000018__age_35": "35 years", + "n05000018__age_36": "36 years", + "n05000018__age_37": "37 years", + "n05000018__age_38": "38 years", + "n05000018__age_39": "39 years", + "n05000018__age_4": "4 years", + "n05000018__age_40": "40 years", + "n05000018__age_41": "41 years", + "n05000018__age_42": "42 years", + "n05000018__age_43": "43 years", + "n05000018__age_44": "44 years", + "n05000018__age_45": "45 years", + "n05000018__age_46": "46 years", + "n05000018__age_47": "47 years", + "n05000018__age_48": "48 years", + "n05000018__age_49": "49 years", + "n05000018__age_5": "5 years", + "n05000018__age_50": "50 years", + "n05000018__age_51": "51 years", + "n05000018__age_52": "52 years", + "n05000018__age_53": "53 years", + "n05000018__age_54": "54 years", + "n05000018__age_55": "55 years", + "n05000018__age_56": "56 years", + "n05000018__age_57": "57 years", + "n05000018__age_58": "58 years", + "n05000018__age_59": "59 years", + "n05000018__age_6": "6 years", + "n05000018__age_60": "60 years", + "n05000018__age_61": "61 years", + "n05000018__age_62": "62 years", + "n05000018__age_63": "63 years", + "n05000018__age_64": "64 years", + "n05000018__age_65": "65 years", + "n05000018__age_66": "66 years", + "n05000018__age_67": "67 years", + "n05000018__age_68": "68 years", + "n05000018__age_69": "69 years", + "n05000018__age_7": "7 years", + "n05000018__age_70": "70 years", + "n05000018__age_71": "71 years", + "n05000018__age_72": "72 years", + "n05000018__age_73": "73 years", + "n05000018__age_74": "74 years", + "n05000018__age_75": "75 years", + "n05000018__age_76": "76 years", + "n05000018__age_77": "77 years", + "n05000018__age_78": "78 years", + "n05000018__age_79": "79 years", + "n05000018__age_8": "8 years", + "n05000018__age_80": "80 years", + "n05000018__age_81": "81 years", + "n05000018__age_82": "82 years", + "n05000018__age_83": "83 years", + "n05000018__age_84": "84 years", + "n05000018__age_85": "85 years", + "n05000018__age_86": "86 years", + "n05000018__age_87": "87 years", + "n05000018__age_88": "88 years", + "n05000018__age_89": "89 years", + "n05000018__age_9": "9 years", + "n05000018__age_90": "90 years", + "n05000018__age_91": "91 years", + "n05000018__age_92": "92 years", + "n05000018__age_93": "93 years", + "n05000018__age_94": "94 years", + "n05000018__age_95": "95 years", + "n05000018__age_96": "96 years", + "n05000018__age_97": "97 years", + "n05000018__age_98": "98 years", + "n05000018__age_99": "99 years", + "n09000001__age_0": "0 years", + "n09000001__age_1": "1 year", + "n09000001__age_10": "10 years", + "n09000001__age_100": "100+ years", + "n09000001__age_11": "11 years", + "n09000001__age_12": "12 years", + "n09000001__age_13": "13 years", + "n09000001__age_14": "14 years", + "n09000001__age_15": "15 years", + "n09000001__age_16": "16 years", + "n09000001__age_17": "17 years", + "n09000001__age_18": "18 years", + "n09000001__age_19": "19 years", + "n09000001__age_2": "2 years", + "n09000001__age_20": "20 years", + "n09000001__age_21": "21 years", + "n09000001__age_22": "22 years", + "n09000001__age_23": "23 years", + "n09000001__age_24": "24 years", + "n09000001__age_25": "25 years", + "n09000001__age_26": "26 years", + "n09000001__age_27": "27 years", + "n09000001__age_28": "28 years", + "n09000001__age_29": "29 years", + "n09000001__age_3": "3 years", + "n09000001__age_30": "30 years", + "n09000001__age_31": "31 years", + "n09000001__age_32": "32 years", + "n09000001__age_33": "33 years", + "n09000001__age_34": "34 years", + "n09000001__age_35": "35 years", + "n09000001__age_36": "36 years", + "n09000001__age_37": "37 years", + "n09000001__age_38": "38 years", + "n09000001__age_39": "39 years", + "n09000001__age_4": "4 years", + "n09000001__age_40": "40 years", + "n09000001__age_41": "41 years", + "n09000001__age_42": "42 years", + "n09000001__age_43": "43 years", + "n09000001__age_44": "44 years", + "n09000001__age_45": "45 years", + "n09000001__age_46": "46 years", + "n09000001__age_47": "47 years", + "n09000001__age_48": "48 years", + "n09000001__age_49": "49 years", + "n09000001__age_5": "5 years", + "n09000001__age_50": "50 years", + "n09000001__age_51": "51 years", + "n09000001__age_52": "52 years", + "n09000001__age_53": "53 years", + "n09000001__age_54": "54 years", + "n09000001__age_55": "55 years", + "n09000001__age_56": "56 years", + "n09000001__age_57": "57 years", + "n09000001__age_58": "58 years", + "n09000001__age_59": "59 years", + "n09000001__age_6": "6 years", + "n09000001__age_60": "60 years", + "n09000001__age_61": "61 years", + "n09000001__age_62": "62 years", + "n09000001__age_63": "63 years", + "n09000001__age_64": "64 years", + "n09000001__age_65": "65 years", + "n09000001__age_66": "66 years", + "n09000001__age_67": "67 years", + "n09000001__age_68": "68 years", + "n09000001__age_69": "69 years", + "n09000001__age_7": "7 years", + "n09000001__age_70": "70 years", + "n09000001__age_71": "71 years", + "n09000001__age_72": "72 years", + "n09000001__age_73": "73 years", + "n09000001__age_74": "74 years", + "n09000001__age_75": "75 years", + "n09000001__age_76": "76 years", + "n09000001__age_77": "77 years", + "n09000001__age_78": "78 years", + "n09000001__age_79": "79 years", + "n09000001__age_8": "8 years", + "n09000001__age_80": "80 years", + "n09000001__age_81": "81 years", + "n09000001__age_82": "82 years", + "n09000001__age_83": "83 years", + "n09000001__age_84": "84 years", + "n09000001__age_85": "85 years", + "n09000001__age_86": "86 years", + "n09000001__age_87": "87 years", + "n09000001__age_88": "88 years", + "n09000001__age_89": "89 years", + "n09000001__age_9": "9 years", + "n09000001__age_90": "90 years", + "n09000001__age_91": "91 years", + "n09000001__age_92": "92 years", + "n09000001__age_93": "93 years", + "n09000001__age_94": "94 years", + "n09000001__age_95": "95 years", + "n09000001__age_96": "96 years", + "n09000001__age_97": "97 years", + "n09000001__age_98": "98 years", + "n09000001__age_99": "99 years", + "n09000002__age_0": "0 years", + "n09000002__age_1": "1 year", + "n09000002__age_10": "10 years", + "n09000002__age_100": "100+ years", + "n09000002__age_11": "11 years", + "n09000002__age_12": "12 years", + "n09000002__age_13": "13 years", + "n09000002__age_14": "14 years", + "n09000002__age_15": "15 years", + "n09000002__age_16": "16 years", + "n09000002__age_17": "17 years", + "n09000002__age_18": "18 years", + "n09000002__age_19": "19 years", + "n09000002__age_2": "2 years", + "n09000002__age_20": "20 years", + "n09000002__age_21": "21 years", + "n09000002__age_22": "22 years", + "n09000002__age_23": "23 years", + "n09000002__age_24": "24 years", + "n09000002__age_25": "25 years", + "n09000002__age_26": "26 years", + "n09000002__age_27": "27 years", + "n09000002__age_28": "28 years", + "n09000002__age_29": "29 years", + "n09000002__age_3": "3 years", + "n09000002__age_30": "30 years", + "n09000002__age_31": "31 years", + "n09000002__age_32": "32 years", + "n09000002__age_33": "33 years", + "n09000002__age_34": "34 years", + "n09000002__age_35": "35 years", + "n09000002__age_36": "36 years", + "n09000002__age_37": "37 years", + "n09000002__age_38": "38 years", + "n09000002__age_39": "39 years", + "n09000002__age_4": "4 years", + "n09000002__age_40": "40 years", + "n09000002__age_41": "41 years", + "n09000002__age_42": "42 years", + "n09000002__age_43": "43 years", + "n09000002__age_44": "44 years", + "n09000002__age_45": "45 years", + "n09000002__age_46": "46 years", + "n09000002__age_47": "47 years", + "n09000002__age_48": "48 years", + "n09000002__age_49": "49 years", + "n09000002__age_5": "5 years", + "n09000002__age_50": "50 years", + "n09000002__age_51": "51 years", + "n09000002__age_52": "52 years", + "n09000002__age_53": "53 years", + "n09000002__age_54": "54 years", + "n09000002__age_55": "55 years", + "n09000002__age_56": "56 years", + "n09000002__age_57": "57 years", + "n09000002__age_58": "58 years", + "n09000002__age_59": "59 years", + "n09000002__age_6": "6 years", + "n09000002__age_60": "60 years", + "n09000002__age_61": "61 years", + "n09000002__age_62": "62 years", + "n09000002__age_63": "63 years", + "n09000002__age_64": "64 years", + "n09000002__age_65": "65 years", + "n09000002__age_66": "66 years", + "n09000002__age_67": "67 years", + "n09000002__age_68": "68 years", + "n09000002__age_69": "69 years", + "n09000002__age_7": "7 years", + "n09000002__age_70": "70 years", + "n09000002__age_71": "71 years", + "n09000002__age_72": "72 years", + "n09000002__age_73": "73 years", + "n09000002__age_74": "74 years", + "n09000002__age_75": "75 years", + "n09000002__age_76": "76 years", + "n09000002__age_77": "77 years", + "n09000002__age_78": "78 years", + "n09000002__age_79": "79 years", + "n09000002__age_8": "8 years", + "n09000002__age_80": "80 years", + "n09000002__age_81": "81 years", + "n09000002__age_82": "82 years", + "n09000002__age_83": "83 years", + "n09000002__age_84": "84 years", + "n09000002__age_85": "85 years", + "n09000002__age_86": "86 years", + "n09000002__age_87": "87 years", + "n09000002__age_88": "88 years", + "n09000002__age_89": "89 years", + "n09000002__age_9": "9 years", + "n09000002__age_90": "90 years", + "n09000002__age_91": "91 years", + "n09000002__age_92": "92 years", + "n09000002__age_93": "93 years", + "n09000002__age_94": "94 years", + "n09000002__age_95": "95 years", + "n09000002__age_96": "96 years", + "n09000002__age_97": "97 years", + "n09000002__age_98": "98 years", + "n09000002__age_99": "99 years", + "n09000003__age_0": "0 years", + "n09000003__age_1": "1 year", + "n09000003__age_10": "10 years", + "n09000003__age_100": "100+ years", + "n09000003__age_11": "11 years", + "n09000003__age_12": "12 years", + "n09000003__age_13": "13 years", + "n09000003__age_14": "14 years", + "n09000003__age_15": "15 years", + "n09000003__age_16": "16 years", + "n09000003__age_17": "17 years", + "n09000003__age_18": "18 years", + "n09000003__age_19": "19 years", + "n09000003__age_2": "2 years", + "n09000003__age_20": "20 years", + "n09000003__age_21": "21 years", + "n09000003__age_22": "22 years", + "n09000003__age_23": "23 years", + "n09000003__age_24": "24 years", + "n09000003__age_25": "25 years", + "n09000003__age_26": "26 years", + "n09000003__age_27": "27 years", + "n09000003__age_28": "28 years", + "n09000003__age_29": "29 years", + "n09000003__age_3": "3 years", + "n09000003__age_30": "30 years", + "n09000003__age_31": "31 years", + "n09000003__age_32": "32 years", + "n09000003__age_33": "33 years", + "n09000003__age_34": "34 years", + "n09000003__age_35": "35 years", + "n09000003__age_36": "36 years", + "n09000003__age_37": "37 years", + "n09000003__age_38": "38 years", + "n09000003__age_39": "39 years", + "n09000003__age_4": "4 years", + "n09000003__age_40": "40 years", + "n09000003__age_41": "41 years", + "n09000003__age_42": "42 years", + "n09000003__age_43": "43 years", + "n09000003__age_44": "44 years", + "n09000003__age_45": "45 years", + "n09000003__age_46": "46 years", + "n09000003__age_47": "47 years", + "n09000003__age_48": "48 years", + "n09000003__age_49": "49 years", + "n09000003__age_5": "5 years", + "n09000003__age_50": "50 years", + "n09000003__age_51": "51 years", + "n09000003__age_52": "52 years", + "n09000003__age_53": "53 years", + "n09000003__age_54": "54 years", + "n09000003__age_55": "55 years", + "n09000003__age_56": "56 years", + "n09000003__age_57": "57 years", + "n09000003__age_58": "58 years", + "n09000003__age_59": "59 years", + "n09000003__age_6": "6 years", + "n09000003__age_60": "60 years", + "n09000003__age_61": "61 years", + "n09000003__age_62": "62 years", + "n09000003__age_63": "63 years", + "n09000003__age_64": "64 years", + "n09000003__age_65": "65 years", + "n09000003__age_66": "66 years", + "n09000003__age_67": "67 years", + "n09000003__age_68": "68 years", + "n09000003__age_69": "69 years", + "n09000003__age_7": "7 years", + "n09000003__age_70": "70 years", + "n09000003__age_71": "71 years", + "n09000003__age_72": "72 years", + "n09000003__age_73": "73 years", + "n09000003__age_74": "74 years", + "n09000003__age_75": "75 years", + "n09000003__age_76": "76 years", + "n09000003__age_77": "77 years", + "n09000003__age_78": "78 years", + "n09000003__age_79": "79 years", + "n09000003__age_8": "8 years", + "n09000003__age_80": "80 years", + "n09000003__age_81": "81 years", + "n09000003__age_82": "82 years", + "n09000003__age_83": "83 years", + "n09000003__age_84": "84 years", + "n09000003__age_85": "85 years", + "n09000003__age_86": "86 years", + "n09000003__age_87": "87 years", + "n09000003__age_88": "88 years", + "n09000003__age_89": "89 years", + "n09000003__age_9": "9 years", + "n09000003__age_90": "90 years", + "n09000003__age_91": "91 years", + "n09000003__age_92": "92 years", + "n09000003__age_93": "93 years", + "n09000003__age_94": "94 years", + "n09000003__age_95": "95 years", + "n09000003__age_96": "96 years", + "n09000003__age_97": "97 years", + "n09000003__age_98": "98 years", + "n09000003__age_99": "99 years", + "n09000004__age_0": "0 years", + "n09000004__age_1": "1 year", + "n09000004__age_10": "10 years", + "n09000004__age_100": "100+ years", + "n09000004__age_11": "11 years", + "n09000004__age_12": "12 years", + "n09000004__age_13": "13 years", + "n09000004__age_14": "14 years", + "n09000004__age_15": "15 years", + "n09000004__age_16": "16 years", + "n09000004__age_17": "17 years", + "n09000004__age_18": "18 years", + "n09000004__age_19": "19 years", + "n09000004__age_2": "2 years", + "n09000004__age_20": "20 years", + "n09000004__age_21": "21 years", + "n09000004__age_22": "22 years", + "n09000004__age_23": "23 years", + "n09000004__age_24": "24 years", + "n09000004__age_25": "25 years", + "n09000004__age_26": "26 years", + "n09000004__age_27": "27 years", + "n09000004__age_28": "28 years", + "n09000004__age_29": "29 years", + "n09000004__age_3": "3 years", + "n09000004__age_30": "30 years", + "n09000004__age_31": "31 years", + "n09000004__age_32": "32 years", + "n09000004__age_33": "33 years", + "n09000004__age_34": "34 years", + "n09000004__age_35": "35 years", + "n09000004__age_36": "36 years", + "n09000004__age_37": "37 years", + "n09000004__age_38": "38 years", + "n09000004__age_39": "39 years", + "n09000004__age_4": "4 years", + "n09000004__age_40": "40 years", + "n09000004__age_41": "41 years", + "n09000004__age_42": "42 years", + "n09000004__age_43": "43 years", + "n09000004__age_44": "44 years", + "n09000004__age_45": "45 years", + "n09000004__age_46": "46 years", + "n09000004__age_47": "47 years", + "n09000004__age_48": "48 years", + "n09000004__age_49": "49 years", + "n09000004__age_5": "5 years", + "n09000004__age_50": "50 years", + "n09000004__age_51": "51 years", + "n09000004__age_52": "52 years", + "n09000004__age_53": "53 years", + "n09000004__age_54": "54 years", + "n09000004__age_55": "55 years", + "n09000004__age_56": "56 years", + "n09000004__age_57": "57 years", + "n09000004__age_58": "58 years", + "n09000004__age_59": "59 years", + "n09000004__age_6": "6 years", + "n09000004__age_60": "60 years", + "n09000004__age_61": "61 years", + "n09000004__age_62": "62 years", + "n09000004__age_63": "63 years", + "n09000004__age_64": "64 years", + "n09000004__age_65": "65 years", + "n09000004__age_66": "66 years", + "n09000004__age_67": "67 years", + "n09000004__age_68": "68 years", + "n09000004__age_69": "69 years", + "n09000004__age_7": "7 years", + "n09000004__age_70": "70 years", + "n09000004__age_71": "71 years", + "n09000004__age_72": "72 years", + "n09000004__age_73": "73 years", + "n09000004__age_74": "74 years", + "n09000004__age_75": "75 years", + "n09000004__age_76": "76 years", + "n09000004__age_77": "77 years", + "n09000004__age_78": "78 years", + "n09000004__age_79": "79 years", + "n09000004__age_8": "8 years", + "n09000004__age_80": "80 years", + "n09000004__age_81": "81 years", + "n09000004__age_82": "82 years", + "n09000004__age_83": "83 years", + "n09000004__age_84": "84 years", + "n09000004__age_85": "85 years", + "n09000004__age_86": "86 years", + "n09000004__age_87": "87 years", + "n09000004__age_88": "88 years", + "n09000004__age_89": "89 years", + "n09000004__age_9": "9 years", + "n09000004__age_90": "90 years", + "n09000004__age_91": "91 years", + "n09000004__age_92": "92 years", + "n09000004__age_93": "93 years", + "n09000004__age_94": "94 years", + "n09000004__age_95": "95 years", + "n09000004__age_96": "96 years", + "n09000004__age_97": "97 years", + "n09000004__age_98": "98 years", + "n09000004__age_99": "99 years", + "n09000005__age_0": "0 years", + "n09000005__age_1": "1 year", + "n09000005__age_10": "10 years", + "n09000005__age_100": "100+ years", + "n09000005__age_11": "11 years", + "n09000005__age_12": "12 years", + "n09000005__age_13": "13 years", + "n09000005__age_14": "14 years", + "n09000005__age_15": "15 years", + "n09000005__age_16": "16 years", + "n09000005__age_17": "17 years", + "n09000005__age_18": "18 years", + "n09000005__age_19": "19 years", + "n09000005__age_2": "2 years", + "n09000005__age_20": "20 years", + "n09000005__age_21": "21 years", + "n09000005__age_22": "22 years", + "n09000005__age_23": "23 years", + "n09000005__age_24": "24 years", + "n09000005__age_25": "25 years", + "n09000005__age_26": "26 years", + "n09000005__age_27": "27 years", + "n09000005__age_28": "28 years", + "n09000005__age_29": "29 years", + "n09000005__age_3": "3 years", + "n09000005__age_30": "30 years", + "n09000005__age_31": "31 years", + "n09000005__age_32": "32 years", + "n09000005__age_33": "33 years", + "n09000005__age_34": "34 years", + "n09000005__age_35": "35 years", + "n09000005__age_36": "36 years", + "n09000005__age_37": "37 years", + "n09000005__age_38": "38 years", + "n09000005__age_39": "39 years", + "n09000005__age_4": "4 years", + "n09000005__age_40": "40 years", + "n09000005__age_41": "41 years", + "n09000005__age_42": "42 years", + "n09000005__age_43": "43 years", + "n09000005__age_44": "44 years", + "n09000005__age_45": "45 years", + "n09000005__age_46": "46 years", + "n09000005__age_47": "47 years", + "n09000005__age_48": "48 years", + "n09000005__age_49": "49 years", + "n09000005__age_5": "5 years", + "n09000005__age_50": "50 years", + "n09000005__age_51": "51 years", + "n09000005__age_52": "52 years", + "n09000005__age_53": "53 years", + "n09000005__age_54": "54 years", + "n09000005__age_55": "55 years", + "n09000005__age_56": "56 years", + "n09000005__age_57": "57 years", + "n09000005__age_58": "58 years", + "n09000005__age_59": "59 years", + "n09000005__age_6": "6 years", + "n09000005__age_60": "60 years", + "n09000005__age_61": "61 years", + "n09000005__age_62": "62 years", + "n09000005__age_63": "63 years", + "n09000005__age_64": "64 years", + "n09000005__age_65": "65 years", + "n09000005__age_66": "66 years", + "n09000005__age_67": "67 years", + "n09000005__age_68": "68 years", + "n09000005__age_69": "69 years", + "n09000005__age_7": "7 years", + "n09000005__age_70": "70 years", + "n09000005__age_71": "71 years", + "n09000005__age_72": "72 years", + "n09000005__age_73": "73 years", + "n09000005__age_74": "74 years", + "n09000005__age_75": "75 years", + "n09000005__age_76": "76 years", + "n09000005__age_77": "77 years", + "n09000005__age_78": "78 years", + "n09000005__age_79": "79 years", + "n09000005__age_8": "8 years", + "n09000005__age_80": "80 years", + "n09000005__age_81": "81 years", + "n09000005__age_82": "82 years", + "n09000005__age_83": "83 years", + "n09000005__age_84": "84 years", + "n09000005__age_85": "85 years", + "n09000005__age_86": "86 years", + "n09000005__age_87": "87 years", + "n09000005__age_88": "88 years", + "n09000005__age_89": "89 years", + "n09000005__age_9": "9 years", + "n09000005__age_90": "90 years", + "n09000005__age_91": "91 years", + "n09000005__age_92": "92 years", + "n09000005__age_93": "93 years", + "n09000005__age_94": "94 years", + "n09000005__age_95": "95 years", + "n09000005__age_96": "96 years", + "n09000005__age_97": "97 years", + "n09000005__age_98": "98 years", + "n09000005__age_99": "99 years", + "n09000006__age_0": "0 years", + "n09000006__age_1": "1 year", + "n09000006__age_10": "10 years", + "n09000006__age_100": "100+ years", + "n09000006__age_11": "11 years", + "n09000006__age_12": "12 years", + "n09000006__age_13": "13 years", + "n09000006__age_14": "14 years", + "n09000006__age_15": "15 years", + "n09000006__age_16": "16 years", + "n09000006__age_17": "17 years", + "n09000006__age_18": "18 years", + "n09000006__age_19": "19 years", + "n09000006__age_2": "2 years", + "n09000006__age_20": "20 years", + "n09000006__age_21": "21 years", + "n09000006__age_22": "22 years", + "n09000006__age_23": "23 years", + "n09000006__age_24": "24 years", + "n09000006__age_25": "25 years", + "n09000006__age_26": "26 years", + "n09000006__age_27": "27 years", + "n09000006__age_28": "28 years", + "n09000006__age_29": "29 years", + "n09000006__age_3": "3 years", + "n09000006__age_30": "30 years", + "n09000006__age_31": "31 years", + "n09000006__age_32": "32 years", + "n09000006__age_33": "33 years", + "n09000006__age_34": "34 years", + "n09000006__age_35": "35 years", + "n09000006__age_36": "36 years", + "n09000006__age_37": "37 years", + "n09000006__age_38": "38 years", + "n09000006__age_39": "39 years", + "n09000006__age_4": "4 years", + "n09000006__age_40": "40 years", + "n09000006__age_41": "41 years", + "n09000006__age_42": "42 years", + "n09000006__age_43": "43 years", + "n09000006__age_44": "44 years", + "n09000006__age_45": "45 years", + "n09000006__age_46": "46 years", + "n09000006__age_47": "47 years", + "n09000006__age_48": "48 years", + "n09000006__age_49": "49 years", + "n09000006__age_5": "5 years", + "n09000006__age_50": "50 years", + "n09000006__age_51": "51 years", + "n09000006__age_52": "52 years", + "n09000006__age_53": "53 years", + "n09000006__age_54": "54 years", + "n09000006__age_55": "55 years", + "n09000006__age_56": "56 years", + "n09000006__age_57": "57 years", + "n09000006__age_58": "58 years", + "n09000006__age_59": "59 years", + "n09000006__age_6": "6 years", + "n09000006__age_60": "60 years", + "n09000006__age_61": "61 years", + "n09000006__age_62": "62 years", + "n09000006__age_63": "63 years", + "n09000006__age_64": "64 years", + "n09000006__age_65": "65 years", + "n09000006__age_66": "66 years", + "n09000006__age_67": "67 years", + "n09000006__age_68": "68 years", + "n09000006__age_69": "69 years", + "n09000006__age_7": "7 years", + "n09000006__age_70": "70 years", + "n09000006__age_71": "71 years", + "n09000006__age_72": "72 years", + "n09000006__age_73": "73 years", + "n09000006__age_74": "74 years", + "n09000006__age_75": "75 years", + "n09000006__age_76": "76 years", + "n09000006__age_77": "77 years", + "n09000006__age_78": "78 years", + "n09000006__age_79": "79 years", + "n09000006__age_8": "8 years", + "n09000006__age_80": "80 years", + "n09000006__age_81": "81 years", + "n09000006__age_82": "82 years", + "n09000006__age_83": "83 years", + "n09000006__age_84": "84 years", + "n09000006__age_85": "85 years", + "n09000006__age_86": "86 years", + "n09000006__age_87": "87 years", + "n09000006__age_88": "88 years", + "n09000006__age_89": "89 years", + "n09000006__age_9": "9 years", + "n09000006__age_90": "90 years", + "n09000006__age_91": "91 years", + "n09000006__age_92": "92 years", + "n09000006__age_93": "93 years", + "n09000006__age_94": "94 years", + "n09000006__age_95": "95 years", + "n09000006__age_96": "96 years", + "n09000006__age_97": "97 years", + "n09000006__age_98": "98 years", + "n09000006__age_99": "99 years", + "n09000007__age_0": "0 years", + "n09000007__age_1": "1 year", + "n09000007__age_10": "10 years", + "n09000007__age_100": "100+ years", + "n09000007__age_11": "11 years", + "n09000007__age_12": "12 years", + "n09000007__age_13": "13 years", + "n09000007__age_14": "14 years", + "n09000007__age_15": "15 years", + "n09000007__age_16": "16 years", + "n09000007__age_17": "17 years", + "n09000007__age_18": "18 years", + "n09000007__age_19": "19 years", + "n09000007__age_2": "2 years", + "n09000007__age_20": "20 years", + "n09000007__age_21": "21 years", + "n09000007__age_22": "22 years", + "n09000007__age_23": "23 years", + "n09000007__age_24": "24 years", + "n09000007__age_25": "25 years", + "n09000007__age_26": "26 years", + "n09000007__age_27": "27 years", + "n09000007__age_28": "28 years", + "n09000007__age_29": "29 years", + "n09000007__age_3": "3 years", + "n09000007__age_30": "30 years", + "n09000007__age_31": "31 years", + "n09000007__age_32": "32 years", + "n09000007__age_33": "33 years", + "n09000007__age_34": "34 years", + "n09000007__age_35": "35 years", + "n09000007__age_36": "36 years", + "n09000007__age_37": "37 years", + "n09000007__age_38": "38 years", + "n09000007__age_39": "39 years", + "n09000007__age_4": "4 years", + "n09000007__age_40": "40 years", + "n09000007__age_41": "41 years", + "n09000007__age_42": "42 years", + "n09000007__age_43": "43 years", + "n09000007__age_44": "44 years", + "n09000007__age_45": "45 years", + "n09000007__age_46": "46 years", + "n09000007__age_47": "47 years", + "n09000007__age_48": "48 years", + "n09000007__age_49": "49 years", + "n09000007__age_5": "5 years", + "n09000007__age_50": "50 years", + "n09000007__age_51": "51 years", + "n09000007__age_52": "52 years", + "n09000007__age_53": "53 years", + "n09000007__age_54": "54 years", + "n09000007__age_55": "55 years", + "n09000007__age_56": "56 years", + "n09000007__age_57": "57 years", + "n09000007__age_58": "58 years", + "n09000007__age_59": "59 years", + "n09000007__age_6": "6 years", + "n09000007__age_60": "60 years", + "n09000007__age_61": "61 years", + "n09000007__age_62": "62 years", + "n09000007__age_63": "63 years", + "n09000007__age_64": "64 years", + "n09000007__age_65": "65 years", + "n09000007__age_66": "66 years", + "n09000007__age_67": "67 years", + "n09000007__age_68": "68 years", + "n09000007__age_69": "69 years", + "n09000007__age_7": "7 years", + "n09000007__age_70": "70 years", + "n09000007__age_71": "71 years", + "n09000007__age_72": "72 years", + "n09000007__age_73": "73 years", + "n09000007__age_74": "74 years", + "n09000007__age_75": "75 years", + "n09000007__age_76": "76 years", + "n09000007__age_77": "77 years", + "n09000007__age_78": "78 years", + "n09000007__age_79": "79 years", + "n09000007__age_8": "8 years", + "n09000007__age_80": "80 years", + "n09000007__age_81": "81 years", + "n09000007__age_82": "82 years", + "n09000007__age_83": "83 years", + "n09000007__age_84": "84 years", + "n09000007__age_85": "85 years", + "n09000007__age_86": "86 years", + "n09000007__age_87": "87 years", + "n09000007__age_88": "88 years", + "n09000007__age_89": "89 years", + "n09000007__age_9": "9 years", + "n09000007__age_90": "90 years", + "n09000007__age_91": "91 years", + "n09000007__age_92": "92 years", + "n09000007__age_93": "93 years", + "n09000007__age_94": "94 years", + "n09000007__age_95": "95 years", + "n09000007__age_96": "96 years", + "n09000007__age_97": "97 years", + "n09000007__age_98": "98 years", + "n09000007__age_99": "99 years", + "n09000008__age_0": "0 years", + "n09000008__age_1": "1 year", + "n09000008__age_10": "10 years", + "n09000008__age_100": "100+ years", + "n09000008__age_11": "11 years", + "n09000008__age_12": "12 years", + "n09000008__age_13": "13 years", + "n09000008__age_14": "14 years", + "n09000008__age_15": "15 years", + "n09000008__age_16": "16 years", + "n09000008__age_17": "17 years", + "n09000008__age_18": "18 years", + "n09000008__age_19": "19 years", + "n09000008__age_2": "2 years", + "n09000008__age_20": "20 years", + "n09000008__age_21": "21 years", + "n09000008__age_22": "22 years", + "n09000008__age_23": "23 years", + "n09000008__age_24": "24 years", + "n09000008__age_25": "25 years", + "n09000008__age_26": "26 years", + "n09000008__age_27": "27 years", + "n09000008__age_28": "28 years", + "n09000008__age_29": "29 years", + "n09000008__age_3": "3 years", + "n09000008__age_30": "30 years", + "n09000008__age_31": "31 years", + "n09000008__age_32": "32 years", + "n09000008__age_33": "33 years", + "n09000008__age_34": "34 years", + "n09000008__age_35": "35 years", + "n09000008__age_36": "36 years", + "n09000008__age_37": "37 years", + "n09000008__age_38": "38 years", + "n09000008__age_39": "39 years", + "n09000008__age_4": "4 years", + "n09000008__age_40": "40 years", + "n09000008__age_41": "41 years", + "n09000008__age_42": "42 years", + "n09000008__age_43": "43 years", + "n09000008__age_44": "44 years", + "n09000008__age_45": "45 years", + "n09000008__age_46": "46 years", + "n09000008__age_47": "47 years", + "n09000008__age_48": "48 years", + "n09000008__age_49": "49 years", + "n09000008__age_5": "5 years", + "n09000008__age_50": "50 years", + "n09000008__age_51": "51 years", + "n09000008__age_52": "52 years", + "n09000008__age_53": "53 years", + "n09000008__age_54": "54 years", + "n09000008__age_55": "55 years", + "n09000008__age_56": "56 years", + "n09000008__age_57": "57 years", + "n09000008__age_58": "58 years", + "n09000008__age_59": "59 years", + "n09000008__age_6": "6 years", + "n09000008__age_60": "60 years", + "n09000008__age_61": "61 years", + "n09000008__age_62": "62 years", + "n09000008__age_63": "63 years", + "n09000008__age_64": "64 years", + "n09000008__age_65": "65 years", + "n09000008__age_66": "66 years", + "n09000008__age_67": "67 years", + "n09000008__age_68": "68 years", + "n09000008__age_69": "69 years", + "n09000008__age_7": "7 years", + "n09000008__age_70": "70 years", + "n09000008__age_71": "71 years", + "n09000008__age_72": "72 years", + "n09000008__age_73": "73 years", + "n09000008__age_74": "74 years", + "n09000008__age_75": "75 years", + "n09000008__age_76": "76 years", + "n09000008__age_77": "77 years", + "n09000008__age_78": "78 years", + "n09000008__age_79": "79 years", + "n09000008__age_8": "8 years", + "n09000008__age_80": "80 years", + "n09000008__age_81": "81 years", + "n09000008__age_82": "82 years", + "n09000008__age_83": "83 years", + "n09000008__age_84": "84 years", + "n09000008__age_85": "85 years", + "n09000008__age_86": "86 years", + "n09000008__age_87": "87 years", + "n09000008__age_88": "88 years", + "n09000008__age_89": "89 years", + "n09000008__age_9": "9 years", + "n09000008__age_90": "90 years", + "n09000008__age_91": "91 years", + "n09000008__age_92": "92 years", + "n09000008__age_93": "93 years", + "n09000008__age_94": "94 years", + "n09000008__age_95": "95 years", + "n09000008__age_96": "96 years", + "n09000008__age_97": "97 years", + "n09000008__age_98": "98 years", + "n09000008__age_99": "99 years", + "n09000009__age_0": "0 years", + "n09000009__age_1": "1 year", + "n09000009__age_10": "10 years", + "n09000009__age_100": "100+ years", + "n09000009__age_11": "11 years", + "n09000009__age_12": "12 years", + "n09000009__age_13": "13 years", + "n09000009__age_14": "14 years", + "n09000009__age_15": "15 years", + "n09000009__age_16": "16 years", + "n09000009__age_17": "17 years", + "n09000009__age_18": "18 years", + "n09000009__age_19": "19 years", + "n09000009__age_2": "2 years", + "n09000009__age_20": "20 years", + "n09000009__age_21": "21 years", + "n09000009__age_22": "22 years", + "n09000009__age_23": "23 years", + "n09000009__age_24": "24 years", + "n09000009__age_25": "25 years", + "n09000009__age_26": "26 years", + "n09000009__age_27": "27 years", + "n09000009__age_28": "28 years", + "n09000009__age_29": "29 years", + "n09000009__age_3": "3 years", + "n09000009__age_30": "30 years", + "n09000009__age_31": "31 years", + "n09000009__age_32": "32 years", + "n09000009__age_33": "33 years", + "n09000009__age_34": "34 years", + "n09000009__age_35": "35 years", + "n09000009__age_36": "36 years", + "n09000009__age_37": "37 years", + "n09000009__age_38": "38 years", + "n09000009__age_39": "39 years", + "n09000009__age_4": "4 years", + "n09000009__age_40": "40 years", + "n09000009__age_41": "41 years", + "n09000009__age_42": "42 years", + "n09000009__age_43": "43 years", + "n09000009__age_44": "44 years", + "n09000009__age_45": "45 years", + "n09000009__age_46": "46 years", + "n09000009__age_47": "47 years", + "n09000009__age_48": "48 years", + "n09000009__age_49": "49 years", + "n09000009__age_5": "5 years", + "n09000009__age_50": "50 years", + "n09000009__age_51": "51 years", + "n09000009__age_52": "52 years", + "n09000009__age_53": "53 years", + "n09000009__age_54": "54 years", + "n09000009__age_55": "55 years", + "n09000009__age_56": "56 years", + "n09000009__age_57": "57 years", + "n09000009__age_58": "58 years", + "n09000009__age_59": "59 years", + "n09000009__age_6": "6 years", + "n09000009__age_60": "60 years", + "n09000009__age_61": "61 years", + "n09000009__age_62": "62 years", + "n09000009__age_63": "63 years", + "n09000009__age_64": "64 years", + "n09000009__age_65": "65 years", + "n09000009__age_66": "66 years", + "n09000009__age_67": "67 years", + "n09000009__age_68": "68 years", + "n09000009__age_69": "69 years", + "n09000009__age_7": "7 years", + "n09000009__age_70": "70 years", + "n09000009__age_71": "71 years", + "n09000009__age_72": "72 years", + "n09000009__age_73": "73 years", + "n09000009__age_74": "74 years", + "n09000009__age_75": "75 years", + "n09000009__age_76": "76 years", + "n09000009__age_77": "77 years", + "n09000009__age_78": "78 years", + "n09000009__age_79": "79 years", + "n09000009__age_8": "8 years", + "n09000009__age_80": "80 years", + "n09000009__age_81": "81 years", + "n09000009__age_82": "82 years", + "n09000009__age_83": "83 years", + "n09000009__age_84": "84 years", + "n09000009__age_85": "85 years", + "n09000009__age_86": "86 years", + "n09000009__age_87": "87 years", + "n09000009__age_88": "88 years", + "n09000009__age_89": "89 years", + "n09000009__age_9": "9 years", + "n09000009__age_90": "90 years", + "n09000009__age_91": "91 years", + "n09000009__age_92": "92 years", + "n09000009__age_93": "93 years", + "n09000009__age_94": "94 years", + "n09000009__age_95": "95 years", + "n09000009__age_96": "96 years", + "n09000009__age_97": "97 years", + "n09000009__age_98": "98 years", + "n09000009__age_99": "99 years", + "n09000010__age_0": "0 years", + "n09000010__age_1": "1 year", + "n09000010__age_10": "10 years", + "n09000010__age_100": "100+ years", + "n09000010__age_11": "11 years", + "n09000010__age_12": "12 years", + "n09000010__age_13": "13 years", + "n09000010__age_14": "14 years", + "n09000010__age_15": "15 years", + "n09000010__age_16": "16 years", + "n09000010__age_17": "17 years", + "n09000010__age_18": "18 years", + "n09000010__age_19": "19 years", + "n09000010__age_2": "2 years", + "n09000010__age_20": "20 years", + "n09000010__age_21": "21 years", + "n09000010__age_22": "22 years", + "n09000010__age_23": "23 years", + "n09000010__age_24": "24 years", + "n09000010__age_25": "25 years", + "n09000010__age_26": "26 years", + "n09000010__age_27": "27 years", + "n09000010__age_28": "28 years", + "n09000010__age_29": "29 years", + "n09000010__age_3": "3 years", + "n09000010__age_30": "30 years", + "n09000010__age_31": "31 years", + "n09000010__age_32": "32 years", + "n09000010__age_33": "33 years", + "n09000010__age_34": "34 years", + "n09000010__age_35": "35 years", + "n09000010__age_36": "36 years", + "n09000010__age_37": "37 years", + "n09000010__age_38": "38 years", + "n09000010__age_39": "39 years", + "n09000010__age_4": "4 years", + "n09000010__age_40": "40 years", + "n09000010__age_41": "41 years", + "n09000010__age_42": "42 years", + "n09000010__age_43": "43 years", + "n09000010__age_44": "44 years", + "n09000010__age_45": "45 years", + "n09000010__age_46": "46 years", + "n09000010__age_47": "47 years", + "n09000010__age_48": "48 years", + "n09000010__age_49": "49 years", + "n09000010__age_5": "5 years", + "n09000010__age_50": "50 years", + "n09000010__age_51": "51 years", + "n09000010__age_52": "52 years", + "n09000010__age_53": "53 years", + "n09000010__age_54": "54 years", + "n09000010__age_55": "55 years", + "n09000010__age_56": "56 years", + "n09000010__age_57": "57 years", + "n09000010__age_58": "58 years", + "n09000010__age_59": "59 years", + "n09000010__age_6": "6 years", + "n09000010__age_60": "60 years", + "n09000010__age_61": "61 years", + "n09000010__age_62": "62 years", + "n09000010__age_63": "63 years", + "n09000010__age_64": "64 years", + "n09000010__age_65": "65 years", + "n09000010__age_66": "66 years", + "n09000010__age_67": "67 years", + "n09000010__age_68": "68 years", + "n09000010__age_69": "69 years", + "n09000010__age_7": "7 years", + "n09000010__age_70": "70 years", + "n09000010__age_71": "71 years", + "n09000010__age_72": "72 years", + "n09000010__age_73": "73 years", + "n09000010__age_74": "74 years", + "n09000010__age_75": "75 years", + "n09000010__age_76": "76 years", + "n09000010__age_77": "77 years", + "n09000010__age_78": "78 years", + "n09000010__age_79": "79 years", + "n09000010__age_8": "8 years", + "n09000010__age_80": "80 years", + "n09000010__age_81": "81 years", + "n09000010__age_82": "82 years", + "n09000010__age_83": "83 years", + "n09000010__age_84": "84 years", + "n09000010__age_85": "85 years", + "n09000010__age_86": "86 years", + "n09000010__age_87": "87 years", + "n09000010__age_88": "88 years", + "n09000010__age_89": "89 years", + "n09000010__age_9": "9 years", + "n09000010__age_90": "90 years", + "n09000010__age_91": "91 years", + "n09000010__age_92": "92 years", + "n09000010__age_93": "93 years", + "n09000010__age_94": "94 years", + "n09000010__age_95": "95 years", + "n09000010__age_96": "96 years", + "n09000010__age_97": "97 years", + "n09000010__age_98": "98 years", + "n09000010__age_99": "99 years", + "n09000011__age_0": "0 years", + "n09000011__age_1": "1 year", + "n09000011__age_10": "10 years", + "n09000011__age_100": "100+ years", + "n09000011__age_11": "11 years", + "n09000011__age_12": "12 years", + "n09000011__age_13": "13 years", + "n09000011__age_14": "14 years", + "n09000011__age_15": "15 years", + "n09000011__age_16": "16 years", + "n09000011__age_17": "17 years", + "n09000011__age_18": "18 years", + "n09000011__age_19": "19 years", + "n09000011__age_2": "2 years", + "n09000011__age_20": "20 years", + "n09000011__age_21": "21 years", + "n09000011__age_22": "22 years", + "n09000011__age_23": "23 years", + "n09000011__age_24": "24 years", + "n09000011__age_25": "25 years", + "n09000011__age_26": "26 years", + "n09000011__age_27": "27 years", + "n09000011__age_28": "28 years", + "n09000011__age_29": "29 years", + "n09000011__age_3": "3 years", + "n09000011__age_30": "30 years", + "n09000011__age_31": "31 years", + "n09000011__age_32": "32 years", + "n09000011__age_33": "33 years", + "n09000011__age_34": "34 years", + "n09000011__age_35": "35 years", + "n09000011__age_36": "36 years", + "n09000011__age_37": "37 years", + "n09000011__age_38": "38 years", + "n09000011__age_39": "39 years", + "n09000011__age_4": "4 years", + "n09000011__age_40": "40 years", + "n09000011__age_41": "41 years", + "n09000011__age_42": "42 years", + "n09000011__age_43": "43 years", + "n09000011__age_44": "44 years", + "n09000011__age_45": "45 years", + "n09000011__age_46": "46 years", + "n09000011__age_47": "47 years", + "n09000011__age_48": "48 years", + "n09000011__age_49": "49 years", + "n09000011__age_5": "5 years", + "n09000011__age_50": "50 years", + "n09000011__age_51": "51 years", + "n09000011__age_52": "52 years", + "n09000011__age_53": "53 years", + "n09000011__age_54": "54 years", + "n09000011__age_55": "55 years", + "n09000011__age_56": "56 years", + "n09000011__age_57": "57 years", + "n09000011__age_58": "58 years", + "n09000011__age_59": "59 years", + "n09000011__age_6": "6 years", + "n09000011__age_60": "60 years", + "n09000011__age_61": "61 years", + "n09000011__age_62": "62 years", + "n09000011__age_63": "63 years", + "n09000011__age_64": "64 years", + "n09000011__age_65": "65 years", + "n09000011__age_66": "66 years", + "n09000011__age_67": "67 years", + "n09000011__age_68": "68 years", + "n09000011__age_69": "69 years", + "n09000011__age_7": "7 years", + "n09000011__age_70": "70 years", + "n09000011__age_71": "71 years", + "n09000011__age_72": "72 years", + "n09000011__age_73": "73 years", + "n09000011__age_74": "74 years", + "n09000011__age_75": "75 years", + "n09000011__age_76": "76 years", + "n09000011__age_77": "77 years", + "n09000011__age_78": "78 years", + "n09000011__age_79": "79 years", + "n09000011__age_8": "8 years", + "n09000011__age_80": "80 years", + "n09000011__age_81": "81 years", + "n09000011__age_82": "82 years", + "n09000011__age_83": "83 years", + "n09000011__age_84": "84 years", + "n09000011__age_85": "85 years", + "n09000011__age_86": "86 years", + "n09000011__age_87": "87 years", + "n09000011__age_88": "88 years", + "n09000011__age_89": "89 years", + "n09000011__age_9": "9 years", + "n09000011__age_90": "90 years", + "n09000011__age_91": "91 years", + "n09000011__age_92": "92 years", + "n09000011__age_93": "93 years", + "n09000011__age_94": "94 years", + "n09000011__age_95": "95 years", + "n09000011__age_96": "96 years", + "n09000011__age_97": "97 years", + "n09000011__age_98": "98 years", + "n09000011__age_99": "99 years" + }, + "C2021_AGE_24_NAME": { + "Aged 10 to 14 years": "Aged 10 to 14 years", + "Aged 15 years": "Aged 15 years", + "Aged 16 to 17 years": "Aged 16 to 17 years", + "Aged 18 to 19 years": "Aged 18 to 19 years", + "Aged 2 years and under": "Aged 2 years and under", + "Aged 20 to 24 years": "Aged 20 to 24 years", + "Aged 25 to 29 years": "Aged 25 to 29 years", + "Aged 3 to 4 years": "Aged 3 to 4 years", + "Aged 30 to 34 years": "Aged 30 to 34 years", + "Aged 35 to 39 years": "Aged 35 to 39 years", + "Aged 40 to 44 years": "Aged 40 to 44 years", + "Aged 45 to 49 years": "Aged 45 to 49 years", + "Aged 5 to 7 years": "Aged 5 to 7 years", + "Aged 50 to 54 years": "Aged 50 to 54 years", + "Aged 55 to 59 years": "Aged 55 to 59 years", + "Aged 60 to 64 years": "Aged 60 to 64 years", + "Aged 65 years": "Aged 65 years", + "Aged 66 to 69 years": "Aged 66 to 69 years", + "Aged 70 to 74 years": "Aged 70 to 74 years", + "Aged 75 to 79 years": "Aged 75 to 79 years", + "Aged 8 to 9 years": "Aged 8 to 9 years", + "Aged 80 to 84 years": "Aged 80 to 84 years", + "Aged 85 years and over": "Aged 85 years and over", + "Total": "Total" + }, + "C2021_RESTYPE_3_NAME": { + "Lives in a communal establishment": "Lives in a communal establishment", + "Lives in a household": "Lives in a household" + }, + "C_SEX_NAME": { + "All persons": "All persons", + "Female": "Female", + "Male": "Male" + }, + "Household Type": { + "Total households": "Total households" + }, + "Residence Type Indicator": { + "Lives in a communal establishment": "Lives in a communal establishment", + "Lives in a household": "Lives in a household" + }, + "Residence Type Label": { + "Person in a communal establishment": "Person in a communal establishment", + "Person in a household": "Person in a household" + }, + "Sex": { + "All people": "All people", + "Female": "Female", + "Male": "Male" + }, + "Sex Label": { + "Female": "Female", + "Male": "Male" + }, + "administering_entity": { + "state_programs": "State programs" + }, + "age": { + "0": "0", + "0_to_4": "Age 0 to 4", + "1": "1", + "10": "10", + "10_to_14": "Age 10 to 14", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "15_to_19": "Age 15 to 19", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "20_to_24": "Age 20 to 24", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "25_to_29": "Age 25 to 29", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "30_to_34": "Age 30 to 34", + "31": "31", + "32": "32", + "33": "33", + "34": "34", + "35": "35", + "35_to_39": "Age 35 to 39", + "36": "36", + "37": "37", + "38": "38", + "39": "39", + "4": "4", + "40": "40", + "40_to_44": "Age 40 to 44", + "41": "41", + "42": "42", + "43": "43", + "44": "44", + "45": "45", + "45_to_49": "Age 45 to 49", + "46": "46", + "47": "47", + "48": "48", + "49": "49", + "5": "5", + "50": "50", + "50_to_54": "Age 50 to 54", + "51": "51", + "52": "52", + "53": "53", + "54": "54", + "55": "55", + "55_to_59": "Age 55 to 59", + "56": "56", + "57": "57", + "58": "58", + "59": "59", + "5_to_9": "Age 5 to 9", + "6": "6", + "60": "60", + "60_to_64": "Age 60 to 64", + "61": "61", + "62": "62", + "63": "63", + "64": "64", + "65": "65", + "65_to_69": "Age 65 to 69", + "66": "66", + "67": "67", + "68": "68", + "69": "69", + "7": "7", + "70": "70", + "70_to_74": "Age 70 to 74", + "71": "71", + "72": "72", + "73": "73", + "74": "74", + "75": "75", + "75_to_79": "Age 75 to 79", + "76": "76", + "77": "77", + "78": "78", + "79": "79", + "8": "8", + "80": "80", + "80_to_84": "Age 80 to 84", + "81": "81", + "82": "82", + "83": "83", + "84": "84", + "85": "85", + "85_plus": "Age 85 plus", + "86": "86", + "87": "87", + "88": "88", + "89": "89", + "9": "9", + "90_plus": "90 and over", + "age_0_to_4": "Age 0 to 4", + "age_10_to_14": "Age 10 to 14", + "age_15_to_19": "Age 15 to 19", + "age_20": "Age 20", + "age_20_to_24": "Age 20 to 24", + "age_21": "Age 21", + "age_25_to_29": "Age 25 to 29", + "age_30_to_34": "Age 30 to 34", + "age_35_to_39": "Age 35 to 39", + "age_40_to_44": "Age 40 to 44", + "age_45_to_49": "Age 45 to 49", + "age_50_to_54": "Age 50 to 54", + "age_55_to_59": "Age 55 to 59", + "age_5_to_9": "Age 5 to 9", + "age_60_to_64": "Age 60 to 64", + "age_65_to_69": "Age 65 to 69", + "age_70_to_74": "Age 70 to 74", + "age_75_to_79": "Age 75 to 79", + "age_80_to_84": "Age 80 to 84", + "age_85_plus": "Age 85 and over", + "all": "All ages", + "northern_ireland": "Northern Ireland", + "scotland": "Scotland", + "uk": "United Kingdom", + "wales": "Wales" + }, + "age_band": { + "16 to 21": "16 to 21", + "22 to 29": "22 to 29", + "30 to 39": "30 to 39", + "40 to 49": "40 to 49", + "50 to 54": "50 to 54", + "55 to 59": "55 to 59", + "60 to 64": "60 to 64", + "65 and over": "65 and over", + "66-69": "66-69", + "70 to 74": "70 to 74", + "70-74": "70-74", + "75 to 79": "75 to 79", + "75-79": "75-79", + "80 and over": "80 and over", + "80-84": "80-84", + "85-89": "85-89", + "90 and over": "90 and over", + "age_35_to_44": "35 to 44", + "age_45_to_54": "45 to 54", + "age_55_to_64": "55 to 64", + "age_66_69": "Aged 66-69", + "age_70_74": "Aged 70-74", + "age_75_79": "Aged 75-79", + "age_80_84": "Aged 80-84", + "age_85_89": "Aged 85-89", + "age_90_plus": "Aged 90 and over", + "all": "All ages", + "all_ages": "All", + "up to 69": "up to 69" + }, + "age_bands_and_single_year": { + "65-69": "65-69", + "70-74": "70-74", + "75-79": "75-79", + "80-84": "80-84", + "85-89": "85-89", + "90 and over": "90 and over", + "Unknown": "Unknown", + "all": "Total" + }, + "age_name": { + "Aged 1 - 4 years": "Aged 1 - 4 years", + "Aged 10 - 14 years": "Aged 10 - 14 years", + "Aged 15 - 19 years": "Aged 15 - 19 years", + "Aged 20 - 24 years": "Aged 20 - 24 years", + "Aged 25 - 29 years": "Aged 25 - 29 years", + "Aged 30 - 34 years": "Aged 30 - 34 years", + "Aged 35 - 39 years": "Aged 35 - 39 years", + "Aged 40 - 44 years": "Aged 40 - 44 years", + "Aged 45 - 49 years": "Aged 45 - 49 years", + "Aged 5 - 9 years": "Aged 5 - 9 years", + "Aged 50 - 54 years": "Aged 50 - 54 years", + "Aged 55 - 59 years": "Aged 55 - 59 years", + "Aged 60 - 64 years": "Aged 60 - 64 years", + "Aged 65 - 69 years": "Aged 65 - 69 years", + "Aged 70 - 74 years": "Aged 70 - 74 years", + "Aged 75 - 79 years": "Aged 75 - 79 years", + "Aged 80 - 84 years": "Aged 80 - 84 years", + "Aged 85 and over": "Aged 85 and over", + "Aged under 1 year": "Aged under 1 year", + "All ages": "All ages" + }, + "agi_lower_usd": { + "1": "1", + "10000": "10000", + "100000": "100000", + "1000000": "1000000", + "10000000": "10000000", + "15000": "15000", + "1500000": "1500000", + "20000": "20000", + "200000": "200000", + "2000000": "2000000", + "25000": "25000", + "30000": "30000", + "40000": "40000", + "5000": "5000", + "50000": "50000", + "500000": "500000", + "5000000": "5000000", + "75000": "75000" + }, + "agi_upper_usd": { + "1": "1", + "10000": "10000", + "100000": "100000", + "1000000": "1000000", + "10000000": "10000000", + "15000": "15000", + "1500000": "1500000", + "20000": "20000", + "200000": "200000", + "2000000": "2000000", + "25000": "25000", + "30000": "30000", + "40000": "40000", + "5000": "5000", + "50000": "50000", + "500000": "500000", + "5000000": "5000000", + "75000": "75000" + }, + "amount": { + "all": "Total", + "£100": "£100", + "£150": "£150", + "£200": "£200", + "£250": "£250", + "£300": "£300", + "£350": "£350", + "£500": "£500", + "£600": "£600" + }, + "benefit_expense_component": { + "Accommodation Assistance": "Accommodation Assistance", + "Disability Assistance": "Disability Assistance", + "Hardship Assistance": "Hardship Assistance", + "Jobseeker Support and Emergency Benefit": "Jobseeker Support and Emergency Benefit", + "New Zealand Superannuation": "New Zealand Superannuation", + "Orphan's/Unsupported Child's Benefit": "Orphan's/Unsupported Child's Benefit", + "Sole Parent Support": "Sole Parent Support", + "Student Allowances": "Student Allowances", + "Supported Living Payment": "Supported Living Payment" + }, + "business_size_band": { + "1 to 12 employees": "1 to 12 employees", + "100 to 499 employees": "100 to 499 employees", + "1000 or more employees": "1000 or more employees", + "13 to 99 employees": "13 to 99 employees", + "500 to 999 employees": "500 to 999 employees" + }, + "c2021_hh_1_name": { + "Number of households": "Number of households" + }, + "category_of_pension": { + "Category A": "Category A", + "Category AB": "Category AB", + "Category ABL": "Category ABL", + "Category B": "Category B", + "Category BL": "Category BL", + "Category D": "Category D", + "New State Pension": "New State Pension", + "Pre-2016 State Pension": "Pre-2016 State Pension", + "all": "Total" + }, + "cgt_gain_band": { + "gain_1000000_to_1999999": "GBP 1,000,000 to GBP 1,999,999", + "gain_100000_to_249999": "GBP 100,000 to GBP 249,999", + "gain_2000000_to_4999999": "GBP 2,000,000 to GBP 4,999,999", + "gain_250000_to_499999": "GBP 250,000 to GBP 499,999", + "gain_25000_to_49999": "GBP 25,000 to GBP 49,999", + "gain_5000000_plus": "GBP 5,000,000 and over", + "gain_500000_to_999999": "GBP 500,000 to GBP 999,999", + "gain_50000_to_99999": "GBP 50,000 to GBP 99,999" + }, + "child_entitlement": { + "No": "No", + "Yes": "Yes" + }, + "client_type": { + "Pension age": "Pension age", + "Working age": "Working age", + "all": "Total" + }, + "cms_aca.state_abrvtn": { + "ak": "Alaska", + "al": "Alabama", + "ar": "Arkansas", + "az": "Arizona", + "ca": "California", + "co": "Colorado", + "ct": "Connecticut", + "dc": "District of Columbia", + "de": "Delaware", + "fl": "Florida", + "ga": "Georgia", + "hi": "Hawaii", + "ia": "Iowa", + "id": "Idaho", + "il": "Illinois", + "in": "Indiana", + "ks": "Kansas", + "ky": "Kentucky", + "la": "Louisiana", + "ma": "Massachusetts", + "md": "Maryland", + "me": "Maine", + "mi": "Michigan", + "mn": "Minnesota", + "mo": "Missouri", + "ms": "Mississippi", + "mt": "Montana", + "nc": "North Carolina", + "nd": "North Dakota", + "ne": "Nebraska", + "nh": "New Hampshire", + "nj": "New Jersey", + "nm": "New Mexico", + "nv": "Nevada", + "ny": "New York", + "oh": "Ohio", + "ok": "Oklahoma", + "or": "Oregon", + "pa": "Pennsylvania", + "ri": "Rhode Island", + "sc": "South Carolina", + "sd": "South Dakota", + "tn": "Tennessee", + "tx": "Texas", + "ut": "Utah", + "va": "Virginia", + "vt": "Vermont", + "wa": "Washington", + "wi": "Wisconsin", + "wv": "West Virginia", + "wy": "Wyoming" + }, + "cms_medicaid.state_abbreviation": { + "ak": "Alaska", + "al": "Alabama", + "ar": "Arkansas", + "az": "Arizona", + "ca": "California", + "co": "Colorado", + "ct": "Connecticut", + "dc": "District of Columbia", + "de": "Delaware", + "fl": "Florida", + "ga": "Georgia", + "hi": "Hawaii", + "ia": "Iowa", + "id": "Idaho", + "il": "Illinois", + "in": "Indiana", + "ks": "Kansas", + "ky": "Kentucky", + "la": "Louisiana", + "ma": "Massachusetts", + "md": "Maryland", + "me": "Maine", + "mi": "Michigan", + "mn": "Minnesota", + "mo": "Missouri", + "ms": "Mississippi", + "mt": "Montana", + "nc": "North Carolina", + "nd": "North Dakota", + "ne": "Nebraska", + "nh": "New Hampshire", + "nj": "New Jersey", + "nm": "New Mexico", + "nv": "Nevada", + "ny": "New York", + "oh": "Ohio", + "ok": "Oklahoma", + "or": "Oregon", + "pa": "Pennsylvania", + "ri": "Rhode Island", + "sc": "South Carolina", + "sd": "South Dakota", + "tn": "Tennessee", + "tx": "Texas", + "ut": "Utah", + "va": "Virginia", + "vt": "Vermont", + "wa": "Washington", + "wi": "Wisconsin", + "wv": "West Virginia", + "wy": "Wyoming" + }, + "contribution_basis": { + "full_pay": "Percentage of full pay", + "qualifying_earnings": "Percentage of qualifying earnings" + }, + "contribution_type": { + "Employer contributions to net pay arrangements and on employer deficit reduction contributions": "Employer contributions to net pay arrangements plus deficit reduction contributions", + "Employer contributions to net pay arrangements plus deficit reduction contributions": "Employer contributions to net pay arrangements plus deficit reduction contributions", + "Employer contributions to relief at source schemes": "Employer contributions to relief at source schemes", + "Individual contributions to net pay arrangements": "Individual contributions to net pay arrangements", + "Individual contributions to relief at source schemes": "Individual contributions to relief at source schemes", + "Salary sacrificed contributions": "Salary sacrificed contributions" + }, + "council_tax_band": { + "A": "Band A", + "A-": "Band A-", + "B": "Band B", + "C": "Band C", + "D": "Band D", + "E": "Band E", + "F": "Band F", + "G": "Band G", + "H": "Band H" + }, + "demographic_group": { + "be1_female_0_17": "Brussels Capital Region female aged 0 to 17", + "be1_female_18_64": "Brussels Capital Region female aged 18 to 64", + "be1_female_65_plus": "Brussels Capital Region female aged 65 and over", + "be1_male_0_17": "Brussels Capital Region male aged 0 to 17", + "be1_male_18_64": "Brussels Capital Region male aged 18 to 64", + "be1_male_65_plus": "Brussels Capital Region male aged 65 and over", + "be2_female_0_17": "Flemish Region female aged 0 to 17", + "be2_female_18_64": "Flemish Region female aged 18 to 64", + "be2_female_65_plus": "Flemish Region female aged 65 and over", + "be2_male_0_17": "Flemish Region male aged 0 to 17", + "be2_male_18_64": "Flemish Region male aged 18 to 64", + "be2_male_65_plus": "Flemish Region male aged 65 and over", + "be3_female_0_17": "Walloon Region female aged 0 to 17", + "be3_female_18_64": "Walloon Region female aged 18 to 64", + "be3_female_65_plus": "Walloon Region female aged 65 and over", + "be3_male_0_17": "Walloon Region male aged 0 to 17", + "be3_male_18_64": "Walloon Region male aged 18 to 64", + "be3_male_65_plus": "Walloon Region male aged 65 and over" + }, + "desnz.energy_trends.series": { + "domestic_consumption": "Domestic consumption" + }, + "desnz.need.household_income_band": { + "all_dwellings": "All dwellings", + "gbp100_000_gbp149_999": "£100,000 - £149,999", + "gbp150_000_or_more": "£150,000 or more", + "gbp15_000_gbp19_999": "£15,000 - £19,999", + "gbp20_000_gbp29_999": "£20,000 - £29,999", + "gbp30_000_gbp39_999": "£30,000 - £39,999", + "gbp40_000_gbp49_999": "£40,000 - £49,999", + "gbp50_000_gbp59_999": "£50,000 - £59,999", + "gbp60_000_gbp69_999": "£60,000 - £69,999", + "gbp70_000_gbp99_999": "£70,000 - £99,999", + "less_than_gbp15_000": "Less than £15,000" + }, + "desnz.need.property_type": { + "all_dwellings": "All dwellings", + "all_flats": "All flats", + "all_houses": "All houses", + "bungalow": "Bungalow", + "converted_flat": "Converted flat", + "detached": "Detached", + "end_terrace": "End terrace", + "mid_terrace": "Mid terrace", + "purpose_built_flat": "Purpose built flat", + "semi_detached": "Semi detached", + "terraced": "Terraced" + }, + "desnz.need.region": { + "all_dwellings": "All dwellings", + "east_midlands": "East Midlands", + "east_of_england": "East of England", + "london": "London", + "north_east": "North East", + "north_west": "North West", + "south_east": "South East", + "south_west": "South West", + "wales": "Wales", + "west_midlands": "West Midlands", + "yorkshire_and_the_humber": "Yorkshire and The Humber" + }, + "desnz.need.tenure": { + "all_dwellings": "All dwellings", + "council_or_housing_association": "Council or Housing Association", + "owner_occupied": "Owner-occupied", + "privately_rented": "Privately rented" + }, + "desnz.qep.bill_scope": { + "united_kingdom": "United Kingdom" + }, + "desnz.qep.price_region": { + "east_midlands": "East Midlands", + "eastern": "Eastern", + "london": "London", + "merseyside_north_wales": "Merseyside & North Wales", + "north_east": "North East", + "north_scotland": "North Scotland", + "north_west": "North West", + "south_east": "South East", + "south_scotland": "South Scotland", + "south_wales": "South Wales", + "south_west": "South West", + "southern": "Southern", + "united_kingdom": "United Kingdom", + "west_midlands": "West Midlands", + "yorkshire": "Yorkshire" + }, + "desnz.subnational.area": { + "unallocated": "Unallocated" + }, + "dfi_ni.bus_service": { + "metro_glider": "Metro / Glider" + }, + "dft.bus_area": { + "england": "England", + "england_outside_london": "England outside London", + "english_metropolitan_areas": "English metropolitan areas", + "english_non_metropolitan_areas": "English non-metropolitan areas", + "london": "London" + }, + "dft.survey_year": { + "cy2023": "2023", + "cy2024": "2024", + "cy2025": "2025" + }, + "domestic_scope": { + "all_domestic": "All domestic meters", + "all_meters": "All meters", + "all_non_domestic": "All non-domestic meters" + }, + "dwp.winter_fuel_payment_country_age_amount_gender": { + "e92000001__70_74__amount_all__female": "England, 70-74, all amounts, Female", + "e92000001__70_74__amount_all__gender_all": "England, 70-74, all amounts, all genders", + "e92000001__70_74__amount_all__male": "England, 70-74, all amounts, Male", + "e92000001__75_79__amount_all__female": "England, 75-79, all amounts, Female", + "e92000001__75_79__amount_all__gender_all": "England, 75-79, all amounts, all genders", + "e92000001__75_79__amount_all__male": "England, 75-79, all amounts, Male", + "e92000001__80_84__300__female": "England, 80-84, £300, Female", + "e92000001__80_84__300__gender_all": "England, 80-84, £300, all genders", + "e92000001__80_84__300__male": "England, 80-84, £300, Male", + "e92000001__80_84__amount_all__female": "England, 80-84, all amounts, Female", + "e92000001__80_84__amount_all__gender_all": "England, 80-84, all amounts, all genders", + "e92000001__80_84__amount_all__male": "England, 80-84, all amounts, Male", + "e92000001__85_89__300__female": "England, 85-89, £300, Female", + "e92000001__85_89__300__gender_all": "England, 85-89, £300, all genders", + "e92000001__85_89__300__male": "England, 85-89, £300, Male", + "e92000001__85_89__amount_all__female": "England, 85-89, all amounts, Female", + "e92000001__85_89__amount_all__gender_all": "England, 85-89, all amounts, all genders", + "e92000001__85_89__amount_all__male": "England, 85-89, all amounts, Male", + "e92000001__90_and_over__300__female": "England, 90 and over, £300, Female", + "e92000001__90_and_over__300__gender_all": "England, 90 and over, £300, all genders", + "e92000001__90_and_over__300__male": "England, 90 and over, £300, Male", + "e92000001__90_and_over__amount_all__female": "England, 90 and over, all amounts, Female", + "e92000001__90_and_over__amount_all__gender_all": "England, 90 and over, all amounts, all genders", + "e92000001__90_and_over__amount_all__male": "England, 90 and over, all amounts, Male", + "e92000001__age_bands_and_single_year_all__300__female": "England, all ages, £300, Female", + "e92000001__age_bands_and_single_year_all__300__gender_all": "England, all ages, £300, all genders", + "e92000001__age_bands_and_single_year_all__300__male": "England, all ages, £300, Male", + "e92000001__age_bands_and_single_year_all__amount_all__female": "England, all ages, all amounts, Female", + "e92000001__age_bands_and_single_year_all__amount_all__gender_all": "England, all ages, all amounts, all genders", + "e92000001__age_bands_and_single_year_all__amount_all__male": "England, all ages, all amounts, Male", + "w92000004__70_74__amount_all__female": "Wales, 70-74, all amounts, Female", + "w92000004__70_74__amount_all__gender_all": "Wales, 70-74, all amounts, all genders", + "w92000004__70_74__amount_all__male": "Wales, 70-74, all amounts, Male", + "w92000004__75_79__amount_all__female": "Wales, 75-79, all amounts, Female", + "w92000004__75_79__amount_all__gender_all": "Wales, 75-79, all amounts, all genders", + "w92000004__75_79__amount_all__male": "Wales, 75-79, all amounts, Male", + "w92000004__80_84__300__female": "Wales, 80-84, £300, Female", + "w92000004__80_84__300__gender_all": "Wales, 80-84, £300, all genders", + "w92000004__80_84__300__male": "Wales, 80-84, £300, Male", + "w92000004__80_84__amount_all__female": "Wales, 80-84, all amounts, Female", + "w92000004__80_84__amount_all__gender_all": "Wales, 80-84, all amounts, all genders", + "w92000004__80_84__amount_all__male": "Wales, 80-84, all amounts, Male", + "w92000004__85_89__300__female": "Wales, 85-89, £300, Female", + "w92000004__85_89__300__gender_all": "Wales, 85-89, £300, all genders", + "w92000004__85_89__300__male": "Wales, 85-89, £300, Male", + "w92000004__85_89__amount_all__female": "Wales, 85-89, all amounts, Female", + "w92000004__85_89__amount_all__gender_all": "Wales, 85-89, all amounts, all genders", + "w92000004__85_89__amount_all__male": "Wales, 85-89, all amounts, Male", + "w92000004__90_and_over__300__female": "Wales, 90 and over, £300, Female", + "w92000004__90_and_over__300__gender_all": "Wales, 90 and over, £300, all genders", + "w92000004__90_and_over__300__male": "Wales, 90 and over, £300, Male", + "w92000004__90_and_over__amount_all__female": "Wales, 90 and over, all amounts, Female", + "w92000004__90_and_over__amount_all__gender_all": "Wales, 90 and over, all amounts, all genders", + "w92000004__90_and_over__amount_all__male": "Wales, 90 and over, all amounts, Male", + "w92000004__age_bands_and_single_year_all__300__female": "Wales, all ages, £300, Female", + "w92000004__age_bands_and_single_year_all__300__gender_all": "Wales, all ages, £300, all genders", + "w92000004__age_bands_and_single_year_all__300__male": "Wales, all ages, £300, Male", + "w92000004__age_bands_and_single_year_all__amount_all__female": "Wales, all ages, all amounts, Female", + "w92000004__age_bands_and_single_year_all__amount_all__gender_all": "Wales, all ages, all amounts, all genders", + "w92000004__age_bands_and_single_year_all__amount_all__male": "Wales, all ages, all amounts, Male" + }, + "early_year_award_line": { + "new_returning_total_returning_31_33_35": "Total returning [31][33][35]" + }, + "eitc_child_count": { + "0": "0", + "1": "1", + "2": "2", + "3plus": "Three or more qualifying children" + }, + "employee_contribution_rate_band": { + "0%": "0%", + "2% to < 3%": "2% to < 3%", + "3% to < 4%": "3% to < 4%", + "4% to < 5%": "4% to < 5%", + "5% to < 6%": "5% to < 6%", + "6% to < 7%": "6% to < 7%", + "7% and over": "7% and over", + "Under 2%": "Under 2%" + }, + "employer_contribution_rate_band": { + "10% to < 12%": "10% to < 12%", + "12% to < 15%": "12% to < 15%", + "15% to < 20%": "15% to < 20%", + "20% and over": "20% and over", + "4% to < 8%": "4% to < 8%", + "8% to < 10%": "8% to < 10%", + "Under 4%": "Under 4%" + }, + "family_type": { + "Couple, no children": "Couple, no children", + "Couple, with children": "Couple, with children", + "Single, no children": "Single, no children", + "Single, with children": "Single, with children", + "Unknown or missing family type": "Unknown or missing family type" + }, + "filing_status": { + "all": "All filing statuses" + }, + "freq": { + "A": "A" + }, + "frequency": { + "annual": "Annual", + "quarterly": "Quarterly" + }, + "fuel": { + "electricity": "Electricity", + "gas": "Gas" + }, + "funding_component": { + "all_government_support": "All government support", + "concessionary_fares": "Concessionary fares", + "local_authority_bus_support": "Local authority bus support", + "network_support_grant": "Network Support Grant" + }, + "gender": { + "Female": "Female", + "Male": "Male", + "Unknown": "Unknown", + "all": "Total" + }, + "geography": { + "northern_ireland": "Northern Ireland" + }, + "groeipakket.component": { + "pleegzorgtoeslag": "Foster care supplement", + "sociale_toeslag": "Social supplement", + "wezentoeslag": "Orphan supplement", + "zorgtoeslag": "Care supplement" + }, + "grouped_amount_of_benefit": { + "Under £20.00": "Under £20.00", + "all": "Total", + "£100.00 to under £150.00": "£100.00 to under £150.00", + "£20.00 to under £40.00": "£20.00 to under £40.00", + "£40.00 to under £60.00": "£40.00 to under £60.00", + "£60.00 to under £80.00": "£60.00 to under £80.00", + "£80.00 to under £100.00": "£80.00 to under £100.00" + }, + "heating_type": { + "no_central_heating": "No central heating" + }, + "hmrc.pension_relief_breakdown": { + "additional_rate": "Additional Rate", + "basic_rate": "Basic Rate", + "defined_benefit": "Defined benefit", + "defined_contribution": "Defined contribution", + "higher_rate": "Higher Rate", + "main_rate": "Main Rate", + "personal_or_private_sector_occupational_scheme": "Personal or private sector occupational scheme", + "public_sector_occupational_scheme": "Public sector occupational scheme", + "total": "Total" + }, + "hmrc.ss_relief_line": { + "it_relief_additional_rate": "Income tax relief, additional rate", + "it_relief_basic_rate": "Income tax relief, basic rate", + "it_relief_higher_rate": "Income tax relief, higher rate", + "it_relief_total": "Income tax relief on salary sacrificed contributions, total", + "nics_relief_employee": "Class 1 primary (employee) NICs relief", + "nics_relief_employer": "Class 1 secondary (employer) NICs relief" + }, + "hmrc.total_income_band": { + "all_ranges": "All ranges of total income", + "band_100000_150000": "Total income from GBP 100 000 to GBP 150 000", + "band_12570_15000": "Total income from GBP 12 570 to GBP 15 000", + "band_150000_200000": "Total income from GBP 150 000 to GBP 200 000", + "band_15000_20000": "Total income from GBP 15 000 to GBP 20 000", + "band_20000_30000": "Total income from GBP 20 000 to GBP 30 000", + "band_500000_1000000": "Total income from GBP 500 000 to GBP 1 000 000" + }, + "household_income_band": { + "all_dwellings": "All dwellings", + "gbp100_000_gbp149_999": "£100,000 - £149,999", + "gbp150_000_or_more": "£150,000 or more", + "gbp15_000_gbp19_999": "£15,000 - £19,999", + "gbp20_000_gbp29_999": "£20,000 - £29,999", + "gbp30_000_gbp39_999": "£30,000 - £39,999", + "gbp40_000_gbp49_999": "£40,000 - £49,999", + "gbp50_000_gbp59_999": "£50,000 - £59,999", + "gbp60_000_gbp69_999": "£60,000 - £69,999", + "gbp70_000_gbp99_999": "£70,000 - £99,999", + "less_than_gbp15_000": "Less than £15,000" + }, + "household_tenure": { + "All occupied households": "All occupied households" + }, + "household_type": { + "couple_3_plus_children_households": "3 or more dependent children" + }, + "housing_costs": { + "after_housing_costs": "After housing costs", + "before_housing_costs": "Before housing costs" + }, + "income_range": { + "100k_to_200k": "$100,000 under $200,000", + "10k_to_11k": "$10,000 under $11,000", + "10k_to_15k": "$10,000 under $15,000", + "10k_to_25k": "$10,000 under $25,000", + "10m_plus": "$10,000,000 or more", + "11k_to_12k": "$11,000 under $12,000", + "12k_to_13k": "$12,000 under $13,000", + "13k_to_14k": "$13,000 under $14,000", + "14k_to_15k": "$14,000 under $15,000", + "15k_to_16k": "$15,000 under $16,000", + "15k_to_20k": "$15,000 under $20,000", + "16k_to_17k": "$16,000 under $17,000", + "17k_to_18k": "$17,000 under $18,000", + "18k_to_19k": "$18,000 under $19,000", + "19k_to_20k": "$19,000 under $20,000", + "1_5m_to_2m": "$1,500,000 under $2,000,000", + "1_to_10k": "$1 under $10,000", + "1_to_1k": "$1 under $1,000", + "1_to_5k": "$1 under $5,000", + "1k_to_2k": "$1,000 under $2,000", + "1m_plus": "$1,000,000 or more", + "1m_to_1_5m": "$1,000,000 under $1,500,000", + "200k_to_500k": "$200,000 under $500,000", + "20k_to_25k": "$20,000 under $25,000", + "25k_to_30k": "$25,000 under $30,000", + "25k_to_50k": "$25,000 under $50,000", + "2k_to_3k": "$2,000 under $3,000", + "2m_to_5m": "$2,000,000 under $5,000,000", + "30k_to_35k": "$30,000 under $35,000", + "30k_to_40k": "$30,000 under $40,000", + "35k_to_40k": "$35,000 under $40,000", + "3k_to_4k": "$3,000 under $4,000", + "40k_to_45k": "$40,000 under $45,000", + "40k_to_50k": "$40,000 under $50,000", + "45k_to_50k": "$45,000 under $50,000", + "4k_to_5k": "$4,000 under $5,000", + "500k_to_1m": "$500,000 under $1,000,000", + "50k_plus": "$50,000 and over", + "50k_to_75k": "$50,000 under $75,000", + "5k_to_10k": "$5,000 under $10,000", + "5k_to_6k": "$5,000 under $6,000", + "5m_to_10m": "$5,000,000 under $10,000,000", + "6k_to_7k": "$6,000 under $7,000", + "75k_to_100k": "$75,000 under $100,000", + "7k_to_8k": "$7,000 under $8,000", + "8k_to_9k": "$8,000 under $9,000", + "9k_to_10k": "$9,000 under $10,000", + "all": "All income ranges", + "under_1": "No adjusted gross income" + }, + "irs_soi.ira_contribution_size": { + "all_taxpayers": "All taxpayers" + }, + "isc.census_line": { + "pupils_at_isc_schools": "Pupils at ISC member schools" + }, + "journey_category": { + "concessionary": "Concessionary passenger journeys" + }, + "low_income_threshold": { + "below_60pct_contemporary_median": "Below 60% of the same year's median income" + }, + "marginal_rate": { + "additional": "Additional rate", + "basic": "Basic rate", + "higher": "Higher rate", + "savers": "Savers rate" + }, + "measure": { + "country_total": "Country total" + }, + "metering_arrangement": { + "standard": "Standard electricity" + }, + "metric": { + "consumption": "Consumption", + "mean_consumption_per_meter": "Mean consumption per meter", + "meter_count": "Meter count" + }, + "msd_supplementary_assistance_type": { + "accommodation_supplement": "Accommodation Supplement (AS)", + "disability_allowance": "Disability Allowance (DA)", + "special_benefit_or_temporary_additional_support": "Special Benefit (SPB) or Temporary Additional Support (TAS)" + }, + "na_item": { + "D2": "D2", + "D5": "D5", + "D51": "D51", + "D61": "D61", + "belgium_household_disposable_income_gross": "Disposable income, gross (B.6G)", + "belgium_household_mixed_income_gross": "Mixed income, gross (B.3G)", + "belgium_household_operating_surplus_gross": "Operating surplus, gross (B.2G)", + "d2_be": "Belgium taxes on production and imports", + "d51_be": "Belgium taxes on income", + "d5_be": "Belgium current taxes on income and wealth", + "d61_be": "Belgium net social contributions" + }, + "nics_relief_class": { + "Class 1 Primary (employee)": "Class 1 Primary (employee)", + "Class 1 Secondary (employer)": "Class 1 Secondary (employer)" + }, + "number_of_children": { + "0": "0", + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5 or more": "5 or more", + "Not available prior to April 2019": "Not available prior to April 2019", + "Unknown or missing": "Unknown or missing" + }, + "nz_census_2023.family_type_source_label": { + "1": "Couple Without Children", + "2": "Couple With Child(ren)", + "3": "One Parent With Child(ren)" + }, + "nz_census_2023.household_size_source_label": { + "1": "One usual resident", + "2": "Two usual residents", + "3": "Three usual residents", + "4": "Four usual residents", + "5": "Five usual residents", + "6": "Six usual residents", + "7": "Seven usual residents", + "8": "Eight or more usual residents" + }, + "nz_census_2023.household_tenure_source_label": { + "1": "Owned", + "2": "Not owned", + "3": "In family trust" + }, + "nz_census_2023.total_kind": { + "families": "Families", + "households": "Households" + }, + "nz_census_2023.weekly_rent_source_label": { + "1": "Under $200", + "2": "$200 - $299", + "3": "$300 - $399", + "4": "$400 - $499", + "5": "$500 - $599", + "6": "$600 - $699", + "7": "$700 - $799", + "8": "$800 and over" + }, + "nz_census_2023.weekly_rent_statistic": { + "median": "Median weekly rent paid by households" + }, + "ons.ashe_age_pension_type": { + "16_to_21__all_pension_types": "Aged 16 to 21, all employee jobs with a pension", + "16_to_21__defined_benefit": "Aged 16 to 21, defined benefit", + "16_to_21__defined_contribution": "Aged 16 to 21, defined contribution", + "16_to_21__group_personal_pension": "Aged 16 to 21, group personal pension", + "16_to_21__group_stakeholder_pension": "Aged 16 to 21, group stakeholder pension", + "22_to_29__all_pension_types": "Aged 22 to 29, all employee jobs with a pension", + "22_to_29__defined_benefit": "Aged 22 to 29, defined benefit", + "22_to_29__defined_contribution": "Aged 22 to 29, defined contribution", + "22_to_29__group_personal_pension": "Aged 22 to 29, group personal pension", + "22_to_29__group_self_invested_personal_pension": "Aged 22 to 29, group self-invested personal pension", + "22_to_29__group_stakeholder_pension": "Aged 22 to 29, group stakeholder pension", + "30_to_39__all_pension_types": "Aged 30 to 39, all employee jobs with a pension", + "30_to_39__defined_benefit": "Aged 30 to 39, defined benefit", + "30_to_39__defined_contribution": "Aged 30 to 39, defined contribution", + "30_to_39__group_personal_pension": "Aged 30 to 39, group personal pension", + "30_to_39__group_self_invested_personal_pension": "Aged 30 to 39, group self-invested personal pension", + "30_to_39__group_stakeholder_pension": "Aged 30 to 39, group stakeholder pension", + "40_to_49__all_pension_types": "Aged 40 to 49, all employee jobs with a pension", + "40_to_49__defined_benefit": "Aged 40 to 49, defined benefit", + "40_to_49__defined_contribution": "Aged 40 to 49, defined contribution", + "40_to_49__group_personal_pension": "Aged 40 to 49, group personal pension", + "40_to_49__group_self_invested_personal_pension": "Aged 40 to 49, group self-invested personal pension", + "40_to_49__group_stakeholder_pension": "Aged 40 to 49, group stakeholder pension", + "50_to_54__all_pension_types": "Aged 50 to 54, all employee jobs with a pension", + "50_to_54__defined_benefit": "Aged 50 to 54, defined benefit", + "50_to_54__defined_contribution": "Aged 50 to 54, defined contribution", + "50_to_54__group_personal_pension": "Aged 50 to 54, group personal pension", + "50_to_54__group_self_invested_personal_pension": "Aged 50 to 54, group self-invested personal pension", + "50_to_54__group_stakeholder_pension": "Aged 50 to 54, group stakeholder pension", + "55_to_59__all_pension_types": "Aged 55 to 59, all employee jobs with a pension", + "55_to_59__defined_benefit": "Aged 55 to 59, defined benefit", + "55_to_59__defined_contribution": "Aged 55 to 59, defined contribution", + "55_to_59__group_personal_pension": "Aged 55 to 59, group personal pension", + "55_to_59__group_self_invested_personal_pension": "Aged 55 to 59, group self-invested personal pension", + "55_to_59__group_stakeholder_pension": "Aged 55 to 59, group stakeholder pension", + "60_to_64__all_pension_types": "Aged 60 to 64, all employee jobs with a pension", + "60_to_64__defined_benefit": "Aged 60 to 64, defined benefit", + "60_to_64__defined_contribution": "Aged 60 to 64, defined contribution", + "60_to_64__group_personal_pension": "Aged 60 to 64, group personal pension", + "60_to_64__group_self_invested_personal_pension": "Aged 60 to 64, group self-invested personal pension", + "60_to_64__group_stakeholder_pension": "Aged 60 to 64, group stakeholder pension", + "65_and_over__all_pension_types": "Aged 65 and over, all employee jobs with a pension", + "65_and_over__defined_benefit": "Aged 65 and over, defined benefit", + "65_and_over__defined_contribution": "Aged 65 and over, defined contribution", + "65_and_over__group_personal_pension": "Aged 65 and over, group personal pension", + "65_and_over__group_self_invested_personal_pension": "Aged 65 and over, group self-invested personal pension", + "65_and_over__group_stakeholder_pension": "Aged 65 and over, group stakeholder pension", + "all_ages__all_pension_types": "All ages, all employee jobs with a pension", + "all_ages__defined_benefit": "All ages, defined benefit", + "all_ages__defined_contribution": "All ages, defined contribution", + "all_ages__group_personal_pension": "All ages, group personal pension", + "all_ages__group_self_invested_personal_pension": "All ages, group self-invested personal pension", + "all_ages__group_stakeholder_pension": "All ages, group stakeholder pension" + }, + "ons.ashe_business_size_pension_type": { + "1000_or_more_employees__all_pension_types": "1000 or more employees, all employee jobs with a pension", + "1000_or_more_employees__defined_benefit": "1000 or more employees, defined benefit", + "1000_or_more_employees__defined_contribution": "1000 or more employees, defined contribution", + "1000_or_more_employees__group_personal_pension": "1000 or more employees, group personal pension", + "1000_or_more_employees__group_self_invested_personal_pension": "1000 or more employees, group self-invested personal pension", + "1000_or_more_employees__group_stakeholder_pension": "1000 or more employees, group stakeholder pension", + "100_to_499_employees__all_pension_types": "100 to 499 employees, all employee jobs with a pension", + "100_to_499_employees__defined_benefit": "100 to 499 employees, defined benefit", + "100_to_499_employees__defined_contribution": "100 to 499 employees, defined contribution", + "100_to_499_employees__group_personal_pension": "100 to 499 employees, group personal pension", + "100_to_499_employees__group_self_invested_personal_pension": "100 to 499 employees, group self-invested personal pension", + "100_to_499_employees__group_stakeholder_pension": "100 to 499 employees, group stakeholder pension", + "13_to_99_employees__all_pension_types": "13 to 99 employees, all employee jobs with a pension", + "13_to_99_employees__defined_benefit": "13 to 99 employees, defined benefit", + "13_to_99_employees__defined_contribution": "13 to 99 employees, defined contribution", + "13_to_99_employees__group_personal_pension": "13 to 99 employees, group personal pension", + "13_to_99_employees__group_self_invested_personal_pension": "13 to 99 employees, group self-invested personal pension", + "13_to_99_employees__group_stakeholder_pension": "13 to 99 employees, group stakeholder pension", + "1_to_12_employees__all_pension_types": "1 to 12 employees, all employee jobs with a pension", + "1_to_12_employees__defined_benefit": "1 to 12 employees, defined benefit", + "1_to_12_employees__defined_contribution": "1 to 12 employees, defined contribution", + "1_to_12_employees__group_personal_pension": "1 to 12 employees, group personal pension", + "1_to_12_employees__group_self_invested_personal_pension": "1 to 12 employees, group self-invested personal pension", + "1_to_12_employees__group_stakeholder_pension": "1 to 12 employees, group stakeholder pension", + "500_to_999_employees__all_pension_types": "500 to 999 employees, all employee jobs with a pension", + "500_to_999_employees__defined_benefit": "500 to 999 employees, defined benefit", + "500_to_999_employees__defined_contribution": "500 to 999 employees, defined contribution", + "500_to_999_employees__group_personal_pension": "500 to 999 employees, group personal pension", + "500_to_999_employees__group_self_invested_personal_pension": "500 to 999 employees, group self-invested personal pension", + "500_to_999_employees__group_stakeholder_pension": "500 to 999 employees, group stakeholder pension" + }, + "ons.ashe_industry_pension_type": { + "index_of_production__all_pension_types": "All Index of Production industries (SIC 2007 sections B to E), all employee jobs with a pension", + "index_of_production__defined_benefit": "All Index of Production industries (SIC 2007 sections B to E), defined benefit", + "index_of_production__defined_contribution": "All Index of Production industries (SIC 2007 sections B to E), defined contribution", + "index_of_production__group_personal_pension": "All Index of Production industries (SIC 2007 sections B to E), group personal pension", + "index_of_production__group_self_invested_personal_pension": "All Index of Production industries (SIC 2007 sections B to E), group self-invested personal pension", + "index_of_production__group_stakeholder_pension": "All Index of Production industries (SIC 2007 sections B to E), group stakeholder pension", + "private_sector__all_pension_types": "Private sector, all employee jobs with a pension", + "private_sector__defined_benefit": "Private sector, defined benefit", + "private_sector__defined_contribution": "Private sector, defined contribution", + "private_sector__group_personal_pension": "Private sector, group personal pension", + "private_sector__group_self_invested_personal_pension": "Private sector, group self-invested personal pension", + "private_sector__group_stakeholder_pension": "Private sector, group stakeholder pension", + "public_sector__all_pension_types": "Public sector, all employee jobs with a pension", + "public_sector__defined_benefit": "Public sector, defined benefit", + "public_sector__defined_contribution": "Public sector, defined contribution", + "public_sector__group_personal_pension": "Public sector, group personal pension", + "public_sector__group_stakeholder_pension": "Public sector, group stakeholder pension", + "service_industries__all_pension_types": "All service industries (SIC 2007 sections G to U), all employee jobs with a pension", + "service_industries__defined_benefit": "All service industries (SIC 2007 sections G to U), defined benefit", + "service_industries__defined_contribution": "All service industries (SIC 2007 sections G to U), defined contribution", + "service_industries__group_personal_pension": "All service industries (SIC 2007 sections G to U), group personal pension", + "service_industries__group_self_invested_personal_pension": "All service industries (SIC 2007 sections G to U), group self-invested personal pension", + "service_industries__group_stakeholder_pension": "All service industries (SIC 2007 sections G to U), group stakeholder pension", + "sic_a__all_pension_types": "Agriculture, Forestry and Fishing, all employee jobs with a pension", + "sic_a__defined_benefit": "Agriculture, Forestry and Fishing, defined benefit", + "sic_a__defined_contribution": "Agriculture, Forestry and Fishing, defined contribution", + "sic_a__group_personal_pension": "Agriculture, Forestry and Fishing, group personal pension", + "sic_a__group_self_invested_personal_pension": "Agriculture, Forestry and Fishing, group self-invested personal pension", + "sic_b__all_pension_types": "Mining and Quarrying, all employee jobs with a pension", + "sic_b__defined_contribution": "Mining and Quarrying, defined contribution", + "sic_b__group_personal_pension": "Mining and Quarrying, group personal pension", + "sic_c__all_pension_types": "Manufacturing, all employee jobs with a pension", + "sic_c__defined_benefit": "Manufacturing, defined benefit", + "sic_c__defined_contribution": "Manufacturing, defined contribution", + "sic_c__group_personal_pension": "Manufacturing, group personal pension", + "sic_c__group_self_invested_personal_pension": "Manufacturing, group self-invested personal pension", + "sic_c__group_stakeholder_pension": "Manufacturing, group stakeholder pension", + "sic_d__all_pension_types": "Electricity, Gas, Steam and Air Conditioning Supply, all employee jobs with a pension", + "sic_d__defined_benefit": "Electricity, Gas, Steam and Air Conditioning Supply, defined benefit", + "sic_d__defined_contribution": "Electricity, Gas, Steam and Air Conditioning Supply, defined contribution", + "sic_d__group_personal_pension": "Electricity, Gas, Steam and Air Conditioning Supply, group personal pension", + "sic_d__group_stakeholder_pension": "Electricity, Gas, Steam and Air Conditioning Supply, group stakeholder pension", + "sic_e__all_pension_types": "Water supply; sewerage, waste management and remediation activities, all employee jobs with a pension", + "sic_e__defined_benefit": "Water supply; sewerage, waste management and remediation activities, defined benefit", + "sic_e__defined_contribution": "Water supply; sewerage, waste management and remediation activities, defined contribution", + "sic_e__group_personal_pension": "Water supply; sewerage, waste management and remediation activities, group personal pension", + "sic_e__group_stakeholder_pension": "Water supply; sewerage, waste management and remediation activities, group stakeholder pension", + "sic_f__all_pension_types": "Construction, all employee jobs with a pension", + "sic_f__defined_benefit": "Construction, defined benefit", + "sic_f__defined_contribution": "Construction, defined contribution", + "sic_f__group_personal_pension": "Construction, group personal pension", + "sic_f__group_self_invested_personal_pension": "Construction, group self-invested personal pension", + "sic_f__group_stakeholder_pension": "Construction, group stakeholder pension", + "sic_g__all_pension_types": "Wholesale and retail trade; repair of motor vehicles and motorcycles, all employee jobs with a pension", + "sic_g__defined_benefit": "Wholesale and retail trade; repair of motor vehicles and motorcycles, defined benefit", + "sic_g__defined_contribution": "Wholesale and retail trade; repair of motor vehicles and motorcycles, defined contribution", + "sic_g__group_personal_pension": "Wholesale and retail trade; repair of motor vehicles and motorcycles, group personal pension", + "sic_g__group_self_invested_personal_pension": "Wholesale and retail trade; repair of motor vehicles and motorcycles, group self-invested personal pension", + "sic_g__group_stakeholder_pension": "Wholesale and retail trade; repair of motor vehicles and motorcycles, group stakeholder pension", + "sic_h__all_pension_types": "Transportation and storage, all employee jobs with a pension", + "sic_h__defined_benefit": "Transportation and storage, defined benefit", + "sic_h__defined_contribution": "Transportation and storage, defined contribution", + "sic_h__group_personal_pension": "Transportation and storage, group personal pension", + "sic_h__group_self_invested_personal_pension": "Transportation and storage, group self-invested personal pension", + "sic_h__group_stakeholder_pension": "Transportation and storage, group stakeholder pension", + "sic_i__all_pension_types": "Accommodation and food service activities, all employee jobs with a pension", + "sic_i__defined_benefit": "Accommodation and food service activities, defined benefit", + "sic_i__defined_contribution": "Accommodation and food service activities, defined contribution", + "sic_i__group_personal_pension": "Accommodation and food service activities, group personal pension", + "sic_i__group_self_invested_personal_pension": "Accommodation and food service activities, group self-invested personal pension", + "sic_i__group_stakeholder_pension": "Accommodation and food service activities, group stakeholder pension", + "sic_j__all_pension_types": "Information and communication, all employee jobs with a pension", + "sic_j__defined_benefit": "Information and communication, defined benefit", + "sic_j__defined_contribution": "Information and communication, defined contribution", + "sic_j__group_personal_pension": "Information and communication, group personal pension", + "sic_j__group_self_invested_personal_pension": "Information and communication, group self-invested personal pension", + "sic_j__group_stakeholder_pension": "Information and communication, group stakeholder pension", + "sic_k__all_pension_types": "Financial and insurance activities, all employee jobs with a pension", + "sic_k__defined_benefit": "Financial and insurance activities, defined benefit", + "sic_k__defined_contribution": "Financial and insurance activities, defined contribution", + "sic_k__group_personal_pension": "Financial and insurance activities, group personal pension", + "sic_k__group_self_invested_personal_pension": "Financial and insurance activities, group self-invested personal pension", + "sic_k__group_stakeholder_pension": "Financial and insurance activities, group stakeholder pension", + "sic_l__all_pension_types": "Real estate activities, all employee jobs with a pension", + "sic_l__defined_benefit": "Real estate activities, defined benefit", + "sic_l__defined_contribution": "Real estate activities, defined contribution", + "sic_l__group_personal_pension": "Real estate activities, group personal pension", + "sic_l__group_self_invested_personal_pension": "Real estate activities, group self-invested personal pension", + "sic_l__group_stakeholder_pension": "Real estate activities, group stakeholder pension", + "sic_m__all_pension_types": "Professional, scientific and technical activities, all employee jobs with a pension", + "sic_m__defined_benefit": "Professional, scientific and technical activities, defined benefit", + "sic_m__defined_contribution": "Professional, scientific and technical activities, defined contribution", + "sic_m__group_personal_pension": "Professional, scientific and technical activities, group personal pension", + "sic_m__group_self_invested_personal_pension": "Professional, scientific and technical activities, group self-invested personal pension", + "sic_m__group_stakeholder_pension": "Professional, scientific and technical activities, group stakeholder pension", + "sic_n__all_pension_types": "Administrative and support service activities, all employee jobs with a pension", + "sic_n__defined_benefit": "Administrative and support service activities, defined benefit", + "sic_n__defined_contribution": "Administrative and support service activities, defined contribution", + "sic_n__group_personal_pension": "Administrative and support service activities, group personal pension", + "sic_n__group_self_invested_personal_pension": "Administrative and support service activities, group self-invested personal pension", + "sic_n__group_stakeholder_pension": "Administrative and support service activities, group stakeholder pension", + "sic_o__all_pension_types": "Public administration and defence; compulsory social security, all employee jobs with a pension", + "sic_o__defined_benefit": "Public administration and defence; compulsory social security, defined benefit", + "sic_o__defined_contribution": "Public administration and defence; compulsory social security, defined contribution", + "sic_o__group_personal_pension": "Public administration and defence; compulsory social security, group personal pension", + "sic_o__group_stakeholder_pension": "Public administration and defence; compulsory social security, group stakeholder pension", + "sic_p__all_pension_types": "Education, all employee jobs with a pension", + "sic_p__defined_benefit": "Education, defined benefit", + "sic_p__defined_contribution": "Education, defined contribution", + "sic_p__group_personal_pension": "Education, group personal pension", + "sic_p__group_stakeholder_pension": "Education, group stakeholder pension", + "sic_q__all_pension_types": "Human health and social work activities, all employee jobs with a pension", + "sic_q__defined_benefit": "Human health and social work activities, defined benefit", + "sic_q__defined_contribution": "Human health and social work activities, defined contribution", + "sic_q__group_personal_pension": "Human health and social work activities, group personal pension", + "sic_q__group_self_invested_personal_pension": "Human health and social work activities, group self-invested personal pension", + "sic_q__group_stakeholder_pension": "Human health and social work activities, group stakeholder pension", + "sic_r__all_pension_types": "Arts, entertainment and recreation, all employee jobs with a pension", + "sic_r__defined_benefit": "Arts, entertainment and recreation, defined benefit", + "sic_r__defined_contribution": "Arts, entertainment and recreation, defined contribution", + "sic_r__group_personal_pension": "Arts, entertainment and recreation, group personal pension", + "sic_r__group_stakeholder_pension": "Arts, entertainment and recreation, group stakeholder pension", + "sic_s__all_pension_types": "Other service activities, all employee jobs with a pension", + "sic_s__defined_benefit": "Other service activities, defined benefit", + "sic_s__defined_contribution": "Other service activities, defined contribution", + "sic_s__group_personal_pension": "Other service activities, group personal pension", + "sic_s__group_self_invested_personal_pension": "Other service activities, group self-invested personal pension", + "sic_s__group_stakeholder_pension": "Other service activities, group stakeholder pension" + }, + "ons.ashe_occupation_pension_type": { + "soc_1__all_pension_types": "Managers, directors and senior officials, all employee jobs with a pension", + "soc_1__defined_benefit": "Managers, directors and senior officials, defined benefit", + "soc_1__defined_contribution": "Managers, directors and senior officials, defined contribution", + "soc_1__group_personal_pension": "Managers, directors and senior officials, group personal pension", + "soc_1__group_self_invested_personal_pension": "Managers, directors and senior officials, group self-invested personal pension", + "soc_1__group_stakeholder_pension": "Managers, directors and senior officials, group stakeholder pension", + "soc_2__all_pension_types": "Professional occupations, all employee jobs with a pension", + "soc_2__defined_benefit": "Professional occupations, defined benefit", + "soc_2__defined_contribution": "Professional occupations, defined contribution", + "soc_2__group_personal_pension": "Professional occupations, group personal pension", + "soc_2__group_self_invested_personal_pension": "Professional occupations, group self-invested personal pension", + "soc_2__group_stakeholder_pension": "Professional occupations, group stakeholder pension", + "soc_3__all_pension_types": "Associate professional and technical occupations, all employee jobs with a pension", + "soc_3__defined_benefit": "Associate professional and technical occupations, defined benefit", + "soc_3__defined_contribution": "Associate professional and technical occupations, defined contribution", + "soc_3__group_personal_pension": "Associate professional and technical occupations, group personal pension", + "soc_3__group_self_invested_personal_pension": "Associate professional and technical occupations, group self-invested personal pension", + "soc_3__group_stakeholder_pension": "Associate professional and technical occupations, group stakeholder pension", + "soc_4__all_pension_types": "Administrative and secretarial occupations, all employee jobs with a pension", + "soc_4__defined_benefit": "Administrative and secretarial occupations, defined benefit", + "soc_4__defined_contribution": "Administrative and secretarial occupations, defined contribution", + "soc_4__group_personal_pension": "Administrative and secretarial occupations, group personal pension", + "soc_4__group_self_invested_personal_pension": "Administrative and secretarial occupations, group self-invested personal pension", + "soc_4__group_stakeholder_pension": "Administrative and secretarial occupations, group stakeholder pension", + "soc_5__all_pension_types": "Skilled trades occupations, all employee jobs with a pension", + "soc_5__defined_benefit": "Skilled trades occupations, defined benefit", + "soc_5__defined_contribution": "Skilled trades occupations, defined contribution", + "soc_5__group_personal_pension": "Skilled trades occupations, group personal pension", + "soc_5__group_self_invested_personal_pension": "Skilled trades occupations, group self-invested personal pension", + "soc_5__group_stakeholder_pension": "Skilled trades occupations, group stakeholder pension", + "soc_6__all_pension_types": "Caring, leisure and other service occupations, all employee jobs with a pension", + "soc_6__defined_benefit": "Caring, leisure and other service occupations, defined benefit", + "soc_6__defined_contribution": "Caring, leisure and other service occupations, defined contribution", + "soc_6__group_personal_pension": "Caring, leisure and other service occupations, group personal pension", + "soc_6__group_self_invested_personal_pension": "Caring, leisure and other service occupations, group self-invested personal pension", + "soc_6__group_stakeholder_pension": "Caring, leisure and other service occupations, group stakeholder pension", + "soc_7__all_pension_types": "Sales and customer service occupations, all employee jobs with a pension", + "soc_7__defined_benefit": "Sales and customer service occupations, defined benefit", + "soc_7__defined_contribution": "Sales and customer service occupations, defined contribution", + "soc_7__group_personal_pension": "Sales and customer service occupations, group personal pension", + "soc_7__group_self_invested_personal_pension": "Sales and customer service occupations, group self-invested personal pension", + "soc_7__group_stakeholder_pension": "Sales and customer service occupations, group stakeholder pension", + "soc_8__all_pension_types": "Process, plant and machine operatives, all employee jobs with a pension", + "soc_8__defined_benefit": "Process, plant and machine operatives, defined benefit", + "soc_8__defined_contribution": "Process, plant and machine operatives, defined contribution", + "soc_8__group_personal_pension": "Process, plant and machine operatives, group personal pension", + "soc_8__group_self_invested_personal_pension": "Process, plant and machine operatives, group self-invested personal pension", + "soc_8__group_stakeholder_pension": "Process, plant and machine operatives, group stakeholder pension", + "soc_9__all_pension_types": "Elementary occupations, all employee jobs with a pension", + "soc_9__defined_benefit": "Elementary occupations, defined benefit", + "soc_9__defined_contribution": "Elementary occupations, defined contribution", + "soc_9__group_personal_pension": "Elementary occupations, group personal pension", + "soc_9__group_self_invested_personal_pension": "Elementary occupations, group self-invested personal pension", + "soc_9__group_stakeholder_pension": "Elementary occupations, group stakeholder pension" + }, + "ons.coicop": { + "coicop_01": "Food and non-alcoholic beverages", + "coicop_02": "Alcoholic beverages and tobacco", + "coicop_04": "Housing, water, electricity, gas and other fuel", + "coicop_05": "Furnishings, household equipment and routine maintenance of the house", + "coicop_11": "Restaurants and hotels", + "coicop_12": "Miscellaneous goods and services" + }, + "ons.household_type": { + "couple_3_plus_children_households": "3 or more dependent children" + }, + "partner_indicator": { + "Has a partner": "Has a partner", + "Has no partner": "Has no partner", + "all": "Total" + }, + "payment_indicator": { + "No": "No", + "Yes": "Yes", + "all": "Total" + }, + "payment_method": { + "all": "All payment methods", + "direct_debit": "Direct debit", + "prepayment": "Prepayment", + "standard_credit": "Standard credit" + }, + "pension_type": { + "all": "All employee jobs", + "defined_benefit": "Defined benefit", + "defined_contribution": "Defined contribution", + "group_personal_pension": "Group personal pension", + "group_self_invested_personal_pension": "Group self-invested personal pension", + "group_stakeholder_pension": "Group stakeholder pension", + "no_pension_provision": "No pension provision", + "unknown_pension_provision": "Unknown pension provision" + }, + "person.age_band": { + "0_17": "0_17", + "18_64": "18_64", + "65_plus": "65+" + }, + "person.sex": { + "female": "female", + "male": "male" + }, + "population_type": { + "UR_CE": "All usual residents in communal establishments", + "UR_HH": "All usual residents in households" + }, + "price_basis": { + "average_2024_25_prices": "Average 2024/25 prices" + }, + "price_component": { + "annual_bill": "Average annual bill", + "fixed_cost": "Average fixed cost", + "variable_unit_cost": "Average variable unit cost" + }, + "private_or_social_rented": { + "Private Rented Sector": "Private Rented Sector", + "Social Rented Sector": "Social Rented Sector", + "Unknown or missing housing sector": "Unknown or missing housing sector", + "all": "Total" + }, + "program": { + "liheap": "LIHEAP", + "state_programs": "State programs" + }, + "property_type": { + "all_dwellings": "All dwellings", + "all_flats": "All flats", + "all_houses": "All houses", + "bungalow": "Bungalow", + "converted_flat": "Converted flat", + "detached": "Detached", + "end_terrace": "End terrace", + "mid_terrace": "Mid terrace", + "purpose_built_flat": "Purpose built flat", + "semi_detached": "Semi detached", + "terraced": "Terraced" + }, + "publication_status": { + "provisional": "Provisional" + }, + "quant_inc": { + "d1": "First income decile", + "d2": "Second income decile", + "d3": "Third income decile", + "d4": "Fourth income decile", + "d5": "Fifth income decile", + "d6": "Sixth income decile", + "d7": "Seventh income decile", + "d8": "Eighth income decile", + "d9": "Ninth income decile" + }, + "quarter": { + "Q1": "Q1" + }, + "region": { + "all_dwellings": "All dwellings", + "east_midlands": "East Midlands", + "east_of_england": "East of England", + "london": "London", + "north_east": "North East", + "north_west": "North West", + "south_east": "South East", + "south_west": "South West", + "wales": "Wales", + "west_midlands": "West Midlands", + "yorkshire_and_the_humber": "Yorkshire and The Humber" + }, + "residence_type": { + "Lives in a communal establishment": "Lives in a communal establishment" + }, + "resident_age_101a": { + "Aged 1 year": "Aged 1 year", + "Aged 10 years": "Aged 10 years", + "Aged 100 years and over": "Aged 100 years and over", + "Aged 11 years": "Aged 11 years", + "Aged 12 years": "Aged 12 years", + "Aged 13 years": "Aged 13 years", + "Aged 14 years": "Aged 14 years", + "Aged 15 years": "Aged 15 years", + "Aged 16 years": "Aged 16 years", + "Aged 17 years": "Aged 17 years", + "Aged 18 years": "Aged 18 years", + "Aged 19 years": "Aged 19 years", + "Aged 2 years": "Aged 2 years", + "Aged 20 years": "Aged 20 years", + "Aged 21 years": "Aged 21 years", + "Aged 22 years": "Aged 22 years", + "Aged 23 years": "Aged 23 years", + "Aged 24 years": "Aged 24 years", + "Aged 25 years": "Aged 25 years", + "Aged 26 years": "Aged 26 years", + "Aged 27 years": "Aged 27 years", + "Aged 28 years": "Aged 28 years", + "Aged 29 years": "Aged 29 years", + "Aged 3 years": "Aged 3 years", + "Aged 30 years": "Aged 30 years", + "Aged 31 years": "Aged 31 years", + "Aged 32 years": "Aged 32 years", + "Aged 33 years": "Aged 33 years", + "Aged 34 years": "Aged 34 years", + "Aged 35 years": "Aged 35 years", + "Aged 36 years": "Aged 36 years", + "Aged 37 years": "Aged 37 years", + "Aged 38 years": "Aged 38 years", + "Aged 39 years": "Aged 39 years", + "Aged 4 years": "Aged 4 years", + "Aged 40 years": "Aged 40 years", + "Aged 41 years": "Aged 41 years", + "Aged 42 years": "Aged 42 years", + "Aged 43 years": "Aged 43 years", + "Aged 44 years": "Aged 44 years", + "Aged 45 years": "Aged 45 years", + "Aged 46 years": "Aged 46 years", + "Aged 47 years": "Aged 47 years", + "Aged 48 years": "Aged 48 years", + "Aged 49 years": "Aged 49 years", + "Aged 5 years": "Aged 5 years", + "Aged 50 years": "Aged 50 years", + "Aged 51 years": "Aged 51 years", + "Aged 52 years": "Aged 52 years", + "Aged 53 years": "Aged 53 years", + "Aged 54 years": "Aged 54 years", + "Aged 55 years": "Aged 55 years", + "Aged 56 years": "Aged 56 years", + "Aged 57 years": "Aged 57 years", + "Aged 58 years": "Aged 58 years", + "Aged 59 years": "Aged 59 years", + "Aged 6 years": "Aged 6 years", + "Aged 60 years": "Aged 60 years", + "Aged 61 years": "Aged 61 years", + "Aged 62 years": "Aged 62 years", + "Aged 63 years": "Aged 63 years", + "Aged 64 years": "Aged 64 years", + "Aged 65 years": "Aged 65 years", + "Aged 66 years": "Aged 66 years", + "Aged 67 years": "Aged 67 years", + "Aged 68 years": "Aged 68 years", + "Aged 69 years": "Aged 69 years", + "Aged 7 years": "Aged 7 years", + "Aged 70 years": "Aged 70 years", + "Aged 71 years": "Aged 71 years", + "Aged 72 years": "Aged 72 years", + "Aged 73 years": "Aged 73 years", + "Aged 74 years": "Aged 74 years", + "Aged 75 years": "Aged 75 years", + "Aged 76 years": "Aged 76 years", + "Aged 77 years": "Aged 77 years", + "Aged 78 years": "Aged 78 years", + "Aged 79 years": "Aged 79 years", + "Aged 8 years": "Aged 8 years", + "Aged 80 years": "Aged 80 years", + "Aged 81 years": "Aged 81 years", + "Aged 82 years": "Aged 82 years", + "Aged 83 years": "Aged 83 years", + "Aged 84 years": "Aged 84 years", + "Aged 85 years": "Aged 85 years", + "Aged 86 years": "Aged 86 years", + "Aged 87 years": "Aged 87 years", + "Aged 88 years": "Aged 88 years", + "Aged 89 years": "Aged 89 years", + "Aged 9 years": "Aged 9 years", + "Aged 90 years": "Aged 90 years", + "Aged 91 years": "Aged 91 years", + "Aged 92 years": "Aged 92 years", + "Aged 93 years": "Aged 93 years", + "Aged 94 years": "Aged 94 years", + "Aged 95 years": "Aged 95 years", + "Aged 96 years": "Aged 96 years", + "Aged 97 years": "Aged 97 years", + "Aged 98 years": "Aged 98 years", + "Aged 99 years": "Aged 99 years", + "Aged under 1 year": "Aged under 1 year", + "age_0": "Aged under 1 year", + "age_1": "Aged 1 year", + "age_10": "Aged 10 years", + "age_100": "Aged 100 years and over", + "age_11": "Aged 11 years", + "age_12": "Aged 12 years", + "age_13": "Aged 13 years", + "age_14": "Aged 14 years", + "age_15": "Aged 15 years", + "age_16": "Aged 16 years", + "age_17": "Aged 17 years", + "age_18": "Aged 18 years", + "age_19": "Aged 19 years", + "age_2": "Aged 2 years", + "age_20": "Aged 20 years", + "age_21": "Aged 21 years", + "age_22": "Aged 22 years", + "age_23": "Aged 23 years", + "age_24": "Aged 24 years", + "age_25": "Aged 25 years", + "age_26": "Aged 26 years", + "age_27": "Aged 27 years", + "age_28": "Aged 28 years", + "age_29": "Aged 29 years", + "age_3": "Aged 3 years", + "age_30": "Aged 30 years", + "age_31": "Aged 31 years", + "age_32": "Aged 32 years", + "age_33": "Aged 33 years", + "age_34": "Aged 34 years", + "age_35": "Aged 35 years", + "age_36": "Aged 36 years", + "age_37": "Aged 37 years", + "age_38": "Aged 38 years", + "age_39": "Aged 39 years", + "age_4": "Aged 4 years", + "age_40": "Aged 40 years", + "age_41": "Aged 41 years", + "age_42": "Aged 42 years", + "age_43": "Aged 43 years", + "age_44": "Aged 44 years", + "age_45": "Aged 45 years", + "age_46": "Aged 46 years", + "age_47": "Aged 47 years", + "age_48": "Aged 48 years", + "age_49": "Aged 49 years", + "age_5": "Aged 5 years", + "age_50": "Aged 50 years", + "age_51": "Aged 51 years", + "age_52": "Aged 52 years", + "age_53": "Aged 53 years", + "age_54": "Aged 54 years", + "age_55": "Aged 55 years", + "age_56": "Aged 56 years", + "age_57": "Aged 57 years", + "age_58": "Aged 58 years", + "age_59": "Aged 59 years", + "age_6": "Aged 6 years", + "age_60": "Aged 60 years", + "age_61": "Aged 61 years", + "age_62": "Aged 62 years", + "age_63": "Aged 63 years", + "age_64": "Aged 64 years", + "age_65": "Aged 65 years", + "age_66": "Aged 66 years", + "age_67": "Aged 67 years", + "age_68": "Aged 68 years", + "age_69": "Aged 69 years", + "age_7": "Aged 7 years", + "age_70": "Aged 70 years", + "age_71": "Aged 71 years", + "age_72": "Aged 72 years", + "age_73": "Aged 73 years", + "age_74": "Aged 74 years", + "age_75": "Aged 75 years", + "age_76": "Aged 76 years", + "age_77": "Aged 77 years", + "age_78": "Aged 78 years", + "age_79": "Aged 79 years", + "age_8": "Aged 8 years", + "age_80": "Aged 80 years", + "age_81": "Aged 81 years", + "age_82": "Aged 82 years", + "age_83": "Aged 83 years", + "age_84": "Aged 84 years", + "age_85": "Aged 85 years", + "age_86": "Aged 86 years", + "age_87": "Aged 87 years", + "age_88": "Aged 88 years", + "age_89": "Aged 89 years", + "age_9": "Aged 9 years", + "age_90": "Aged 90 years", + "age_91": "Aged 91 years", + "age_92": "Aged 92 years", + "age_93": "Aged 93 years", + "age_94": "Aged 94 years", + "age_95": "Aged 95 years", + "age_96": "Aged 96 years", + "age_97": "Aged 97 years", + "age_98": "Aged 98 years", + "age_99": "Aged 99 years" + }, + "scheme_type": { + "Defined benefit": "Defined benefit", + "Defined contribution": "Defined contribution", + "all": "Total" + }, + "school_membership": { + "isc": "ISC member schools" + }, + "scotgov.bus_scope": { + "scotland": "Scotland" + }, + "scotgov.funding_component": { + "all_government_support": "All government support", + "concessionary_fares": "Concessionary fares", + "local_authority_bus_support": "Local authority bus support", + "network_support_grant": "Network Support Grant" + }, + "scotgov.geography_row": { + "scotland": "Scotland" + }, + "scotgov.journey_category": { + "all_passenger_journeys": "All passenger journeys", + "concessionary_passenger_journeys": "Concessionary passenger journeys" + }, + "sector": { + "Private": "Private sector", + "Public": "Public sector", + "S13": "S13", + "all": "All sectors" + }, + "sector_scheme": { + "Personal or private sector occupational scheme": "Personal or private sector occupational scheme", + "Public sector occupational scheme": "Public sector occupational scheme", + "all": "Total" + }, + "service": { + "metro_glider": "Metro / Glider" + }, + "sex": { + "Female": "Female", + "Male": "Male", + "all": "All", + "female": "Female", + "male": "Male" + }, + "sic2007_section": { + "A": "Agriculture, Forestry and Fishing", + "B": "Mining and Quarrying", + "C": "Manufacturing", + "D": "Electricity, Gas, Steam and Air Conditioning Supply", + "E": "Water supply; sewerage, waste management and remediation activities", + "F": "Construction", + "G": "Wholesale and retail trade; repair of motor vehicles and motorcycles", + "H": "Transportation and storage", + "I": "Accommodation and food service activities", + "J": "Information and communication", + "K": "Financial and insurance activities", + "L": "Real estate activities", + "M": "Professional, scientific and technical activities", + "N": "Administrative and support service activities", + "O": "Public administration and defence; compulsory social security", + "P": "Education", + "Q": "Human health and social work activities", + "R": "Arts, entertainment and recreation", + "S": "Other service activities", + "all": "All industries" + }, + "sic2007_summary_category": { + "all": "All industries", + "index_of_production": "All Index of Production industries (SIC 2007 sections B to E)", + "service_industries": "All service industries (SIC 2007 sections G to U)" + }, + "slc.repayment_line": { + "he_repayments": "Higher education borrower repayments" + }, + "slc.repayment_plan": { + "net_repayments": "Net repayments posted during the financial year" + }, + "soc2020_major_group": { + "1": "Managers, directors and senior officials", + "2": "Professional occupations", + "3": "Associate professional and technical occupations", + "4": "Administrative and secretarial occupations", + "5": "Skilled trades occupations", + "6": "Caring, leisure and other service occupations", + "7": "Sales and customer service occupations", + "8": "Process, plant and machine operatives", + "9": "Elementary occupations" + }, + "spdeps": { + "SPR": "SPR" + }, + "spfunc": { + "DIS": "DIS", + "EXCL": "EXCL", + "FAM": "FAM", + "HOU": "HOU", + "OLD": "OLD", + "SICK": "SICK", + "SRV": "SRV", + "TOTAL": "TOTAL", + "UNE": "UNE", + "disability": "Disability", + "family_children": "Family and children", + "housing": "Housing", + "old_age": "Old age", + "sickness_health_care": "Sickness and health care", + "social_exclusion_nec": "Social exclusion not elsewhere classified", + "survivors": "Survivors", + "total": "Total social protection benefits", + "unemployment": "Unemployment" + }, + "statistic": { + "mean": "Mean", + "median": "Median" + }, + "tariff_type": { + "all": "All tariffs", + "fixed": "Fixed tariffs", + "variable": "Variable tariffs" + }, + "tax_rate": { + "Additional Rate": "Additional Rate", + "Basic Rate": "Basic Rate", + "Higher Rate": "Higher Rate", + "Main Rate": "Main Rate", + "all": "Total" + }, + "taxpayer_type": { + "all": "All", + "individuals": "Individuals", + "trusts": "Trusts" + }, + "temperature_adjustment": { + "actual_temperature": "Actual temperature", + "as_reported": "As reported", + "weather_corrected": "Weather corrected" + }, + "tenure": { + "all_dwellings": "All dwellings", + "council_or_housing_association": "Council or Housing Association", + "owner_occupied": "Owner-occupied", + "privately_rented": "Privately rented" + }, + "time": { + "2022": "2022", + "2023": "2023", + "2024": "2024" + }, + "total_income_lower_bound": { + "100000": "100000", + "1000000": "1000000", + "12570": "12570", + "15000": "15000", + "150000": "150000", + "20000": "20000", + "200000": "200000", + "30000": "30000", + "300000": "300000", + "40000": "40000", + "50000": "50000", + "500000": "500000", + "70000": "70000" + }, + "total_income_upper_bound": { + "100000": "100000", + "1000000": "1000000", + "15000": "15000", + "150000": "150000", + "20000": "20000", + "200000": "200000", + "30000": "30000", + "300000": "300000", + "40000": "40000", + "50000": "50000", + "500000": "500000", + "70000": "70000" + }, + "type_of_pension_credit": { + "Both Guarantee and Savings Credit": "Both Guarantee and Savings Credit", + "Guarantee Credit only": "Guarantee Credit only", + "Savings Credit only": "Savings Credit only", + "all": "Total" + }, + "uk.firm.employment_band": { + "0_4": "0_4", + "100_249": "100_249", + "10_19": "10_19", + "20_49": "20_49", + "250_plus": "250_plus", + "50_99": "50_99", + "5_9": "5_9" + }, + "uk.firm.sic_code": { + "00001": "1", + "00002": "2", + "00003": "3", + "00005": "5", + "00006": "6", + "00007": "7", + "00008": "8", + "00009": "9", + "00010": "10", + "00011": "11", + "00012": "12", + "00013": "13", + "00014": "14", + "00015": "15", + "00016": "16", + "00017": "17", + "00018": "18", + "00019": "19", + "00020": "20", + "00021": "21", + "00022": "22", + "00023": "23", + "00024": "24", + "00025": "25", + "00026": "26", + "00027": "27", + "00028": "28", + "00029": "29", + "00030": "30", + "00031": "31", + "00032": "32", + "00033": "33", + "00035": "35", + "00036": "36", + "00037": "37", + "00038": "38", + "00039": "39", + "00041": "41", + "00042": "42", + "00043": "43", + "00045": "45", + "00046": "46", + "00047": "47", + "00049": "49", + "00050": "50", + "00051": "51", + "00052": "52", + "00053": "53", + "00055": "55", + "00056": "56", + "00058": "58", + "00059": "59", + "00060": "60", + "00061": "61", + "00062": "62", + "00063": "63", + "00064": "64", + "00065": "65", + "00066": "66", + "00068": "68", + "00069": "69", + "00070": "70", + "00071": "71", + "00072": "72", + "00073": "73", + "00074": "74", + "00075": "75", + "00077": "77", + "00078": "78", + "00079": "79", + "00080": "80", + "00081": "81", + "00082": "82", + "00084": "84", + "00085": "85", + "00086": "86", + "00087": "87", + "00088": "88", + "00090": "90", + "00091": "91", + "00092": "92", + "00093": "93", + "00094": "94", + "00095": "95", + "00096": "96", + "00097": "97", + "00098": "98", + "00099": "99" + }, + "uk.firm.turnover_band": { + "0_49k": "0_49k", + "1000_4999k": "1000_4999k", + "100_249k": "100_249k", + "250_499k": "250_499k", + "5000k_plus": "5000k_plus", + "500_999k": "500_999k", + "50_99k": "50_99k" + }, + "uk.firm.vat_registered": { + "true": "True" + }, + "unit": { + "MIO_EUR": "MIO_EUR" + }, + "us:statutes/26/62#adjusted_gross_income": { + "100k_to_200k": "$100,000 under $200,000", + "10k_to_11k": "$10,000 under $11,000", + "10k_to_15k": "$10,000 under $15,000", + "10k_to_25k": "$10,000 under $25,000", + "10m_plus": "$10,000,000 or more", + "11k_to_12k": "$11,000 under $12,000", + "12k_to_13k": "$12,000 under $13,000", + "13k_to_14k": "$13,000 under $14,000", + "14k_to_15k": "$14,000 under $15,000", + "15k_to_16k": "$15,000 under $16,000", + "15k_to_20k": "$15,000 under $20,000", + "16k_to_17k": "$16,000 under $17,000", + "17k_to_18k": "$17,000 under $18,000", + "18k_to_19k": "$18,000 under $19,000", + "19k_to_20k": "$19,000 under $20,000", + "1_5m_to_2m": "$1,500,000 under $2,000,000", + "1_to_10k": "$1 under $10,000", + "1_to_1k": "$1 under $1,000", + "1_to_5k": "$1 under $5,000", + "1k_to_2k": "$1,000 under $2,000", + "1m_plus": "$1,000,000 or more", + "1m_to_1_5m": "$1,000,000 under $1,500,000", + "200k_to_500k": "$200,000 under $500,000", + "20k_to_25k": "$20,000 under $25,000", + "25k_to_30k": "$25,000 under $30,000", + "25k_to_50k": "$25,000 under $50,000", + "2k_to_3k": "$2,000 under $3,000", + "2m_to_5m": "$2,000,000 under $5,000,000", + "30k_to_35k": "$30,000 under $35,000", + "30k_to_40k": "$30,000 under $40,000", + "35k_to_40k": "$35,000 under $40,000", + "3k_to_4k": "$3,000 under $4,000", + "40k_to_45k": "$40,000 under $45,000", + "40k_to_50k": "$40,000 under $50,000", + "45k_to_50k": "$45,000 under $50,000", + "4k_to_5k": "$4,000 under $5,000", + "500k_to_1m": "$500,000 under $1,000,000", + "50k_plus": "$50,000 and over", + "50k_to_75k": "$50,000 under $75,000", + "5k_to_10k": "$5,000 under $10,000", + "5k_to_6k": "$5,000 under $6,000", + "5m_to_10m": "$5,000,000 under $10,000,000", + "6k_to_7k": "$6,000 under $7,000", + "75k_to_100k": "$75,000 under $100,000", + "7k_to_8k": "$7,000 under $8,000", + "8k_to_9k": "$8,000 under $9,000", + "9k_to_10k": "$9,000 under $10,000", + "all": "All returns, total", + "total": "Total", + "under_1": "No adjusted gross income" + }, + "usda_snap.state_or_territory": { + "ak": "Alaska", + "al": "Alabama", + "ar": "Arkansas", + "az": "Arizona", + "ca": "California", + "co": "Colorado", + "ct": "Connecticut", + "dc": "District of Columbia", + "de": "Delaware", + "fl": "Florida", + "ga": "Georgia", + "gu": "Guam", + "hi": "Hawaii", + "ia": "Iowa", + "id": "Idaho", + "il": "Illinois", + "in": "Indiana", + "ks": "Kansas", + "ky": "Kentucky", + "la": "Louisiana", + "ma": "Massachusetts", + "md": "Maryland", + "me": "Maine", + "mi": "Michigan", + "mn": "Minnesota", + "mo": "Missouri", + "ms": "Mississippi", + "mt": "Montana", + "nc": "North Carolina", + "nd": "North Dakota", + "ne": "Nebraska", + "nh": "New Hampshire", + "nj": "New Jersey", + "nm": "New Mexico", + "nv": "Nevada", + "ny": "New York", + "oh": "Ohio", + "ok": "Oklahoma", + "or": "Oregon", + "pa": "Pennsylvania", + "ri": "Rhode Island", + "sc": "South Carolina", + "sd": "South Dakota", + "tn": "Tennessee", + "tx": "Texas", + "ut": "Utah", + "va": "Virginia", + "vi": "Virgin Islands", + "vt": "Vermont", + "wa": "Washington", + "wi": "Wisconsin", + "wv": "West Virginia", + "wy": "Wyoming" + }, + "use_frequency_band": { + "3 or more times a week (%)": "3 or more times a week", + "Less than once a year or never (%)": "Less than once a year or never", + "Once or twice a month (%)": "Once or twice a month", + "Once or twice a week (%)": "Once or twice a week", + "Once or twice a year (%)": "Once or twice a year" + }, + "vat_treatment": { + "including_vat": "Including VAT" + }, + "weekly_earnings_band": { + "Less than £100": "Less than £100", + "Over £600": "Over £600", + "all": "All weekly earnings", + "£100 to £200": "£100 to £200", + "£200 to £300": "£200 to £300", + "£300 to £400": "£300 to £400", + "£400 to £500": "£400 to £500", + "£500 to £600": "£500 to £600" + }, + "welshgov.geography_row": { + "wales": "Wales" + } + } +} diff --git a/chronicle/epoch.py b/chronicle/epoch.py index 5db6d259..3662e19e 100644 --- a/chronicle/epoch.py +++ b/chronicle/epoch.py @@ -164,18 +164,21 @@ def key_for_epoch(self, key: str, epoch: Epoch) -> str: # The consumer-fact row contracts Chronicle has emitted, oldest first. Each is a # superset of the one before it, so a row restamped with a later id stays valid: # v2 (chronicle#261) is v1 plus optional dimension and value labels, v3 -# (chronicle#266) is v2 plus the publisher's name for the fact's geography, and -# v4 (chronicle#281) is v3 with that name resolved to one spelling per -# identifier where Chronicle's register carries it, the publisher's own text -# moving to an optional geography.publisher_name. The pair above is the Ledger -# rename table, not this contract history, so only v1 and v2 name a naming -# epoch. The row contract and the hash-key domains move independently: +# (chronicle#266) is v2 plus the publisher's name for the fact's geography, v4 +# (chronicle#281) is v3 with that name resolved to one spelling per identifier +# where Chronicle's register carries it, the publisher's own text moving to an +# optional geography.publisher_name, and v5 (chronicle#376) is v4 with one +# label per dimension value where Chronicle's register lists it, the package's +# own wording moving to an optional publisher_dimension_value_labels. The pair +# above is the Ledger rename table, not this contract history, so only v1 and +# v2 name a naming epoch. The row contract and the hash-key domains move independently: # EMIT_EPOCH still keeps every key Ledger-named. CONSUMER_FACT_ROW_CONTRACTS: tuple[str, ...] = ( SCHEMA_IDS["consumer_fact"].ledger, SCHEMA_IDS["consumer_fact"].chronicle, "chronicle.consumer_fact.v3", "chronicle.consumer_fact.v4", + "chronicle.consumer_fact.v5", ) CONSUMER_FACT_EMIT_SCHEMA_VERSION = CONSUMER_FACT_ROW_CONTRACTS[-1] diff --git a/chronicle/fixtures/consumer_facts.jsonl b/chronicle/fixtures/consumer_facts.jsonl index 315cb9dd..d5900b3f 100644 --- a/chronicle/fixtures/consumer_facts.jsonl +++ b/chronicle/fixtures/consumer_facts.jsonl @@ -1,3 +1,3 @@ -{"aggregate_fact_key": "ledger.aggregate_fact.v2:13153fb8dfd73fa12d5715e9", "aggregation": {"method": "sum"}, "assertion": "observation", "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year count irs soi individual income tax returns for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns", "measure_id": "return_count", "measure_label": "Number of returns", "measure_ordinal": 0, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "return_count", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:079f0697e3b090abf422781b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.return_count", "source_cell_keys": ["ledger.source_cell.v1:82f39b2a5aeab53c1b717928"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.individual_income_tax_returns", "source_measure_id": "return_count", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "count"}, "observed_measure_key": "ledger.observed_measure.v2:bc96277bae6ef31da4a5794c", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "schema_version": "chronicle.consumer_fact.v4", "semantic_fact_key": "ledger.semantic_fact.v2:1ae4c4f93f554ec7ca14d2ad", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 160602107, "value_type": "integer"} -{"aggregate_fact_key": "ledger.aggregate_fact.v2:36ce90001adb50e8e9758e4f", "aggregation": {"method": "sum"}, "assertion": "observation", "concept_alignment": {"authority": "ledger-us", "canonical_concept": "us:statutes/26/62#adjusted_gross_income", "concept_alignment_key": "ledger.concept_alignment.v2:2e0ac323f1b261710e4397a1", "evidence_notes": "IRS SOI Table 1.1 reports adjusted gross income for individual income tax returns; IRC section 62 defines adjusted gross income. This Ledger assertion treats the SOI AGI column as exactly adopting that legal concept for the tax-year source record.", "evidence_url": "https://uscode.house.gov/view.xhtml?req=(title:26%20section:62%20edition:prelim)", "legal_vintage": "tax_year_2023", "relation": "exact", "source_concept": "irs_soi.adjusted_gross_income"}, "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year sum us:statutes/26/62#adjusted gross income for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns", "measure_id": "adjusted_gross_income", "measure_label": "Adjusted gross income", "measure_ordinal": 1, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "adjusted_gross_income", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:7f8e9d3c64fec2670b571c3b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.adjusted_gross_income", "source_cell_keys": ["ledger.source_cell.v1:c3d349e26856fb6224303675"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.adjusted_gross_income", "source_measure_id": "adjusted_gross_income", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "usd"}, "observed_measure_key": "ledger.observed_measure.v2:ac9c3cef252ef7ead0066cb1", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "schema_version": "chronicle.consumer_fact.v4", "semantic_fact_key": "ledger.semantic_fact.v2:b83082b8e69c1ddc6fdebed2", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 15286017359000, "value_type": "integer"} -{"aggregate_fact_key": "ledger.aggregate_fact.v2:0fa0e8c17cfe0ebd844c4f95", "aggregation": {"method": "sum"}, "assertion": "observation", "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year sum irs soi total income tax for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns", "measure_id": "total_income_tax", "measure_label": "Total income tax", "measure_ordinal": 2, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "total_income_tax", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:ec48b42ac43674a2a2206b8b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.total_income_tax", "source_cell_keys": ["ledger.source_cell.v1:f38c48e6e2f23204eb997143"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.total_income_tax", "source_measure_id": "total_income_tax", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "usd"}, "observed_measure_key": "ledger.observed_measure.v2:c11ff13c2dfe8af0795a1194", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "schema_version": "chronicle.consumer_fact.v4", "semantic_fact_key": "ledger.semantic_fact.v2:f8b2801687c0ba560054ed36", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 2147909818000, "value_type": "integer"} +{"aggregate_fact_key": "ledger.aggregate_fact.v2:13153fb8dfd73fa12d5715e9", "aggregation": {"method": "sum"}, "assertion": "observation", "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns, total"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year count irs soi individual income tax returns for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns, total", "measure_id": "return_count", "measure_label": "Number of returns", "measure_ordinal": 0, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "return_count", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:079f0697e3b090abf422781b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.return_count", "source_cell_keys": ["ledger.source_cell.v1:82f39b2a5aeab53c1b717928"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.individual_income_tax_returns", "source_measure_id": "return_count", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "count"}, "observed_measure_key": "ledger.observed_measure.v2:bc96277bae6ef31da4a5794c", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "publisher_dimension_value_labels": {"us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "schema_version": "chronicle.consumer_fact.v5", "semantic_fact_key": "ledger.semantic_fact.v2:1ae4c4f93f554ec7ca14d2ad", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 160602107, "value_type": "integer"} +{"aggregate_fact_key": "ledger.aggregate_fact.v2:36ce90001adb50e8e9758e4f", "aggregation": {"method": "sum"}, "assertion": "observation", "concept_alignment": {"authority": "ledger-us", "canonical_concept": "us:statutes/26/62#adjusted_gross_income", "concept_alignment_key": "ledger.concept_alignment.v2:2e0ac323f1b261710e4397a1", "evidence_notes": "IRS SOI Table 1.1 reports adjusted gross income for individual income tax returns; IRC section 62 defines adjusted gross income. This Ledger assertion treats the SOI AGI column as exactly adopting that legal concept for the tax-year source record.", "evidence_url": "https://uscode.house.gov/view.xhtml?req=(title:26%20section:62%20edition:prelim)", "legal_vintage": "tax_year_2023", "relation": "exact", "source_concept": "irs_soi.adjusted_gross_income"}, "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns, total"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year sum us:statutes/26/62#adjusted gross income for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns, total", "measure_id": "adjusted_gross_income", "measure_label": "Adjusted gross income", "measure_ordinal": 1, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "adjusted_gross_income", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:7f8e9d3c64fec2670b571c3b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.adjusted_gross_income", "source_cell_keys": ["ledger.source_cell.v1:c3d349e26856fb6224303675"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.adjusted_gross_income", "source_measure_id": "adjusted_gross_income", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "usd"}, "observed_measure_key": "ledger.observed_measure.v2:ac9c3cef252ef7ead0066cb1", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "publisher_dimension_value_labels": {"us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "schema_version": "chronicle.consumer_fact.v5", "semantic_fact_key": "ledger.semantic_fact.v2:b83082b8e69c1ddc6fdebed2", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 15286017359000, "value_type": "integer"} +{"aggregate_fact_key": "ledger.aggregate_fact.v2:0fa0e8c17cfe0ebd844c4f95", "aggregation": {"method": "sum"}, "assertion": "observation", "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns, total"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year sum irs soi total income tax for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns, total", "measure_id": "total_income_tax", "measure_label": "Total income tax", "measure_ordinal": 2, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "total_income_tax", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:ec48b42ac43674a2a2206b8b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.total_income_tax", "source_cell_keys": ["ledger.source_cell.v1:f38c48e6e2f23204eb997143"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.total_income_tax", "source_measure_id": "total_income_tax", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "usd"}, "observed_measure_key": "ledger.observed_measure.v2:c11ff13c2dfe8af0795a1194", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "publisher_dimension_value_labels": {"us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "schema_version": "chronicle.consumer_fact.v5", "semantic_fact_key": "ledger.semantic_fact.v2:f8b2801687c0ba560054ed36", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 2147909818000, "value_type": "integer"} diff --git a/chronicle/source_package.py b/chronicle/source_package.py index 277ee2ff..214a4e2d 100644 --- a/chronicle/source_package.py +++ b/chronicle/source_package.py @@ -10,6 +10,7 @@ from pathlib import Path import re import time +from collections.abc import Mapping, Sequence from typing import Any from urllib.parse import unquote, urlparse from zipfile import ZipFile @@ -32,7 +33,13 @@ AggregateFact, build_label, ) -from chronicle.dimension_labels import label_facts, unused_label_declarations +from chronicle.dimension_labels import ( + ONE_LABEL_PER_VALUE, + canonical_value_label, + dimension_value_id, + label_facts, + unused_label_declarations, +) from chronicle.epoch import SCHEMA_IDS, schema_id from chronicle.sources.cells import ( SourceArtifactMetadata, @@ -2348,6 +2355,71 @@ def try_load_source_package(source: str | Path) -> SourcePackage | None: return None +def _row_value_label_issues( + record_sets: Sequence[SourceRecordSetSpec], + declared_value_labels: Mapping[str, Mapping[str, str]], + *, + package_path: Path | None = None, +) -> list[SourcePackageIssue]: + """Refuse a groupby value the package's rows label two ways (chronicle#376). + + Each row's ``label:`` becomes its facts' value label unless Chronicle's + register lists the value or the package declares one label for it in + ``dimension_value_labels``; the rows' wording is then kept as the + package's own, in ``publisher_dimension_value_labels``. Otherwise rows + that word one value differently would give its facts two labels, which + ``build-bundle`` refuses. This finds it from the package file alone, at + any build year. + """ + + labels_by_value: dict[tuple[str, str], set[str]] = {} + record_sets_by_value: dict[tuple[str, str], list[str]] = {} + for record_set in record_sets: + groupby = (record_set.groupby_dimension or "").strip() + if not groupby: + continue + for row in record_set.rows: + label = (row.label or "").strip() + if not label: + continue + value_id = dimension_value_id(row.value_id) + if canonical_value_label(groupby, value_id) or ( + declared_value_labels.get(groupby, {}).get(value_id) + ): + continue + labels_by_value.setdefault((groupby, value_id), set()).add(label) + record_sets_by_value.setdefault((groupby, value_id), []).append( + record_set.record_set_id + ) + where = f" in {_display_package_path(package_path)}" if package_path else "" + return [ + SourcePackageIssue( + code="conflicting_value_label", + message=( + f"Dimension {dimension_id!r} value {value_id!r} is labelled " + f"{sorted(labels)!r} by the rows of this package{where}. " + f"{ONE_LABEL_PER_VALUE}" + ), + record_set_id=record_sets_by_value[(dimension_id, value_id)][0], + row_id=value_id, + ) + for (dimension_id, value_id), labels in sorted(labels_by_value.items()) + if len(labels) > 1 + ] + + +def _display_package_path(package_path: str | Path) -> str: + """Return a package file's path relative to the repository when it can.""" + + path = Path(package_path) + if path.is_dir(): + path = path / "source_package.yaml" + try: + return str(path.resolve().relative_to(Path(__file__).resolve().parents[1])) + except ValueError: + return str(path) + + def validate_source_package( source: str | Path, *, @@ -2448,6 +2520,13 @@ def validate_source_package( source_record_ids.setdefault(spec.source_record_id, []).append(spec_index) counts["source_record_count"] += 1 + errors.extend( + _row_value_label_issues( + record_sets, + package.dimension_value_labels or {}, + package_path=package.package_path, + ) + ) for record_set_id, indices in record_set_ids.items(): if len(indices) > 1: errors.append( diff --git a/chronicle/store.py b/chronicle/store.py index b62e5349..2ae31a0a 100644 --- a/chronicle/store.py +++ b/chronicle/store.py @@ -92,6 +92,12 @@ def fact_from_mapping(payload: dict[str, Any]) -> AggregateFact: "dimension_value_labels", {} ).items() }, + publisher_dimension_value_labels={ + dimension_id: dict(value_labels) + for dimension_id, value_labels in payload.get( + "publisher_dimension_value_labels", {} + ).items() + }, ) @@ -101,7 +107,11 @@ def fact_to_mapping(fact: AggregateFact) -> dict[str, Any]: if isinstance(fact.value, Decimal): payload["value"] = str(fact.value) payload["value_type"] = "decimal" - for label_field in ("dimension_labels", "dimension_value_labels"): + for label_field in ( + "dimension_labels", + "dimension_value_labels", + "publisher_dimension_value_labels", + ): if not payload[label_field]: payload.pop(label_field) payload["measure"] = { diff --git a/chronicle/suite.py b/chronicle/suite.py index 78064d4f..71087e82 100644 --- a/chronicle/suite.py +++ b/chronicle/suite.py @@ -615,6 +615,58 @@ def _canonicalize_lineage_key(domain_name: str, key: Any) -> str: return key +_VALUE_LABEL_CONFLICT_CODES = frozenset( + {"conflicting_dimension_value_label", "conflicting_groupby_value_label"} +) + + +def _value_label_acceptance_issues( + facts: Sequence[AggregateFact], +) -> list[AgentAcceptanceIssue]: + """Refuse a value this package's facts label two ways (chronicle#376). + + The facts already carry the label Chronicle's register gives every value + it lists, so this finds a value the register does not list whose rows, + source text or declarations word it two ways, before ``build-bundle`` + would refuse it. + """ + + # Imported here: chronicle.dimension_labels imports chronicle.suite's + # source-row helpers lazily, and the suite imports the package loader. + from chronicle.dimension_labels import dimension_label_issues + + rows = [] + for fact in facts: + layout = fact.layout + rows.append( + { + "aggregate_fact_key": fact.source_record_id, + "dimensions": { + key: value + for key, value in fact.filters.items() + if value is not None + }, + "dimension_labels": fact.dimension_labels, + "dimension_value_labels": fact.dimension_value_labels, + "layout": ( + { + "groupby_dimension": layout.groupby_dimension, + "groupby_dimension_label": layout.groupby_dimension_label, + "groupby_value_id": layout.groupby_value_id, + "groupby_value_label": layout.groupby_value_label, + } + if layout + else {} + ), + } + ) + return [ + AgentAcceptanceIssue(code="conflicting_value_label", message=issue.message) + for issue in dimension_label_issues(rows) + if issue.code in _VALUE_LABEL_CONFLICT_CODES + ] + + def build_agent_acceptance_report( facts: list[AggregateFact], rows: list[SourceRow], @@ -863,6 +915,7 @@ def build_agent_acceptance_report( ) ) + errors.extend(_value_label_acceptance_issues(facts)) checks = { "stage_reports_valid": stage_reports_valid, "raw_artifacts_have_r2": raw_r2_link_count == len(artifacts) diff --git a/docs/agent-source-package-harness.md b/docs/agent-source-package-harness.md index b314d3b9..a6a27c6b 100644 --- a/docs/agent-source-package-harness.md +++ b/docs/agent-source-package-harness.md @@ -852,19 +852,44 @@ dimension_value_labels: A groupby axis whose rows cross several fields takes the label of its leading field, the one its id names; the crossed fields carry their own labels as dimensions of the same fact. A declaration wins over every recovered label. `build-bundle` reports an error for any package fact with an unlabelled or doubly labelled dimension or value, and for a dimension id two packages label differently (chronicle#265 — the rule -covers every country, since a consumer reads one artifact). One dimension value -id carries one label too (chronicle#376): `build-bundle` reports an error for a -value two packages label differently, and for publisher row labels that differ -across one groupby value's rows. Microcosm copies the labels into every target, -so a release would otherwise label one value two ways. A package whose table -words a band differently from the other packages takes their label in the row's -`label:` and keeps the publisher's text in its guards; the IRS filing-season -week-47 table heads its below-$1 column "No adjusted gross income and deficit", -and the package labels it "No adjusted gross income", as every other IRS SOI -package does. The exceptions are the publisher wordings Chronicle reviewed and -kept before that rule, listed with every label each value may carry in -`REVIEWED_VALUE_LABEL_VARIANTS` (`chronicle/dimension_labels.py`); those are -warnings. `build_facts` refuses a declaration no fact uses. +covers every country, since a consumer reads one artifact). `build_facts` +refuses a declaration no fact uses. + +One dimension value carries one label (chronicle#376). Microcosm copies labels +into every target and merges packages built for different years into one feed, +so a value must not depend on which packages one build compares. Chronicle's +register, `chronicle/dimension_values/canonical_labels.json`, gives one label to +every value that two or more packages emit, and to every value its packages word +more than one way. When a package is built, at any year, each value the +register lists takes the register's label in `dimension_value_labels` and, for +the groupby value, `layout.groupby_value_label`; the package's own wording (the +row's `label:`, else its declaration, else its source-row text) is kept in +`publisher_dimension_value_labels` where it differs, the way +`geography.publisher_name` keeps a publisher's area name (chronicle#281). So a +row's `label:` stays the publisher's text, and the row's guards still check it +against the source: the IRS filing-season week-47 table heads its below-$1 +column "No adjusted gross income and deficit", the package's rows say so, and +their facts carry the register's "No adjusted gross income" with the IRS text +beside it. + +A value the register does not list is one only one package emits, so its +package's label is its one label. A value labelled two ways is an error, with no +list of exceptions: + +- inside one package, `validate-package` reports `conflicting_value_label` for + a groupby value whose rows word it two ways, `build-suite` reports the same + for any value its facts label two ways, and `build-bundle` reports + `conflicting_groupby_value_label` or `conflicting_dimension_value_label`; + declare one label for the value in the package's `dimension_value_labels`; +- across packages, `build-bundle` reports + `conflicting_value_label_across_packages`; add the value to the register with + one label. + +A new package that emits a value another package already emits should check the +register: the value is either listed, and takes the listed label, or needs an +entry. To choose a register label for a value its packages word differently, +take the wording most packages use, ties going to the national statistics +office's wording, and keep each package's text in its rows. Every fact also carries the publisher's name for its geography (chronicle#266), taken from the record set's or the row's `geography_name`. `build-bundle` @@ -874,8 +899,9 @@ catalog only for identifiers it already knows, and an identifier is not a name. A country-level geography may stay unnamed, and no name is ever invented from the code. Where two packages name one area differently — the IRS truncates county names to twenty characters, and ONS writes "Yorkshire and The Humber" -where HMRC writes "the" — Chronicle keeps each publisher's text and warns, the -same way it does for a groupby value's row labels. +where HMRC writes "the" — Chronicle's UK register gives one name where it +carries the identifier (chronicle#281), and the bundle warns about the areas it +does not carry yet. ### Pinned artifacts and year labels diff --git a/docs/schemas/consumer_fact.v5.schema.json b/docs/schemas/consumer_fact.v5.schema.json new file mode 100644 index 00000000..d267ef80 --- /dev/null +++ b/docs/schemas/consumer_fact.v5.schema.json @@ -0,0 +1,502 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://policyengine.org/chronicle/schemas/consumer_fact.v5.schema.json", + "title": "Chronicle consumer fact contract row", + "description": "The v4 contract row with one label per dimension value (chronicle#376). Where Chronicle's register of dimension value labels carries a value, dimension_value_labels and layout.groupby_value_label carry the register's label, and publisher_dimension_value_labels carries the source package's own wording of that value when it differs; elsewhere the labels are the package's, as in v4. Both stay display metadata: neither enters a fact key, and a v4 row restamped with this schema_version is a valid v5 row.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "aggregate_fact_key", + "semantic_fact_key", + "legacy_fact_key", + "source_release_key", + "source_series_key", + "observed_measure_key", + "dimension_set_key", + "universe_constraint_set_key", + "value", + "value_type", + "assertion", + "provenance_class", + "period", + "geography", + "entity", + "aggregation", + "observed_measure", + "dimensions", + "universe_constraints", + "source", + "lineage" + ], + "allOf": [ + { + "if": { + "properties": { + "provenance_class": { + "const": "survey_aggregate" + } + }, + "required": [ + "provenance_class" + ] + }, + "then": { + "required": [ + "survey_instrument" + ] + }, + "else": { + "not": { + "required": [ + "survey_instrument" + ] + } + } + } + ], + "properties": { + "schema_version": { + "const": "chronicle.consumer_fact.v5" + }, + "aggregate_fact_key": { + "type": "string", + "pattern": "^ledger\\.aggregate_fact\\.v2:[0-9a-f]{24}$" + }, + "semantic_fact_key": { + "type": "string", + "pattern": "^ledger\\.semantic_fact\\.v2:[0-9a-f]{24}$" + }, + "legacy_fact_key": { + "type": "string", + "pattern": "^ledger\\.fact\\.v1:[0-9a-f]{24}$" + }, + "source_release_key": { + "type": "string", + "pattern": "^ledger\\.source_release\\.v2:[0-9a-f]{24}$" + }, + "source_series_key": { + "type": "string", + "pattern": "^ledger\\.source_series\\.v2:[0-9a-f]{24}$" + }, + "observed_measure_key": { + "type": "string", + "pattern": "^ledger\\.observed_measure\\.v2:[0-9a-f]{24}$" + }, + "dimension_set_key": { + "type": "string", + "pattern": "^ledger\\.dimension_set\\.v2:[0-9a-f]{24}$" + }, + "universe_constraint_set_key": { + "type": "string", + "pattern": "^ledger\\.universe_constraint_set\\.v2:[0-9a-f]{24}$" + }, + "value": { + "type": [ + "integer", + "number", + "string", + "boolean" + ] + }, + "value_type": { + "enum": [ + "integer", + "number", + "decimal", + "string", + "boolean" + ] + }, + "period": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "description": "Publisher-asserted period kind; the ledger.core.ALLOWED_PERIOD_TYPES vocabulary: calendar_year, tax_year, fiscal_year, academic_year, gas_year, month, quarter, week. Split-label years store the opening year of the publisher's label (UK FY2024-25 -> fiscal_year 2024; AY 2024/25 -> academic_year 2024). gas_year keeps DESNZ's year label: the opening year from 2016 (mid-May 2024 to mid-May 2025 -> gas_year 2024) and the closing year of the October-to-September years to 2015. Publishers labelling a fiscal year with a single year keep that label year (US federal FY2024 -> 2024). Quarter values use YYYY-QN and ISO week values use YYYY-Www, with exact dates in period_coverage." + }, + "value": { + "type": [ + "integer", + "string" + ] + } + } + }, + "geography": { + "type": "object", + "additionalProperties": false, + "required": [ + "level", + "id" + ], + "properties": { + "level": { + "type": "string" + }, + "id": { + "type": "string" + }, + "vintage": { + "type": "string" + }, + "name": { + "$ref": "#/$defs/label", + "description": "The name for this geography: Chronicle's register name where it carries the identifier, otherwise the publisher's own text; absent when neither states a name." + }, + "publisher_name": { + "$ref": "#/$defs/label", + "description": "The publisher's own name for this geography, present only where it differs from the register name carried in 'name'." + } + } + }, + "entity": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "aggregation": { + "type": "object", + "additionalProperties": false, + "required": [ + "method" + ], + "properties": { + "method": { + "type": "string" + }, + "denominator": { + "type": "string" + } + } + }, + "observed_measure": { + "type": "object", + "additionalProperties": false, + "required": [ + "source_name", + "source_table", + "source_measure_id", + "source_concept", + "unit" + ], + "properties": { + "source_name": { + "type": "string" + }, + "source_table": { + "type": "string" + }, + "source_measure_id": { + "type": "string" + }, + "source_concept": { + "type": "string" + }, + "unit": { + "type": "string" + } + } + }, + "dimensions": { + "type": "object", + "additionalProperties": { + "type": [ + "integer", + "number", + "string", + "boolean" + ] + } + }, + "dimension_labels": { + "type": "object", + "description": "Label for each dimension id: every key of dimensions and, when layout.groupby_dimension is set, that id.", + "additionalProperties": { + "$ref": "#/$defs/label" + } + }, + "dimension_value_labels": { + "type": "object", + "description": "Label for each dimension value, keyed dimension id then value id: the label in Chronicle's register of dimension value labels when it carries the value, otherwise the source package's label. A value id is the value's string form: booleans lower-cased (true, false), integers, numbers and strings as Python str() writes them, lists and mappings JSON-serialised with sorted keys and no spaces. Covers every dimensions entry and, when layout.groupby_dimension is set, layout.groupby_value_id, whose label layout.groupby_value_label repeats.", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/label" + } + } + }, + "publisher_dimension_value_labels": { + "type": "object", + "description": "The source package's own wording of a dimension value, keyed dimension id then value id like dimension_value_labels: the publisher's row or column text the package reads, present only where it differs from the label in dimension_value_labels.", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/label" + } + } + }, + "universe_constraints": { + "type": "object", + "additionalProperties": false, + "required": [ + "domain" + ], + "properties": { + "domain": { + "type": "string" + }, + "constraints": { + "type": "array", + "items": { + "$ref": "#/$defs/constraint" + } + } + } + }, + "source": { + "type": "object", + "additionalProperties": true, + "required": [ + "source_name", + "source_table", + "source_file", + "vintage", + "extracted_at", + "extraction_method", + "source_sha256", + "source_size_bytes", + "raw_r2_uri" + ], + "properties": { + "source_name": { + "type": "string" + }, + "source_table": { + "type": "string" + }, + "source_file": { + "type": "string" + }, + "url": { + "type": "string" + }, + "vintage": { + "type": "string" + }, + "extracted_at": { + "type": "string" + }, + "extraction_method": { + "type": "string" + }, + "method_notes": { + "type": "string" + }, + "source_sha256": { + "type": "string" + }, + "source_size_bytes": { + "type": "integer" + }, + "raw_r2_bucket": { + "type": "string" + }, + "raw_r2_key": { + "type": "string" + }, + "raw_r2_uri": { + "type": "string" + } + } + }, + "lineage": { + "type": "object", + "additionalProperties": false, + "required": [ + "source_record_id", + "source_cell_keys" + ], + "properties": { + "source_record_id": { + "type": "string" + }, + "source_cell_keys": { + "type": "array", + "items": { + "type": "string" + } + }, + "source_row_keys": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "layout": { + "type": "object", + "additionalProperties": true, + "properties": { + "groupby_dimension_label": { + "$ref": "#/$defs/label", + "description": "Label of layout.groupby_dimension; equals dimension_labels for that id." + } + } + }, + "label": { + "type": "string" + }, + "concept_alignment": { + "type": "object", + "additionalProperties": false, + "required": [ + "concept_alignment_key", + "source_concept", + "canonical_concept", + "relation", + "authority" + ], + "properties": { + "concept_alignment_key": { + "type": "string", + "pattern": "^ledger\\.concept_alignment\\.v2:[0-9a-f]{24}$" + }, + "source_concept": { + "type": "string" + }, + "canonical_concept": { + "type": "string" + }, + "relation": { + "type": "string" + }, + "authority": { + "type": "string" + }, + "evidence_url": { + "type": "string" + }, + "evidence_notes": { + "type": "string" + }, + "legal_vintage": { + "type": "string" + } + } + }, + "assertion": { + "type": "string", + "enum": [ + "observation", + "source_projection" + ], + "description": "Who asserted the value: a publisher-measured outcome (observation) or the publisher's own forward-looking estimate (source_projection). PolicyEngine-computed values are never consumer facts." + }, + "provenance_class": { + "type": "string", + "enum": [ + "administrative", + "census", + "survey_aggregate", + "model_output" + ], + "description": "Publisher measurement basis. Consumers can gate calibration inputs on this required, closed classification." + }, + "survey_instrument": { + "type": "string", + "pattern": ".*\\S.*", + "description": "Survey name required exactly when provenance_class is survey_aggregate." + }, + "period_coverage": { + "type": "object", + "additionalProperties": false, + "description": "Non-identity provenance for the fact's reference period: coverage dates, basis, the publisher's period label, and accounting basis.", + "properties": { + "start_date": { + "type": "string" + }, + "end_date": { + "type": "string" + }, + "basis": { + "type": "string", + "enum": [ + "calendar", + "tax", + "fiscal", + "survey_reference", + "projection_horizon" + ] + }, + "source_period_label": { + "type": "string" + }, + "accounting_basis": { + "type": "string", + "enum": [ + "cash", + "accrual" + ] + }, + "notes": { + "type": "string" + } + } + } + }, + "$defs": { + "label": { + "type": "string", + "pattern": ".*\\S.*" + }, + "constraint": { + "type": "object", + "additionalProperties": false, + "required": [ + "variable", + "operator", + "value", + "role" + ], + "properties": { + "variable": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "value": { + "type": [ + "integer", + "number", + "string", + "boolean" + ] + }, + "unit": { + "type": "string" + }, + "role": { + "type": "string" + } + } + } + } +} diff --git a/packages/census/pep_2024_national_age_sex/source_package.yaml b/packages/census/pep_2024_national_age_sex/source_package.yaml index 7feccc87..bf93cdc1 100644 --- a/packages/census/pep_2024_national_age_sex/source_package.yaml +++ b/packages/census/pep_2024_national_age_sex/source_package.yaml @@ -843,8 +843,10 @@ record_sets: end: 84 label: single-year age sequence table_record_kind: detail + # The file has numeric age codes only (85 to 100 summed here), so the label + # is Chronicle's: the one its register gives age 85_plus (chronicle#376). - value_id: 85_plus - label: Age 85 and over + label: Age 85 plus ordinal: 18 row_number: 88 row_end_number: 103 diff --git a/packages/irs_soi/filing_season_week47_2024/source_package.yaml b/packages/irs_soi/filing_season_week47_2024/source_package.yaml index b2ff8a04..271c1efa 100644 --- a/packages/irs_soi/filing_season_week47_2024/source_package.yaml +++ b/packages/irs_soi/filing_season_week47_2024/source_package.yaml @@ -145,14 +145,14 @@ record_sets: domain: all_individual_income_tax_returns groupby_dimension: us:statutes/26/62#adjusted_gross_income rows: - # The IRS heads column C "No adjusted gross income and deficit" (the - # guard below checks that text). It is the class every other IRS SOI - # package labels "No adjusted gross income": adjusted gross income - # below $1, deficits included (column C's AGI is -$120.2 billion, and - # the 16 columns add up to all returns). One value id carries one label - # (chronicle#376). + # The label is the IRS's column header, which the guard below checks. + # It is the class every other IRS SOI package calls "No adjusted gross + # income": adjusted gross income below $1, deficits included (column C's + # AGI is -$120.2 billion, and the 16 columns add up to all returns). + # Facts take Chronicle's register label for under_1 and keep this text + # in publisher_dimension_value_labels (chronicle#376). - value_id: under_1 - label: No adjusted gross income + label: No adjusted gross income and deficit ordinal: 0 row_number: 115 column: C @@ -417,9 +417,9 @@ record_sets: domain: all_individual_income_tax_returns groupby_dimension: us:statutes/26/62#adjusted_gross_income rows: - # Labelled as in the count record set above (chronicle#376). + # The IRS's column header, as in the count record set above. - value_id: under_1 - label: No adjusted gross income + label: No adjusted gross income and deficit ordinal: 0 row_number: 116 column: C diff --git a/packages/irs_soi/historic_table_2/source_package.yaml b/packages/irs_soi/historic_table_2/source_package.yaml index bfb5cfe8..59fb1459 100644 --- a/packages/irs_soi/historic_table_2/source_package.yaml +++ b/packages/irs_soi/historic_table_2/source_package.yaml @@ -72,8 +72,11 @@ record_sets: domain: all_individual_income_tax_returns groupby_dimension: us:statutes/26/62#adjusted_gross_income rows: + # The CSV has no text for AGI_STUB 0 (the IRS guide calls it "No AGI + # Stub"), so the label is Chronicle's: the one its register gives + # adjusted gross income all (chronicle#376). - value_id: all - label: All returns + label: All returns, total ordinal: 0 row_number: 2 expected_row_header_column: B diff --git a/policyengine_chronicle/consumer.py b/policyengine_chronicle/consumer.py index 2e94e762..7f05f30c 100644 --- a/policyengine_chronicle/consumer.py +++ b/policyengine_chronicle/consumer.py @@ -284,7 +284,13 @@ def _geography_key_payload(row: dict[str, Any]) -> Any: def _recompute_aggregate_fact_key(row: dict[str, Any]) -> str: - """Recompute the aggregate fact key from the row's content.""" + """Recompute the aggregate fact key from the row's content. + + Labels are display metadata and never enter the payload: + ``dimension_labels``, ``dimension_value_labels``, ``layout`` and the v5 + ``publisher_dimension_value_labels`` (chronicle#376) are not read here, and + the geography's display names are dropped by :func:`_geography_key_payload`. + """ declared_key = row.get("aggregate_fact_key") epoch = HASH_DOMAINS["aggregate_fact"].infer_key_epoch(declared_key) assertion = row.get("assertion") diff --git a/policyengine_chronicle/schema.py b/policyengine_chronicle/schema.py index 70c62933..c6506e13 100644 --- a/policyengine_chronicle/schema.py +++ b/policyengine_chronicle/schema.py @@ -9,7 +9,9 @@ (chronicle#266), for ``chronicle.consumer_fact.v3`` rows, and the ``consumer_fact.v4`` schema, v3 with one name per geography identifier and the publisher's own text beside it (chronicle#281), for -``chronicle.consumer_fact.v4`` rows. The packaged schema +``chronicle.consumer_fact.v4`` rows, and the ``consumer_fact.v5`` schema, v4 +with one label per dimension value and the package's own wording beside it +(chronicle#376), for ``chronicle.consumer_fact.v5`` rows. The packaged schema bytes are the single source of truth: each artifact manifest records the sha256 of the schema its rows use, and a load rejects any manifest that claims a different one. @@ -40,6 +42,7 @@ SCHEMA_IDS["consumer_fact"].chronicle: "consumer_fact.v2.schema.json", "chronicle.consumer_fact.v3": "consumer_fact.v3.schema.json", "chronicle.consumer_fact.v4": "consumer_fact.v4.schema.json", + "chronicle.consumer_fact.v5": "consumer_fact.v5.schema.json", } ) diff --git a/policyengine_chronicle/schemas/consumer_fact.v5.schema.json b/policyengine_chronicle/schemas/consumer_fact.v5.schema.json new file mode 100644 index 00000000..d267ef80 --- /dev/null +++ b/policyengine_chronicle/schemas/consumer_fact.v5.schema.json @@ -0,0 +1,502 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://policyengine.org/chronicle/schemas/consumer_fact.v5.schema.json", + "title": "Chronicle consumer fact contract row", + "description": "The v4 contract row with one label per dimension value (chronicle#376). Where Chronicle's register of dimension value labels carries a value, dimension_value_labels and layout.groupby_value_label carry the register's label, and publisher_dimension_value_labels carries the source package's own wording of that value when it differs; elsewhere the labels are the package's, as in v4. Both stay display metadata: neither enters a fact key, and a v4 row restamped with this schema_version is a valid v5 row.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "aggregate_fact_key", + "semantic_fact_key", + "legacy_fact_key", + "source_release_key", + "source_series_key", + "observed_measure_key", + "dimension_set_key", + "universe_constraint_set_key", + "value", + "value_type", + "assertion", + "provenance_class", + "period", + "geography", + "entity", + "aggregation", + "observed_measure", + "dimensions", + "universe_constraints", + "source", + "lineage" + ], + "allOf": [ + { + "if": { + "properties": { + "provenance_class": { + "const": "survey_aggregate" + } + }, + "required": [ + "provenance_class" + ] + }, + "then": { + "required": [ + "survey_instrument" + ] + }, + "else": { + "not": { + "required": [ + "survey_instrument" + ] + } + } + } + ], + "properties": { + "schema_version": { + "const": "chronicle.consumer_fact.v5" + }, + "aggregate_fact_key": { + "type": "string", + "pattern": "^ledger\\.aggregate_fact\\.v2:[0-9a-f]{24}$" + }, + "semantic_fact_key": { + "type": "string", + "pattern": "^ledger\\.semantic_fact\\.v2:[0-9a-f]{24}$" + }, + "legacy_fact_key": { + "type": "string", + "pattern": "^ledger\\.fact\\.v1:[0-9a-f]{24}$" + }, + "source_release_key": { + "type": "string", + "pattern": "^ledger\\.source_release\\.v2:[0-9a-f]{24}$" + }, + "source_series_key": { + "type": "string", + "pattern": "^ledger\\.source_series\\.v2:[0-9a-f]{24}$" + }, + "observed_measure_key": { + "type": "string", + "pattern": "^ledger\\.observed_measure\\.v2:[0-9a-f]{24}$" + }, + "dimension_set_key": { + "type": "string", + "pattern": "^ledger\\.dimension_set\\.v2:[0-9a-f]{24}$" + }, + "universe_constraint_set_key": { + "type": "string", + "pattern": "^ledger\\.universe_constraint_set\\.v2:[0-9a-f]{24}$" + }, + "value": { + "type": [ + "integer", + "number", + "string", + "boolean" + ] + }, + "value_type": { + "enum": [ + "integer", + "number", + "decimal", + "string", + "boolean" + ] + }, + "period": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "description": "Publisher-asserted period kind; the ledger.core.ALLOWED_PERIOD_TYPES vocabulary: calendar_year, tax_year, fiscal_year, academic_year, gas_year, month, quarter, week. Split-label years store the opening year of the publisher's label (UK FY2024-25 -> fiscal_year 2024; AY 2024/25 -> academic_year 2024). gas_year keeps DESNZ's year label: the opening year from 2016 (mid-May 2024 to mid-May 2025 -> gas_year 2024) and the closing year of the October-to-September years to 2015. Publishers labelling a fiscal year with a single year keep that label year (US federal FY2024 -> 2024). Quarter values use YYYY-QN and ISO week values use YYYY-Www, with exact dates in period_coverage." + }, + "value": { + "type": [ + "integer", + "string" + ] + } + } + }, + "geography": { + "type": "object", + "additionalProperties": false, + "required": [ + "level", + "id" + ], + "properties": { + "level": { + "type": "string" + }, + "id": { + "type": "string" + }, + "vintage": { + "type": "string" + }, + "name": { + "$ref": "#/$defs/label", + "description": "The name for this geography: Chronicle's register name where it carries the identifier, otherwise the publisher's own text; absent when neither states a name." + }, + "publisher_name": { + "$ref": "#/$defs/label", + "description": "The publisher's own name for this geography, present only where it differs from the register name carried in 'name'." + } + } + }, + "entity": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "aggregation": { + "type": "object", + "additionalProperties": false, + "required": [ + "method" + ], + "properties": { + "method": { + "type": "string" + }, + "denominator": { + "type": "string" + } + } + }, + "observed_measure": { + "type": "object", + "additionalProperties": false, + "required": [ + "source_name", + "source_table", + "source_measure_id", + "source_concept", + "unit" + ], + "properties": { + "source_name": { + "type": "string" + }, + "source_table": { + "type": "string" + }, + "source_measure_id": { + "type": "string" + }, + "source_concept": { + "type": "string" + }, + "unit": { + "type": "string" + } + } + }, + "dimensions": { + "type": "object", + "additionalProperties": { + "type": [ + "integer", + "number", + "string", + "boolean" + ] + } + }, + "dimension_labels": { + "type": "object", + "description": "Label for each dimension id: every key of dimensions and, when layout.groupby_dimension is set, that id.", + "additionalProperties": { + "$ref": "#/$defs/label" + } + }, + "dimension_value_labels": { + "type": "object", + "description": "Label for each dimension value, keyed dimension id then value id: the label in Chronicle's register of dimension value labels when it carries the value, otherwise the source package's label. A value id is the value's string form: booleans lower-cased (true, false), integers, numbers and strings as Python str() writes them, lists and mappings JSON-serialised with sorted keys and no spaces. Covers every dimensions entry and, when layout.groupby_dimension is set, layout.groupby_value_id, whose label layout.groupby_value_label repeats.", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/label" + } + } + }, + "publisher_dimension_value_labels": { + "type": "object", + "description": "The source package's own wording of a dimension value, keyed dimension id then value id like dimension_value_labels: the publisher's row or column text the package reads, present only where it differs from the label in dimension_value_labels.", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/label" + } + } + }, + "universe_constraints": { + "type": "object", + "additionalProperties": false, + "required": [ + "domain" + ], + "properties": { + "domain": { + "type": "string" + }, + "constraints": { + "type": "array", + "items": { + "$ref": "#/$defs/constraint" + } + } + } + }, + "source": { + "type": "object", + "additionalProperties": true, + "required": [ + "source_name", + "source_table", + "source_file", + "vintage", + "extracted_at", + "extraction_method", + "source_sha256", + "source_size_bytes", + "raw_r2_uri" + ], + "properties": { + "source_name": { + "type": "string" + }, + "source_table": { + "type": "string" + }, + "source_file": { + "type": "string" + }, + "url": { + "type": "string" + }, + "vintage": { + "type": "string" + }, + "extracted_at": { + "type": "string" + }, + "extraction_method": { + "type": "string" + }, + "method_notes": { + "type": "string" + }, + "source_sha256": { + "type": "string" + }, + "source_size_bytes": { + "type": "integer" + }, + "raw_r2_bucket": { + "type": "string" + }, + "raw_r2_key": { + "type": "string" + }, + "raw_r2_uri": { + "type": "string" + } + } + }, + "lineage": { + "type": "object", + "additionalProperties": false, + "required": [ + "source_record_id", + "source_cell_keys" + ], + "properties": { + "source_record_id": { + "type": "string" + }, + "source_cell_keys": { + "type": "array", + "items": { + "type": "string" + } + }, + "source_row_keys": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "layout": { + "type": "object", + "additionalProperties": true, + "properties": { + "groupby_dimension_label": { + "$ref": "#/$defs/label", + "description": "Label of layout.groupby_dimension; equals dimension_labels for that id." + } + } + }, + "label": { + "type": "string" + }, + "concept_alignment": { + "type": "object", + "additionalProperties": false, + "required": [ + "concept_alignment_key", + "source_concept", + "canonical_concept", + "relation", + "authority" + ], + "properties": { + "concept_alignment_key": { + "type": "string", + "pattern": "^ledger\\.concept_alignment\\.v2:[0-9a-f]{24}$" + }, + "source_concept": { + "type": "string" + }, + "canonical_concept": { + "type": "string" + }, + "relation": { + "type": "string" + }, + "authority": { + "type": "string" + }, + "evidence_url": { + "type": "string" + }, + "evidence_notes": { + "type": "string" + }, + "legal_vintage": { + "type": "string" + } + } + }, + "assertion": { + "type": "string", + "enum": [ + "observation", + "source_projection" + ], + "description": "Who asserted the value: a publisher-measured outcome (observation) or the publisher's own forward-looking estimate (source_projection). PolicyEngine-computed values are never consumer facts." + }, + "provenance_class": { + "type": "string", + "enum": [ + "administrative", + "census", + "survey_aggregate", + "model_output" + ], + "description": "Publisher measurement basis. Consumers can gate calibration inputs on this required, closed classification." + }, + "survey_instrument": { + "type": "string", + "pattern": ".*\\S.*", + "description": "Survey name required exactly when provenance_class is survey_aggregate." + }, + "period_coverage": { + "type": "object", + "additionalProperties": false, + "description": "Non-identity provenance for the fact's reference period: coverage dates, basis, the publisher's period label, and accounting basis.", + "properties": { + "start_date": { + "type": "string" + }, + "end_date": { + "type": "string" + }, + "basis": { + "type": "string", + "enum": [ + "calendar", + "tax", + "fiscal", + "survey_reference", + "projection_horizon" + ] + }, + "source_period_label": { + "type": "string" + }, + "accounting_basis": { + "type": "string", + "enum": [ + "cash", + "accrual" + ] + }, + "notes": { + "type": "string" + } + } + } + }, + "$defs": { + "label": { + "type": "string", + "pattern": ".*\\S.*" + }, + "constraint": { + "type": "object", + "additionalProperties": false, + "required": [ + "variable", + "operator", + "value", + "role" + ], + "properties": { + "variable": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "value": { + "type": [ + "integer", + "number", + "string", + "boolean" + ] + }, + "unit": { + "type": "string" + }, + "role": { + "type": "string" + } + } + } + } +} diff --git a/tests/test_chronicle_core.py b/tests/test_chronicle_core.py index 6d3fc6f9..4d6a2531 100644 --- a/tests/test_chronicle_core.py +++ b/tests/test_chronicle_core.py @@ -470,11 +470,12 @@ def test_frozen_fixture_bytes_are_unchanged(): # chronicle#266 re-pinned this from cc18841e… when the rows began # carrying the publisher's geography name, chronicle#265 again # because the sample's own package now declares its dimension - # labels, and chronicle#281 again for the v4 contract. Only the - # schema_version moved that last time: the three rows are IRS SOI - # and the geography register is UK-only. The keys and values in - # them are unchanged throughout. - "d02308d049edcd0e60b29d590211456c6427a505e1acf50f1ae71bbcd2962a8c" + # labels, chronicle#281 again for the v4 contract, and chronicle#376 + # for v5: Table 1.1's "All returns" row now exports the register's + # "All returns, total" and keeps the IRS text in + # publisher_dimension_value_labels. The keys and values in them + # are unchanged throughout. + "d14e2bd4b66d46dd765e82805713f03e46141c479368bc7f78a73ef9cf9cc0a9" ), fixture_root / "source_cells" / "soi_table_1_1_2023_cells.jsonl": ( "615639f21ee63e54595c677e24c3eddff484c00a795a2f91b45a8575f021c7e2" diff --git a/tests/test_chronicle_uk_national_2025.py b/tests/test_chronicle_uk_national_2025.py index 1989d86a..05a02113 100644 --- a/tests/test_chronicle_uk_national_2025.py +++ b/tests/test_chronicle_uk_national_2025.py @@ -224,21 +224,30 @@ def test_slc_returning_support_keeps_both_publisher_footnote_spellings(facts_for line = "new_returning_total_returning_31_33_35" returning = {f.aggregation.method: f for f in facts if f.filters[axis] == line} # Table 7C(ii)!B75 and B111 contain different literal footnote spellings. - labels = { + # Both facts carry the one label Chronicle's register gives the value, and + # the row whose spelling differs keeps it as the package's own wording + # (chronicle#376). + publisher_spellings = { "sum": "Total returning [31][33][35]", "mean": "Total returning [31[[33][35]", } - assert set(returning) == set(labels) - for method, label in labels.items(): + assert set(returning) == set(publisher_spellings) + for method, spelling in publisher_spellings.items(): fact = returning[method] - assert fact.layout.groupby_value_label == label - assert fact.dimension_value_labels[axis][line] == label + assert fact.layout.groupby_value_label == "Total returning [31][33][35]" + assert fact.dimension_value_labels[axis][line] == ( + "Total returning [31][33][35]" + ) + assert ( + fact.publisher_dimension_value_labels.get(axis, {}).get( + line, "Total returning [31][33][35]" + ) + == spelling + ) errors, warnings = _dimension_label_reports(alias, consumer_fact_rows(facts)) assert not errors, [error.to_dict() for error in errors] - assert [(warning.code, warning.key) for warning in warnings] == [ - ("conflicting_groupby_value_label", f"{axis}={line}") - ] + assert warnings == [] def test_public_sector_employment_keeps_four_published_quarters(facts_for): diff --git a/tests/test_issue_261_dimension_labels.py b/tests/test_issue_261_dimension_labels.py index c72df502..3666d05b 100644 --- a/tests/test_issue_261_dimension_labels.py +++ b/tests/test_issue_261_dimension_labels.py @@ -242,22 +242,24 @@ def test_constraint_labels_that_disagree_label_nothing(): def test_the_groupby_row_label_labels_a_filter_on_the_same_axis(): + # A value Chronicle's register does not list (chronicle#376), so the row + # label is its label. [labelled] = label_facts( [ _fact( { - "household_income_band": "gbp15_000_gbp19_999", - "region": "gbp15_000_gbp19_999", + "household_income_band": "gbp15_000_gbp19_998", + "region": "gbp15_000_gbp19_998", }, groupby="desnz.need.household_income_band", - value_id="gbp15_000_gbp19_999", - value_label="£15,000 to £19,999 ", + value_id="gbp15_000_gbp19_998", + value_label="£15,000 to £19,998 ", ) ] ) assert labelled.dimension_value_labels["household_income_band"] == { - "gbp15_000_gbp19_999": "£15,000 to £19,999" + "gbp15_000_gbp19_998": "£15,000 to £19,998" } assert "region" not in labelled.dimension_value_labels @@ -534,9 +536,9 @@ def test_build_bundle_reports_an_unlabelled_uk_package(tmp_path, monkeypatch): ) assert not report.valid - assert {issue.code for issue in report.errors} == { - "missing_dimension_label", - "missing_dimension_value_label", - } + # The package's values are ones other ISC packages emit too, so they take + # the labels Chronicle's register gives them (chronicle#376); its + # dimension ids still need the declarations this copy dropped. + assert {issue.code for issue in report.errors} == {"missing_dimension_label"} assert {issue.source for issue in report.errors} == {str(package_dir)} assert "isc.census_line" in {issue.key for issue in report.errors} diff --git a/tests/test_issue_376_one_label_per_value.py b/tests/test_issue_376_one_label_per_value.py index 7a83cc30..1985c561 100644 --- a/tests/test_issue_376_one_label_per_value.py +++ b/tests/test_issue_376_one_label_per_value.py @@ -354,7 +354,7 @@ def test_a_package_declaration_settles_its_own_value_and_keeps_the_rows(tmp_path fact for fact in facts if fact.source_record_id.endswith( - "eitc_all_returns.total_earned_income_credit_amount" + "eitc_all_returns.earned_income_credit.total_earned_income_credit_amount" ) ) assert amount.layout.groupby_value_label == "Earned income credit" diff --git a/tests/test_policyengine_chronicle_schema.py b/tests/test_policyengine_chronicle_schema.py index 5113ef98..853ed163 100644 --- a/tests/test_policyengine_chronicle_schema.py +++ b/tests/test_policyengine_chronicle_schema.py @@ -92,11 +92,13 @@ def _chronicle_epoch_row(row): def _v2_row(index=0): """A fixture row restated as a ``chronicle.consumer_fact.v2`` row. - v2 carries the labels and knows nothing of the publisher's geography name. + v2 carries the labels and knows nothing of the publisher's geography name + or of a package's own value wording (v5). """ row = _fixture_row(index) row["schema_version"] = _V2 row.get("geography", {}).pop("name", None) + row.pop("publisher_dimension_value_labels", None) return row From 7faaa665f421a2a43b90a2b138101f011af3565a Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:44:35 +0400 Subject: [PATCH 05/13] Test the re-review fixes for one label per dimension value (#376) Tests only; they fail at e29da32. From the re-reviews of e29da32 (ledger-source-fidelity FAIL, ledger-contract and ledger-boundary PASS with findings): - The new fact field holds each package's own wording, which is often text Chronicle wrote (Statbel "65_plus", B01001 "Female 20 years" and "female", Eurostat "First income decile cut-point", the four Belgian country-total phrases, eurostat-nasa-10-nf-tr "... paid-direction cell", Groeipakket "Social supplement child caseload", NTS0601 "Age 30 to 39"). The tests expect it named package_dimension_value_labels and described in the v5 schema as unchecked text Chronicle may have written, and the Eurostat groupby key (d1, not the filter's D1) described. - ons.coicop=coicop_02 takes ONS's "Alcohol, tobacco and narcotics"; a source test checks ADFL = ADFM + ADFN + MNC2 in cpnsa.xlsx (it passes now, as a statement about the source). - validate-package refuses a dimension_value_labels declaration the register contradicts, and the 13 overridden declarations in six packages agree with the register. - Each one-label error gives only the remedy that applies: a register entry, with the rule for choosing its label, for two packages; a declaration for one package; the register's label when it lists the value. - scripts/build_dimension_value_register.py regenerates the register and its --static-check compares it with the package files; the register records what it was built from. - The harness doc says the guards check expected_row_header and expected_column_header, which are fields separate from label:. Co-Authored-By: Claude Opus 5.5 --- tests/test_chronicle_core.py | 2 +- tests/test_chronicle_uk_national_2025.py | 2 +- tests/test_dimension_value_register.py | 159 ++++++++++ tests/test_issue_376_one_label_per_value.py | 314 +++++++++++++++++++- tests/test_policyengine_chronicle_schema.py | 35 ++- 5 files changed, 491 insertions(+), 21 deletions(-) create mode 100644 tests/test_dimension_value_register.py diff --git a/tests/test_chronicle_core.py b/tests/test_chronicle_core.py index 4d6a2531..b8ca10a1 100644 --- a/tests/test_chronicle_core.py +++ b/tests/test_chronicle_core.py @@ -473,7 +473,7 @@ def test_frozen_fixture_bytes_are_unchanged(): # labels, chronicle#281 again for the v4 contract, and chronicle#376 # for v5: Table 1.1's "All returns" row now exports the register's # "All returns, total" and keeps the IRS text in - # publisher_dimension_value_labels. The keys and values in them + # package_dimension_value_labels. The keys and values in them # are unchanged throughout. "d14e2bd4b66d46dd765e82805713f03e46141c479368bc7f78a73ef9cf9cc0a9" ), diff --git a/tests/test_chronicle_uk_national_2025.py b/tests/test_chronicle_uk_national_2025.py index 05a02113..6cf0903b 100644 --- a/tests/test_chronicle_uk_national_2025.py +++ b/tests/test_chronicle_uk_national_2025.py @@ -239,7 +239,7 @@ def test_slc_returning_support_keeps_both_publisher_footnote_spellings(facts_for "Total returning [31][33][35]" ) assert ( - fact.publisher_dimension_value_labels.get(axis, {}).get( + fact.package_dimension_value_labels.get(axis, {}).get( line, "Total returning [31][33][35]" ) == spelling diff --git a/tests/test_dimension_value_register.py b/tests/test_dimension_value_register.py new file mode 100644 index 00000000..9b7e235e --- /dev/null +++ b/tests/test_dimension_value_register.py @@ -0,0 +1,159 @@ +"""The script that builds and checks Chronicle's dimension value register. + +``chronicle/dimension_values/canonical_labels.json`` gives one label to every +dimension value two or more packages emit, and to every value its packages +word more than one way (chronicle#376). The chronicle#377 reviews found that +the script which generated it was not committed and that nothing checked the +register against the packages, so a value two packages start sharing could +reach a feed with two labels. ``scripts/build_dimension_value_register.py`` +regenerates the register from built packages, and its ``--static-check`` reads +only the package files: every value two packages' files name must be listed, +every listed value must be named by some package file, and no package may +declare a label that differs from the register's. +""" + +from __future__ import annotations + +import importlib +import json +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +_REGISTER = ROOT / "chronicle" / "dimension_values" / "canonical_labels.json" + + +def _script(): + scripts = str(ROOT / "scripts") + if scripts not in sys.path: + sys.path.insert(0, scripts) + return importlib.import_module("build_dimension_value_register") + + +def test_the_static_check_passes_on_the_repository(): + assert _script().static_check_problems() == [] + + +def test_the_static_check_reports_a_shared_value_the_register_does_not_list(): + problems = _script().register_problems( + register={"sex": {"female": "Female"}}, + emitters={ + ("sex", "female"): {"ons-a", "nrs-b"}, + ("filing_status", "single"): {"soi-a", "soi-b"}, + }, + declarations={}, + ) + + assert problems == [ + "filing_status=single is emitted by 2 packages (soi-a, soi-b) but the " + "register does not list it; add it to " + "chronicle/dimension_values/canonical_labels.json with one label." + ] + + +def test_the_static_check_reports_a_listed_value_no_package_emits(): + problems = _script().register_problems( + register={"sex": {"female": "Female", "unknown": "Unknown"}}, + emitters={("sex", "female"): {"ons-a", "nrs-b"}}, + declarations={}, + ) + + assert problems == [ + "sex=unknown is in the register but no package emits it; remove it " + "from chronicle/dimension_values/canonical_labels.json." + ] + + +def test_the_static_check_reports_a_declaration_the_register_contradicts(): + problems = _script().register_problems( + register={"sex": {"female": "Female"}}, + emitters={("sex", "female"): {"ons-a", "nrs-b"}}, + declarations={("nrs-b", "sex", "female"): "Females"}, + ) + + assert problems == [ + "nrs-b declares dimension_value_labels.sex.female 'Females', but the " + "register labels it 'Female'; change the declaration to 'Female'." + ] + + +def test_the_register_is_built_from_each_packages_own_wording(): + script = _script() + rows_by_package = { + "pkg-a": [ + { + "geography": {"id": "UK"}, + "dimension_value_labels": {"sex": {"female": "Female"}}, + "layout": {}, + } + ], + "pkg-b": [ + { + "geography": {"id": "UK"}, + # Built after the register: the label is the register's, the + # package's wording is kept beside it. + "dimension_value_labels": {"sex": {"female": "Female"}}, + "package_dimension_value_labels": {"sex": {"female": "Females"}}, + "layout": {}, + }, + { + "geography": {"id": "UK"}, + "dimension_value_labels": {"region": {"uk": "UK"}}, + "layout": { + "groupby_dimension": "line", + "groupby_value_id": "total", + "groupby_value_label": "Total", + }, + }, + ], + } + + wordings = script.wordings_by_value(rows_by_package) + labels = script.register_labels(wordings, chosen={("sex", "female"): "Female"}) + + assert wordings[("sex", "female")] == { + "Female": {"pkg-a": 1}, + "Females": {"pkg-b": 1}, + } + # The region value equals the row's geography id, so it is not a value. + assert ("region", "uk") not in wordings + assert labels == {"sex": {"female": "Female"}} + with pytest.raises(SystemExit, match="sex=female"): + script.register_labels(wordings, chosen={}) + + +def test_the_chosen_labels_are_the_registers_labels(): + script = _script() + register = json.loads(_REGISTER.read_text(encoding="utf-8"))["labels"] + + assert len(script.CHOSEN_LABELS) == 43 + for (dimension_id, value_id), label in script.CHOSEN_LABELS.items(): + assert register[dimension_id][value_id] == label + + +def test_the_register_records_what_it_was_built_from(): + payload = json.loads(_REGISTER.read_text(encoding="utf-8")) + sources = payload["sources"] + + assert sources["script"] == "scripts/build_dimension_value_register.py" + assert sources["packages"] == "chronicle.bundle.DEFAULT_BUNDLE_SOURCES" + assert sources["built_at_chronicle_commit"].startswith("aa288c3") + assert sources["default_build_year"] == 2023 + assert sources["build_years"] == { + "census-acs-s0101-congressional-district-age-2024": 2024, + "census-acs-s0101-national-age-2024": 2024, + "census-acs-s0101-state-age-2024": 2024, + "census-acs-s2201-congressional-district-snap-2024": 2024, + "cms-aca-effectuated-enrollment-2022": 2022, + "cms-aca-oep-state-level": 2024, + "cms-aca-oep-state-level-2022": 2022, + "cms-aca-oep-state-level-2025": 2025, + "jct-obbba-revenue-estimates-2025": 2026, + "jct-tax-expenditures-2024": 2024, + } + assert sorted(sources["chosen_labels"]) == sorted( + f"{dimension_id}={value_id}" + for dimension_id, value_id in _script().CHOSEN_LABELS + ) diff --git a/tests/test_issue_376_one_label_per_value.py b/tests/test_issue_376_one_label_per_value.py index 1985c561..82f46032 100644 --- a/tests/test_issue_376_one_label_per_value.py +++ b/tests/test_issue_376_one_label_per_value.py @@ -14,8 +14,10 @@ label in Chronicle's register, ``chronicle/dimension_values/canonical_labels.json``. Every fact takes that label when it is built, at any build year, in ``dimension_value_labels`` and ``layout.groupby_value_label``. A package's own -wording, where it differs, is kept in ``publisher_dimension_value_labels`` -(``chronicle.consumer_fact.v5``), as chronicle#281 did for geography names. A +wording, where it differs, is kept in ``package_dimension_value_labels`` +(``chronicle.consumer_fact.v5``). That wording is the package's, not +necessarily the publisher's: often it is text Chronicle wrote, so the field +says so and nothing presents it as the publisher's text. A value no register entry covers takes its package's label, and two labels for it are an error wherever Chronicle can see both: inside one package in ``validate-package``, ``build-suite`` and ``build-bundle``, and across packages @@ -28,6 +30,8 @@ import json from pathlib import Path +import openpyxl +import pytest import yaml from chronicle import bundle, dimension_labels @@ -56,6 +60,16 @@ / "dimension_values" / "canonical_labels.json" ) +_REGISTER_PATH_TEXT = "chronicle/dimension_values/canonical_labels.json" +_HARNESS_DOC = Path(__file__).parents[1] / "docs" / "agent-source-package-harness.md" +_ONS_CONSUMER_TRENDS = ( + Path(__file__).parents[1] + / "db" + / "data" + / "ons" + / "consumer_trends_current_price_2026" + / "cpnsa.xlsx" +) def _week47_facts(): @@ -144,7 +158,7 @@ def test_week47_facts_carry_the_register_label_and_keep_the_irs_wording(): "under_1": IRS_SOI_LABEL } assert fact.layout.groupby_value_label == IRS_SOI_LABEL - assert fact.publisher_dimension_value_labels == { + assert fact.package_dimension_value_labels == { ADJUSTED_GROSS_INCOME: {"under_1": WEEK47_WORDING} } assert [ @@ -158,7 +172,7 @@ def test_week47_facts_carry_the_register_label_and_keep_the_irs_wording(): "under_1": IRS_SOI_LABEL } assert row["layout"]["groupby_value_label"] == IRS_SOI_LABEL - assert row["publisher_dimension_value_labels"] == { + assert row["package_dimension_value_labels"] == { ADJUSTED_GROSS_INCOME: {"under_1": WEEK47_WORDING} } @@ -233,7 +247,7 @@ def test_a_row_with_the_publisher_wording_loads_with_its_fact_key(tmp_path): "ledger.aggregate_fact.v2:fba2311e623134c71d8c78cd", ] assert all( - row["publisher_dimension_value_labels"] + row["package_dimension_value_labels"] == {ADJUSTED_GROSS_INCOME: {"under_1": WEEK47_WORDING}} for row in artifact.rows ) @@ -294,10 +308,21 @@ def test_a_value_two_packages_label_differently_is_refused_with_its_remedy(): ), ("conflicting_value_label_across_packages", "sex=male"), ] - message = errors[0].message - assert "'Heads of household' in ['a-new-package']" in message - assert "chronicle/dimension_values/canonical_labels.json" in message - assert "publisher_dimension_value_labels" in message + unlisted, listed = errors[0].message, errors[1].message + assert "'Heads of household' in ['a-new-package']" in unlisted + # Two packages emit it, so only a register entry can apply, and the + # message says how to choose its label. + assert f"Add it to {_REGISTER_PATH_TEXT}" in unlisted + assert "the wording most of these packages use" in unlisted + assert "national statistics office" in unlisted + assert "scripts/build_dimension_value_register.py --static-check" in unlisted + assert "dimension_value_labels" not in unlisted.replace( + "package_dimension_value_labels", "" + ) + # A listed value already has its label: the message names it. + assert "Chronicle's register labels this value 'Male'" in listed + assert "rebuild" in listed + assert "Add it to" not in listed # One label inside one package, refused as early as validate-package. @@ -316,8 +341,12 @@ def test_validate_package_refuses_a_value_one_package_labels_two_ways(tmp_path): assert f"{FILING_SEASON_LINE!r} value 'earned_income_credit'" in message assert "'Earned income credit'" in message assert "'Earned income credit amount'" in message - assert "dimension_value_labels" in message - assert "chronicle/dimension_values/canonical_labels.json" in message + # One package emits it and the register does not list it: the one remedy + # is a declaration in this package. + assert "Declare one label for it in this package's dimension_value_labels" in ( + message + ) + assert _REGISTER_PATH_TEXT not in message def test_build_suite_refuses_a_value_one_package_labels_two_ways(tmp_path): @@ -358,7 +387,7 @@ def test_a_package_declaration_settles_its_own_value_and_keeps_the_rows(tmp_path ) ) assert amount.layout.groupby_value_label == "Earned income credit" - assert amount.publisher_dimension_value_labels == { + assert amount.package_dimension_value_labels == { FILING_SEASON_LINE: {"earned_income_credit": "Earned income credit amount"} } @@ -390,10 +419,269 @@ def test_the_bundle_refuses_a_value_one_package_labels_two_ways(): f"{FILING_SEASON_LINE}=earned_income_credit", ) ] - assert "chronicle/dimension_values/canonical_labels.json" in errors[0].message + assert "Declare one label for it in this package's dimension_value_labels" in ( + errors[0].message + ) + assert _REGISTER_PATH_TEXT not in errors[0].message assert warnings == [] +def test_a_listed_value_labelled_two_ways_in_old_rows_names_the_register_label(): + """Rows built before the register (or by hand) can still disagree.""" + rows = [ + { + "aggregate_fact_key": f"ledger.aggregate_fact.v2:{label}", + "dimensions": {"sex": "male"}, + "dimension_labels": {"sex": "Sex"}, + "dimension_value_labels": {"sex": {"male": label}}, + "layout": {}, + } + for label in ("Male", "Males") + ] + + errors, _warnings = bundle._dimension_label_reports("nrs-a", rows) + + assert [(error.code, error.key) for error in errors] == [ + ("conflicting_dimension_value_label", "sex=male") + ] + assert "Chronicle's register labels this value 'Male'" in errors[0].message + assert "Declare one label" not in errors[0].message + + +# A package declaration cannot contradict the register (chronicle#377 review). + + +def test_validate_package_refuses_a_declaration_the_register_contradicts(tmp_path): + package_dir = _week47_copy_with_drifting_line_label(tmp_path) + package_yaml = package_dir / "source_package.yaml" + payload = yaml.safe_load(package_yaml.read_text()) + payload["dimension_value_labels"][ADJUSTED_GROSS_INCOME] = { + "under_1": WEEK47_WORDING + } + package_yaml.write_text(yaml.safe_dump(payload, sort_keys=False)) + + issues = [ + issue + for issue in validate_source_package(package_dir, year=2024).errors + if issue.code == "declared_value_label_differs_from_register" + ] + + assert len(issues) == 1 + message = issues[0].message + assert ( + f"dimension_value_labels.{ADJUSTED_GROSS_INCOME}.under_1 declares " + f"{WEEK47_WORDING!r}" + ) in message + assert f"labels this value {IRS_SOI_LABEL!r}" in message + assert _REGISTER_PATH_TEXT in message + assert f"Change the declaration to {IRS_SOI_LABEL!r}" in message + assert str(package_yaml) in message or "source_package.yaml" in message + + +@pytest.mark.parametrize( + ("package_dir", "dimension_id", "value_id", "register_label"), + [ + ("nrs/mye_2025_council_area", "sex", "female", "Female"), + ("nrs/mye_2025_council_area", "sex", "male", "Male"), + ("nisra/mye_2025_lgd_single_year_age", "sex", "female", "Female"), + ("nisra/mye_2025_lgd_single_year_age", "sex", "male", "Male"), + ("nrs/pcon24_population_by_age_2024_revised_2026", "sex", "female", "Female"), + ("nrs/pcon24_population_by_age_2024_revised_2026", "sex", "male", "Male"), + ("ons/hpssa_pcon24_year_ending_march_2026", "property_type", "detached", "Detached"), + ( + "ons/hpssa_pcon24_year_ending_march_2026", + "property_type", + "semi_detached", + "Semi detached", + ), + ("ons/hpssa_pcon24_year_ending_march_2026", "property_type", "terraced", "Terraced"), + ("ons/pipr_rents_by_area_august_2026", "property_type", "detached", "Detached"), + ("ons/pipr_rents_by_area_august_2026", "property_type", "terraced", "Terraced"), + ("dft/nts0601_age_mode_trips_2025", "age_band", "30 to 39", "30 to 39"), + ("dft/nts0601_age_mode_trips_2025", "age_band", "40 to 49", "40 to 49"), + ], +) +def test_the_declarations_the_register_overrode_now_agree_with_it( + package_dir, dimension_id, value_id, register_label +): + """The 13 declarations the register silently overrode at e29da32.""" + payload = yaml.safe_load( + (_PACKAGES / package_dir / "source_package.yaml").read_text() + ) + + assert dimension_labels.canonical_value_label(dimension_id, value_id) == ( + register_label + ) + assert payload["dimension_value_labels"][dimension_id][value_id] == register_label + + +# The package's own wording: a field that says what it holds. + +# Each wording is text Chronicle wrote, not the publisher's (the chronicle#377 +# source-fidelity, contract and boundary reviews): a curated extract's value +# id, a parser's constant, or a package author's phrase. Each must be kept in a +# field that names it as the package's wording and says it is unchecked. +_CHRONICLE_WRITTEN_WORDINGS = [ + # The Statbel ZIP has single-year CD_AGE only; "65_plus" is the curated + # CSV's value id. + ("statbel-population-structure-2025", 2025, "person.age_band", "65_plus", "65_plus"), + # The B01001 JSON holds variable codes and numbers; the text is + # B01001_FEMALE_AGE_COLUMNS and the parser's sex column. + ("census-b01001-female-age-2023", 2023, "age", "age_20", "Female 20 years"), + ("census-b01001-female-age-2023", 2023, "sex", "female", "female"), + # Eurostat calls D1 "First decile"; the guard checks the code D1. + ( + "eurostat-ilc-di01", + 2023, + "quant_inc", + "d1", + "First income decile cut-point", + ), + # The four Belgian country-total rows: guards check Chronicle identifiers. + ( + "spf-finances-pit-2023", + 2023, + "measure", + "country_total", + "Belgium PIT federal and local tax before withholding", + ), + ( + "onem-rva-unemployment-2024", + 2023, + "measure", + "country_total", + "Complete unemployment benefit recipients", + ), + ( + "nbb-national-accounts-household-disposable-income-2024", + 2023, + "measure", + "country_total", + "Household disposable income gross", + ), + ( + "onss-contributions-2024", + 2023, + "measure", + "country_total", + "Worker-borne personal social security contributions", + ), + # The guard checks the code B2G in column D. + ( + "eurostat-nasa-10-nf-tr", + 2023, + "na_item", + "belgium_household_operating_surplus_gross", + "Operating surplus, gross (B.2G), paid-direction cell", + ), + # The guard checks the code sociale_toeslag. + ( + "opgroeien-groeipakket-caseload-2025", + 2023, + "groeipakket.component", + "sociale_toeslag", + "Social supplement child caseload", + ), + # DfT's row header, which the guard checks, is "30 to 39". + ("dft-nts0601-age-mode-trips-2025", 2023, "age_band", "30 to 39", "Age 30 to 39"), +] + + +@pytest.mark.parametrize( + ("source", "year", "dimension_id", "value_id", "wording"), + _CHRONICLE_WRITTEN_WORDINGS, +) +def test_text_chronicle_wrote_is_kept_as_the_packages_wording_not_the_publishers( + source, year, dimension_id, value_id, wording +): + package = load_source_package(source) + facts = package.build_facts(year, cells=package.build_source_cells(year)) + rows = consumer_fact_rows(facts) + description = json.loads( + ( + Path(__file__).parents[1] + / "docs" + / "schemas" + / "consumer_fact.v5.schema.json" + ).read_text() + )["properties"]["package_dimension_value_labels"]["description"] + + wordings = { + fact.package_dimension_value_labels.get(dimension_id, {}).get(value_id) + for fact in facts + } + assert wording in wordings + assert all("publisher_dimension_value_labels" not in row for row in rows) + assert any( + (row.get("package_dimension_value_labels") or {}) + .get(dimension_id, {}) + .get(value_id) + == wording + for row in rows + ) + assert "text Chronicle wrote" in description + + +def test_a_groupby_value_whose_axis_is_also_a_filter_is_keyed_by_its_row_value(): + """Eurostat ilc_di01: quant_inc is the filter D1 and the groupby row d1.""" + package = load_source_package("eurostat-ilc-di01") + facts = package.build_facts(2023, cells=package.build_source_cells(2023)) + fact = next( + fact + for fact in facts + if fact.layout.groupby_value_id == "d1" and fact.filters["statinfo"] == "SHARE" + ) + + assert fact.filters["quant_inc"] == "D1" + assert fact.layout.groupby_value_label == "First income decile" + assert fact.package_dimension_value_labels == { + "quant_inc": {"d1": "First income decile share"} + } + + +def test_the_harness_doc_says_which_fields_the_guards_check(): + text = " ".join(_HARNESS_DOC.read_text(encoding="utf-8").split()) + + assert "a row's `label:` stays the publisher's text" not in text + assert "package_dimension_value_labels" in text + assert "publisher_dimension_value_labels" not in text + assert ( + "The guards compare a row's `expected_row_header` and " + "`expected_column_header`, which are fields separate from its `label:`" + ) in text + + +# ONS COICOP 02: the total includes narcotics (chronicle#377 review). + + +def test_coicop_02_takes_the_ons_wording_that_names_narcotics(): + assert ( + dimension_labels.canonical_value_label("ons.coicop", "coicop_02") + == "Alcohol, tobacco and narcotics" + ) + + +def test_the_ons_coicop_02_total_includes_narcotics(): + """ADFL = ADFM (alcoholic beverages) + ADFN (tobacco) + MNC2 (narcotics).""" + workbook = openpyxl.load_workbook( + _ONS_CONSUMER_TRENDS, read_only=True, data_only=True + ) + sheet_rows = list(workbook["02CN"].iter_rows(values_only=True)) + workbook.close() + cdid_row = next(row for row in sheet_rows if row[0] == "CDID identifier code") + column_by_cdid = {cdid: index for index, cdid in enumerate(cdid_row) if cdid} + annual = [ + row + for row in sheet_rows + if isinstance(row[0], int) and 1997 <= row[0] <= 2100 + ] + + assert len(annual) >= 29 + for row in annual: + parts = [row[column_by_cdid[cdid]] for cdid in ("ADFM", "ADFN", "MNC2")] + assert abs(row[column_by_cdid["ADFL"]] - sum(parts)) < 0.5, row[0] + + def test_irs_soi_packages_label_the_band_below_one_dollar_one_way(tmp_path): report = bundle.build_bundle( tmp_path / "bundle", diff --git a/tests/test_policyengine_chronicle_schema.py b/tests/test_policyengine_chronicle_schema.py index 853ed163..92a03d87 100644 --- a/tests/test_policyengine_chronicle_schema.py +++ b/tests/test_policyengine_chronicle_schema.py @@ -98,7 +98,7 @@ def _v2_row(index=0): row = _fixture_row(index) row["schema_version"] = _V2 row.get("geography", {}).pop("name", None) - row.pop("publisher_dimension_value_labels", None) + row.pop("package_dimension_value_labels", None) return row @@ -145,11 +145,11 @@ def test_v5_schema_is_v4_plus_the_packages_own_value_wording(): assert v5["properties"]["schema_version"] == {"const": _V5} assert v5["required"] == v4["required"] assert set(v5["properties"]) - set(v4["properties"]) == { - "publisher_dimension_value_labels" + "package_dimension_value_labels" } - publisher_labels = v5["properties"]["publisher_dimension_value_labels"] - assert publisher_labels["type"] == "object" - assert publisher_labels["additionalProperties"] == { + package_wording = v5["properties"]["package_dimension_value_labels"] + assert package_wording["type"] == "object" + assert package_wording["additionalProperties"] == { "type": "object", "additionalProperties": {"$ref": "#/$defs/label"}, } @@ -157,11 +157,34 @@ def test_v5_schema_is_v4_plus_the_packages_own_value_wording(): for key in ("$id", "title", "description"): schema.pop(key, None) schema["properties"].pop("schema_version") - schema["properties"].pop("publisher_dimension_value_labels", None) + schema["properties"].pop("package_dimension_value_labels", None) schema["properties"]["dimension_value_labels"].pop("description") assert v5 == v4 +def test_v5_describes_the_package_wording_as_unchecked_text_chronicle_may_write(): + """The field holds a package's own wording, which is often Chronicle's text. + + Statbel's "65_plus", Census B01001's "Female 20 years" and "female", + Eurostat's "First income decile cut-point" and the Belgian country-total + phrases are text Chronicle wrote (the chronicle#377 source-fidelity + review), so the contract must not call the field the publisher's text. + """ + v5_text = (_REPO_ROOT / "docs" / "schemas" / _SCHEMA_FILES[_V5]).read_text() + description = consumer_fact_schema(_V5)["properties"][ + "package_dimension_value_labels" + ]["description"] + + assert "publisher_dimension_value_labels" not in v5_text + assert "the publisher's row or column text" not in v5_text + assert "not checked against the publisher's file" in description + assert "text Chronicle wrote" in description + assert "lineage.source_cell_keys" in description + # A groupby value whose axis is also a filter with another value (Eurostat + # quant_inc: filter D1, row d1) is keyed by its groupby value id. + assert "layout.groupby_value_id" in description + + def test_v4_schema_is_v3_plus_the_publishers_own_geography_name(): v3 = copy.deepcopy(consumer_fact_schema(_V3)) v4 = copy.deepcopy(consumer_fact_schema(_V4)) From d24819c740dccee405d279fe0f96853da8a77758 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:15:08 +0400 Subject: [PATCH 06/13] Call the field the package's wording; make the register the one source (#376) Fixes the re-review findings on e29da32: - publisher_dimension_value_labels becomes package_dimension_value_labels. It held each package's own wording (row label, else declaration, else source-row text), which is often text Chronicle wrote, while the v5 schema called it "the publisher's row or column text". Filling it only with guard-checked publisher text is not possible with the existing guards: B01001's guard compares the row label with a cell its parser builds from Chronicle's own constants, and Statbel's and the Belgian country-total guards compare text in extracts Chronicle prepared. The v5 schema, code comments, README, changelog and harness doc now say the field is the package's text, not checked against the publisher's file, often Chronicle's, with the publisher's cells reached through lineage; the schema also says how a groupby value whose axis is a filter with another value is keyed (Eurostat quant_inc: d1, not D1). v5 schema sha256: acd33a091cd8d192be5d264e3e94c13e00afb6fd1972e57d4576ac55a54f6970 (was c4d2579c...). The sample rows' sha256 is re-pinned. - ons.coicop=coicop_02 takes ONS's overview wording "Alcohol, tobacco and narcotics": the total (ADFL) includes narcotics (MNC2). - validate-package refuses a dimension_value_labels declaration the register contradicts (declared_value_label_differs_from_register). The 13 declarations in six packages that the register silently overrode now equal its labels: NRS mid-year estimates and PCON24 and NISRA sex, ONS HPSSA and PIPR property_type, DfT NTS0601 age_band. No exported label changes; those facts no longer carry the old declaration text in the package-wording field. - Each one-label error gives only the remedy that applies: two packages, a register entry with the rule for choosing its label and the static check to run; one package, a declaration; a value the register lists, its label and a rebuild. - scripts/build_dimension_value_register.py regenerates the register (building every default-bundle package at its build year, or reading stored builds) with --check, and --static-check compares it with the package files only. Run over the stored main builds the register was made from, it reproduces all 4,824 entries; only coicop_02 differs, by design. tests/test_dimension_value_register.py runs the static check in CI. The register records its sources: the script, the commit and years the packages were built at, and the chosen labels. - The harness doc says the guards compare expected_row_header and expected_column_header, fields separate from a row's label:. Test changed after the red commit: the NTS0601 example in test_text_chronicle_wrote_is_kept_as_the_packages_wording_not_the_publishers is replaced by test_nts0601_age_bands_now_carry_dfts_header_and_no_package_wording, because its "Age 30 to 39" came from the declaration this commit corrects. Two new files were also formatted with ruff format. Co-Authored-By: Claude Opus 5.5 --- README.md | 5 +- changelog.d/376.fixed.md | 2 +- chronicle/bundle.py | 10 +- chronicle/consumer_contract.py | 4 +- chronicle/core.py | 5 +- chronicle/database.py | 2 +- chronicle/dimension_labels.py | 131 ++++- .../dimension_values/canonical_labels.json | 67 ++- chronicle/epoch.py | 2 +- chronicle/fixtures/consumer_facts.jsonl | 6 +- chronicle/source_package.py | 46 +- chronicle/store.py | 6 +- docs/agent-source-package-harness.md | 36 +- docs/schemas/consumer_fact.v5.schema.json | 6 +- .../source_package.yaml | 6 +- .../source_package.yaml | 2 +- .../source_package.yaml | 6 +- .../mye_2025_council_area/source_package.yaml | 6 +- .../source_package.yaml | 6 +- .../source_package.yaml | 7 +- .../source_package.yaml | 6 +- policyengine_chronicle/consumer.py | 2 +- .../schemas/consumer_fact.v5.schema.json | 6 +- scripts/build_dimension_value_register.py | 467 ++++++++++++++++++ tests/test_chronicle_core.py | 9 +- tests/test_issue_376_one_label_per_value.py | 49 +- tests/test_policyengine_chronicle_schema.py | 2 +- 27 files changed, 810 insertions(+), 92 deletions(-) create mode 100644 scripts/build_dimension_value_register.py diff --git a/README.md b/README.md index dd552a3b..89d80da0 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,8 @@ must label all of them, and `build-bundle` reports a gap as an error two or more packages emit takes the label in `chronicle/dimension_values/canonical_labels.json` at every build year, and the package's own wording, where it differs, rides in -`publisher_dimension_value_labels`. A value labelled two ways is an error, never +`package_dimension_value_labels` (the package's text, which is not checked +against the publisher's file and is often Chronicle's). A value labelled two ways is an error, never a warning: `validate-package` and `build-suite` refuse one a package's rows label two ways, and `build-bundle` also refuses one two packages label differently. See `docs/agent-source-package-harness.md` for where labels come from. @@ -593,7 +594,7 @@ payload as its Ledger key; only the prefix differs. with one name per UK geography identifier and the publisher's text in an optional `geography.publisher_name`, and `chronicle.consumer_fact.v5` (chronicle#376) is v4 with one label per dimension value and the package's - own wording in an optional `publisher_dimension_value_labels`. Rows are + own wording in an optional `package_dimension_value_labels`. Rows are emitted as v5 with Ledger-named keys, and each row validates against the schema of the contract it names. Only v1 and v2 name a naming epoch: the epoch table renames identifiers, it does not version the row. diff --git a/changelog.d/376.fixed.md b/changelog.d/376.fixed.md index ddbd6ba6..0935b9bd 100644 --- a/changelog.d/376.fixed.md +++ b/changelog.d/376.fixed.md @@ -1 +1 @@ -One dimension value now carries one label, whichever packages and build years a consumer combines. Every value that two or more packages emit, or that its packages word more than one way, has one label in Chronicle's register, `chronicle/dimension_values/canonical_labels.json` (4,824 values). Facts take that label in `dimension_value_labels` and `layout.groupby_value_label` when they are built, and keep the package's own wording, where it differs, in the new optional `publisher_dimension_value_labels`; rows are emitted as `chronicle.consumer_fact.v5`, v4 plus that field. The IRS filing-season week-47 rows for adjusted gross income `under_1` keep the IRS header "No adjusted gross income and deficit" as their label and in that field, and their facts carry "No adjusted gross income", as every other IRS SOI package does. A value labelled two ways is an error, never a warning: `validate-package` and `build-suite` refuse one that a package labels two ways, and `build-bundle` also refuses one that two packages label differently; the list of accepted label variants is gone. No fact key or value moves (chronicle#376). +One dimension value now carries one label, whichever packages and build years a consumer combines. Every value that two or more packages emit, or that its packages word more than one way, has one label in Chronicle's register, `chronicle/dimension_values/canonical_labels.json` (4,824 values). Facts take that label in `dimension_value_labels` and `layout.groupby_value_label` when they are built, and keep the package's own wording, where it differs, in the new optional `package_dimension_value_labels`, which is the package's text (not checked against the publisher's file, and often Chronicle's own); rows are emitted as `chronicle.consumer_fact.v5`, v4 plus that field. The IRS filing-season week-47 rows for adjusted gross income `under_1` keep the IRS header "No adjusted gross income and deficit" as their label and in that field, and their facts carry "No adjusted gross income", as every other IRS SOI package does. A value labelled two ways is an error, never a warning: `validate-package` and `build-suite` refuse one that a package labels two ways, and `build-bundle` also refuses one that two packages label differently; the list of accepted label variants is gone. A package may not declare a label the register contradicts, and `scripts/build_dimension_value_register.py` regenerates the register, with a `--static-check` that compares it with the package files in CI. No fact key or value moves (chronicle#376). diff --git a/chronicle/bundle.py b/chronicle/bundle.py index a660cfd0..a1675068 100644 --- a/chronicle/bundle.py +++ b/chronicle/bundle.py @@ -10,7 +10,7 @@ from typing import Any, Callable, Sequence from chronicle.dimension_labels import ( - ONE_LABEL_PER_VALUE, + cross_package_value_label_remedy, dimension_label_issues, dimension_labels_by_id, ) @@ -908,9 +908,9 @@ def _cross_package_value_label_errors( adjusted gross income and deficit" where every other IRS SOI package writes "No adjusted gross income"). Every fact takes the label Chronicle's register gives a value it lists, so this finds values the register does not - list yet. There are no accepted exceptions: the remedy is one register - entry, and each package's own wording is then kept in - ``publisher_dimension_value_labels``. + list yet. There are no accepted exceptions: two packages emit the value, + so the one remedy is a register entry, and the message says how to choose + its label (or names the register's label, for rows built before it). """ return [ BuildBundleIssue( @@ -922,7 +922,7 @@ def _cross_package_value_label_errors( f"{label!r} in {sorted(sources)}" for label, sources in sorted(labels.items()) ) - + f". {ONE_LABEL_PER_VALUE}" + + f". {cross_package_value_label_remedy(dimension_id, value_id)}" ), key=f"{dimension_id}={value_id}", ) diff --git a/chronicle/consumer_contract.py b/chronicle/consumer_contract.py index 2c60e446..78793194 100644 --- a/chronicle/consumer_contract.py +++ b/chronicle/consumer_contract.py @@ -691,10 +691,10 @@ def _consumer_fact_row( }, # The package's own wording where Chronicle's register relabels a value # (chronicle#376); display metadata, like the labels, never in a key. - "publisher_dimension_value_labels": { + "package_dimension_value_labels": { dimension_id: dict(value_labels) for dimension_id, value_labels in ( - fact.publisher_dimension_value_labels.items() + fact.package_dimension_value_labels.items() ) }, "universe_constraints": _universe_constraint_set_payload(fact), diff --git a/chronicle/core.py b/chronicle/core.py index da1c4bfc..85b29018 100644 --- a/chronicle/core.py +++ b/chronicle/core.py @@ -292,8 +292,9 @@ class AggregateFact: dimension_value_labels: dict[str, dict[str, str]] = field(default_factory=dict) # The package's own wording of a value, keyed like dimension_value_labels, # where it differs from the label Chronicle's register gives the value - # (chronicle#376). Display metadata too. - publisher_dimension_value_labels: dict[str, dict[str, str]] = field( + # (chronicle#376). Display metadata too, and not checked against the + # publisher's file: often it is text Chronicle wrote. + package_dimension_value_labels: dict[str, dict[str, str]] = field( default_factory=dict ) diff --git a/chronicle/database.py b/chronicle/database.py index 065753de..a7325fac 100644 --- a/chronicle/database.py +++ b/chronicle/database.py @@ -1064,7 +1064,7 @@ def _canonical_fact_mapping(fact: AggregateFact) -> dict[str, Any]: for label_field in ( "dimension_labels", "dimension_value_labels", - "publisher_dimension_value_labels", + "package_dimension_value_labels", ): if not mapping[label_field]: mapping.pop(label_field) diff --git a/chronicle/dimension_labels.py b/chronicle/dimension_labels.py index ddb26060..5bb649ab 100644 --- a/chronicle/dimension_labels.py +++ b/chronicle/dimension_labels.py @@ -37,14 +37,23 @@ That label goes on ``dimension_value_labels`` and, for the groupby value, on ``layout.groupby_value_label``. The package's own wording of the value (its row label, else its declaration, else its source-row text) is kept in -``publisher_dimension_value_labels`` where it differs, as -``geography.publisher_name`` keeps a publisher's area name (chronicle#281). - -A value the register does not list is one that only one package emits, so its -package's label is its one label. :func:`dimension_label_issues` reports such a -value labelled two ways inside one package, and ``build-bundle`` reports it -labelled two ways by two packages; both are errors, with no exceptions. -Everything else that is unlabelled must be declared. +``package_dimension_value_labels`` where it differs. That wording is the +package's, and nothing checks it against the publisher's file: it is the +publisher's text only where a row guard happens to compare the same text with a +source cell (week 47's IRS column header), and elsewhere it is often text +Chronicle wrote (a parser's constant, a value id in an extract Chronicle +prepared, or a package author's phrase). The publisher's cells stay reachable +through each fact's lineage. + +A package may not declare a label the register contradicts: ``validate-package`` +refuses it (:func:`declared_value_label_issues`), so the register is the one +source of the label of every value it lists. A value the register does not list +is one that only one package emits (``scripts/build_dimension_value_register.py +--static-check`` checks this), so its package's label is its one label. +:func:`dimension_label_issues` reports such a value labelled two ways inside one +package, and ``build-bundle`` reports it labelled two ways by two packages; both +are errors, with no exceptions. Everything else that is unlabelled must be +declared. """ from __future__ import annotations @@ -71,17 +80,82 @@ REGISTER_PATH = Path(__file__).with_name("dimension_values") / "canonical_labels.json" #: The register's path as error messages name it, relative to the repository. REGISTER_DISPLAY_PATH = "chronicle/dimension_values/canonical_labels.json" -#: The remedy every one-label-per-value error states. -ONE_LABEL_PER_VALUE = ( - "One dimension value carries one label (chronicle#376). If other packages " - f"emit this value, add it to {REGISTER_DISPLAY_PATH} with one label; if " - "only this package does, declare one label for it in the package's " - "dimension_value_labels. Each package's own wording is then kept in " - "publisher_dimension_value_labels, and its guards still check the " - "publisher's text." +#: Where the rule for choosing a register label is written down. +LABEL_RULE_DISPLAY_PATH = "docs/agent-source-package-harness.md" +#: The command that compares the register with the package files. +REGISTER_STATIC_CHECK = ( + "uv run python scripts/build_dimension_value_register.py --static-check" ) +def _register_label_remedy(canonical: str) -> str: + return ( + f"Chronicle's register labels this value {canonical!r} " + f"({REGISTER_DISPLAY_PATH}), and every fact built at this Chronicle " + "version carries that label, so rebuild these facts (chronicle#376)." + ) + + +def in_package_value_label_remedy(dimension_id: str, value_id: str) -> str: + """Return the one remedy for a value one package labels two ways. + + A value the register lists already has its label, which the message names. + Any other value is one only this package emits, so the remedy is one + declaration in the package, never a register entry. + """ + + canonical = canonical_value_label(dimension_id, value_id) + if canonical is not None: + return _register_label_remedy(canonical) + return ( + "Declare one label for it in this package's dimension_value_labels " + "(chronicle#376): the publisher's wording of the value itself, without " + "words that name the measure, cell or table it was read from. Each " + "row's own wording is then kept in package_dimension_value_labels." + ) + + +def cross_package_value_label_remedy(dimension_id: str, value_id: str) -> str: + """Return the one remedy for a value two packages label differently. + + Two packages emit the value, so a package declaration cannot settle it: + the remedy is a register entry, and the message says how to choose its + label. A value the register lists already has its label. + """ + + canonical = canonical_value_label(dimension_id, value_id) + if canonical is not None: + return _register_label_remedy(canonical) + return ( + f"Add it to {REGISTER_DISPLAY_PATH} with one label (chronicle#376), " + f"chosen by the rule in {LABEL_RULE_DISPLAY_PATH}: the wording most of " + "these packages use, a tie going to the national statistics office's " + "wording, without footnote markers or words that name the measure. " + f"Then run `{REGISTER_STATIC_CHECK}`." + ) + + +def declared_value_label_issues( + dimension_value_labels: Mapping[str, Mapping[str, Any]], +) -> list[tuple[str, str, str, str]]: + """Return each declaration the register contradicts. + + Returns ``(dimension id, value id, declared label, register label)``. The + register's label wins on every fact, so a different declaration would only + mislead someone reading the package file. + """ + + found = [] + for dimension_id, value_labels in (dimension_value_labels or {}).items(): + for value_id, declared in (value_labels or {}).items(): + canonical = canonical_value_label(str(dimension_id), str(value_id)) + if canonical is not None and str(declared).strip() != canonical: + found.append( + (str(dimension_id), str(value_id), str(declared), canonical) + ) + return sorted(found) + + @cache def _register() -> Mapping[str, Mapping[str, str]]: payload = json.loads(REGISTER_PATH.read_text(encoding="utf-8")) @@ -140,7 +214,7 @@ def label_facts( columns, and ``source_rows`` the parsed rows the facts' lineage points at. A value the register lists takes the register's label, in ``dimension_value_labels`` and ``layout.groupby_value_label`` alike, and - the package's own wording goes to ``publisher_dimension_value_labels`` + the package's own wording goes to ``package_dimension_value_labels`` where it differs. A dimension or value no source labels is left unlabelled for :func:`dimension_label_issues` to report. """ @@ -200,8 +274,9 @@ def resolve( if not label: return value_labels[dimension_id][value_id] = label - # The package's own wording: its row's label (the publisher's text - # its guards check), else its declaration, else the source text. + # The package's own wording: its row's label, else its declaration, + # else the source-row text. Nothing checks it against the + # publisher's file, and it is often Chronicle's text. package_wording = row_label or declared_label or source_text if package_wording and package_wording != label: publisher_labels[dimension_id][value_id] = package_wording @@ -241,7 +316,7 @@ def resolve( fact, dimension_labels=labels, dimension_value_labels=dict(value_labels), - publisher_dimension_value_labels=dict(publisher_labels), + package_dimension_value_labels=dict(publisher_labels), layout=layout, ) ) @@ -333,15 +408,15 @@ def message(self) -> str: target = f"{target} value {self.value_id!r}" if self.code == "conflicting_groupby_value_label": return ( - f"{target} carries publisher row labels that differ across " - f"facts: {list(self.labels)!r}; a consumer selecting facts " - "from more than one of those rows sees conflicting labels. " - f"{ONE_LABEL_PER_VALUE}" + f"{target} carries row labels that differ across facts: " + f"{list(self.labels)!r}; a consumer selecting facts from more " + "than one of those rows sees conflicting labels. " + f"{in_package_value_label_remedy(self.dimension_id, self.value_id)}" ) if self.code == "conflicting_dimension_value_label": return ( f"{target} carries more than one label: {list(self.labels)!r}. " - f"{ONE_LABEL_PER_VALUE}" + f"{in_package_value_label_remedy(self.dimension_id, self.value_id)}" ) if self.code.startswith("conflicting"): return f"{target} carries more than one label: {list(self.labels)!r}." @@ -626,14 +701,18 @@ def _count( __all__ = [ - "ONE_LABEL_PER_VALUE", + "LABEL_RULE_DISPLAY_PATH", "REGISTER_DISPLAY_PATH", "REGISTER_PATH", + "REGISTER_STATIC_CHECK", "DimensionLabelIssue", "canonical_value_label", + "cross_package_value_label_remedy", + "declared_value_label_issues", "dimension_label_issues", "dimension_labels_by_id", "dimension_value_id", + "in_package_value_label_remedy", "label_facts", "register_size", "unused_label_declarations", diff --git a/chronicle/dimension_values/canonical_labels.json b/chronicle/dimension_values/canonical_labels.json index fb1bb166..c8fe0441 100644 --- a/chronicle/dimension_values/canonical_labels.json +++ b/chronicle/dimension_values/canonical_labels.json @@ -1,6 +1,69 @@ { "schema_version": "chronicle.dimension_value_labels.v1", - "description": "One label per dimension value (chronicle#376). Every fact built from a source package carries the label listed here for a value in dimension_value_labels and, for its groupby value, layout.groupby_value_label; the package's own wording, where it differs, is kept in publisher_dimension_value_labels. A value enters this register when two or more packages emit it or when its packages word it more than one way. A value no entry covers takes its package's label, and build-bundle refuses it if two packages label it differently.", + "description": "One label per dimension value (chronicle#376). Every fact built from a source package carries the label listed here for a value in dimension_value_labels and, for its groupby value, layout.groupby_value_label; the package's own wording, where it differs, is kept in package_dimension_value_labels. A value enters this register when two or more packages emit it or when its packages word it more than one way. These are Chronicle's display labels, not a publisher's file: a value with one wording keeps it, and a value worded more than one way takes the label chosen in scripts/build_dimension_value_register.py. A package may not declare a different label for a listed value. A value no entry covers takes its package's label, and build-bundle refuses it if two packages label it differently.", + "sources": { + "script": "scripts/build_dimension_value_register.py", + "packages": "chronicle.bundle.DEFAULT_BUNDLE_SOURCES", + "built_at_chronicle_commit": "aa288c3", + "default_build_year": 2023, + "build_years": { + "census-acs-s0101-congressional-district-age-2024": 2024, + "census-acs-s0101-national-age-2024": 2024, + "census-acs-s0101-state-age-2024": 2024, + "census-acs-s2201-congressional-district-snap-2024": 2024, + "cms-aca-effectuated-enrollment-2022": 2022, + "cms-aca-oep-state-level": 2024, + "cms-aca-oep-state-level-2022": 2022, + "cms-aca-oep-state-level-2025": 2025, + "jct-obbba-revenue-estimates-2025": 2026, + "jct-tax-expenditures-2024": 2024 + }, + "chosen_labels": [ + "age=85_plus", + "age=age_20", + "age=age_21", + "age=age_25_to_29", + "age=age_30_to_34", + "age=age_35_to_39", + "age=age_40_to_44", + "age_band=30 to 39", + "age_band=40 to 49", + "early_year_award_line=new_returning_total_returning_31_33_35", + "groeipakket.component=pleegzorgtoeslag", + "groeipakket.component=sociale_toeslag", + "groeipakket.component=wezentoeslag", + "groeipakket.component=zorgtoeslag", + "household_type=couple_3_plus_children_households", + "measure=country_total", + "na_item=belgium_household_disposable_income_gross", + "na_item=belgium_household_mixed_income_gross", + "na_item=belgium_household_operating_surplus_gross", + "ons.coicop=coicop_01", + "ons.coicop=coicop_02", + "ons.coicop=coicop_04", + "ons.coicop=coicop_05", + "ons.coicop=coicop_11", + "ons.coicop=coicop_12", + "ons.household_type=couple_3_plus_children_households", + "person.age_band=65_plus", + "property_type=detached", + "property_type=semi_detached", + "property_type=terraced", + "quant_inc=d1", + "quant_inc=d2", + "quant_inc=d3", + "quant_inc=d4", + "quant_inc=d5", + "quant_inc=d6", + "quant_inc=d7", + "quant_inc=d8", + "quant_inc=d9", + "sex=female", + "sex=male", + "us:statutes/26/62#adjusted_gross_income=all", + "us:statutes/26/62#adjusted_gross_income=under_1" + ] + }, "labels": { "Age": { "0 - 4": "0 - 4", @@ -4376,7 +4439,7 @@ }, "ons.coicop": { "coicop_01": "Food and non-alcoholic beverages", - "coicop_02": "Alcoholic beverages and tobacco", + "coicop_02": "Alcohol, tobacco and narcotics", "coicop_04": "Housing, water, electricity, gas and other fuel", "coicop_05": "Furnishings, household equipment and routine maintenance of the house", "coicop_11": "Restaurants and hotels", diff --git a/chronicle/epoch.py b/chronicle/epoch.py index 3662e19e..f6549d0f 100644 --- a/chronicle/epoch.py +++ b/chronicle/epoch.py @@ -169,7 +169,7 @@ def key_for_epoch(self, key: str, epoch: Epoch) -> str: # where Chronicle's register carries it, the publisher's own text moving to an # optional geography.publisher_name, and v5 (chronicle#376) is v4 with one # label per dimension value where Chronicle's register lists it, the package's -# own wording moving to an optional publisher_dimension_value_labels. The pair +# own wording moving to an optional package_dimension_value_labels. The pair # above is the Ledger rename table, not this contract history, so only v1 and # v2 name a naming epoch. The row contract and the hash-key domains move independently: # EMIT_EPOCH still keeps every key Ledger-named. diff --git a/chronicle/fixtures/consumer_facts.jsonl b/chronicle/fixtures/consumer_facts.jsonl index d5900b3f..b9ce5b65 100644 --- a/chronicle/fixtures/consumer_facts.jsonl +++ b/chronicle/fixtures/consumer_facts.jsonl @@ -1,3 +1,3 @@ -{"aggregate_fact_key": "ledger.aggregate_fact.v2:13153fb8dfd73fa12d5715e9", "aggregation": {"method": "sum"}, "assertion": "observation", "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns, total"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year count irs soi individual income tax returns for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns, total", "measure_id": "return_count", "measure_label": "Number of returns", "measure_ordinal": 0, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "return_count", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:079f0697e3b090abf422781b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.return_count", "source_cell_keys": ["ledger.source_cell.v1:82f39b2a5aeab53c1b717928"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.individual_income_tax_returns", "source_measure_id": "return_count", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "count"}, "observed_measure_key": "ledger.observed_measure.v2:bc96277bae6ef31da4a5794c", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "publisher_dimension_value_labels": {"us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "schema_version": "chronicle.consumer_fact.v5", "semantic_fact_key": "ledger.semantic_fact.v2:1ae4c4f93f554ec7ca14d2ad", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 160602107, "value_type": "integer"} -{"aggregate_fact_key": "ledger.aggregate_fact.v2:36ce90001adb50e8e9758e4f", "aggregation": {"method": "sum"}, "assertion": "observation", "concept_alignment": {"authority": "ledger-us", "canonical_concept": "us:statutes/26/62#adjusted_gross_income", "concept_alignment_key": "ledger.concept_alignment.v2:2e0ac323f1b261710e4397a1", "evidence_notes": "IRS SOI Table 1.1 reports adjusted gross income for individual income tax returns; IRC section 62 defines adjusted gross income. This Ledger assertion treats the SOI AGI column as exactly adopting that legal concept for the tax-year source record.", "evidence_url": "https://uscode.house.gov/view.xhtml?req=(title:26%20section:62%20edition:prelim)", "legal_vintage": "tax_year_2023", "relation": "exact", "source_concept": "irs_soi.adjusted_gross_income"}, "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns, total"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year sum us:statutes/26/62#adjusted gross income for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns, total", "measure_id": "adjusted_gross_income", "measure_label": "Adjusted gross income", "measure_ordinal": 1, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "adjusted_gross_income", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:7f8e9d3c64fec2670b571c3b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.adjusted_gross_income", "source_cell_keys": ["ledger.source_cell.v1:c3d349e26856fb6224303675"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.adjusted_gross_income", "source_measure_id": "adjusted_gross_income", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "usd"}, "observed_measure_key": "ledger.observed_measure.v2:ac9c3cef252ef7ead0066cb1", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "publisher_dimension_value_labels": {"us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "schema_version": "chronicle.consumer_fact.v5", "semantic_fact_key": "ledger.semantic_fact.v2:b83082b8e69c1ddc6fdebed2", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 15286017359000, "value_type": "integer"} -{"aggregate_fact_key": "ledger.aggregate_fact.v2:0fa0e8c17cfe0ebd844c4f95", "aggregation": {"method": "sum"}, "assertion": "observation", "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns, total"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year sum irs soi total income tax for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns, total", "measure_id": "total_income_tax", "measure_label": "Total income tax", "measure_ordinal": 2, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "total_income_tax", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:ec48b42ac43674a2a2206b8b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.total_income_tax", "source_cell_keys": ["ledger.source_cell.v1:f38c48e6e2f23204eb997143"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.total_income_tax", "source_measure_id": "total_income_tax", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "usd"}, "observed_measure_key": "ledger.observed_measure.v2:c11ff13c2dfe8af0795a1194", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "publisher_dimension_value_labels": {"us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "schema_version": "chronicle.consumer_fact.v5", "semantic_fact_key": "ledger.semantic_fact.v2:f8b2801687c0ba560054ed36", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 2147909818000, "value_type": "integer"} +{"aggregate_fact_key": "ledger.aggregate_fact.v2:13153fb8dfd73fa12d5715e9", "aggregation": {"method": "sum"}, "assertion": "observation", "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns, total"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year count irs soi individual income tax returns for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns, total", "measure_id": "return_count", "measure_label": "Number of returns", "measure_ordinal": 0, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "return_count", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:079f0697e3b090abf422781b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.return_count", "source_cell_keys": ["ledger.source_cell.v1:82f39b2a5aeab53c1b717928"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.individual_income_tax_returns", "source_measure_id": "return_count", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "count"}, "observed_measure_key": "ledger.observed_measure.v2:bc96277bae6ef31da4a5794c", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "package_dimension_value_labels": {"us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "schema_version": "chronicle.consumer_fact.v5", "semantic_fact_key": "ledger.semantic_fact.v2:1ae4c4f93f554ec7ca14d2ad", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 160602107, "value_type": "integer"} +{"aggregate_fact_key": "ledger.aggregate_fact.v2:36ce90001adb50e8e9758e4f", "aggregation": {"method": "sum"}, "assertion": "observation", "concept_alignment": {"authority": "ledger-us", "canonical_concept": "us:statutes/26/62#adjusted_gross_income", "concept_alignment_key": "ledger.concept_alignment.v2:2e0ac323f1b261710e4397a1", "evidence_notes": "IRS SOI Table 1.1 reports adjusted gross income for individual income tax returns; IRC section 62 defines adjusted gross income. This Ledger assertion treats the SOI AGI column as exactly adopting that legal concept for the tax-year source record.", "evidence_url": "https://uscode.house.gov/view.xhtml?req=(title:26%20section:62%20edition:prelim)", "legal_vintage": "tax_year_2023", "relation": "exact", "source_concept": "irs_soi.adjusted_gross_income"}, "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns, total"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year sum us:statutes/26/62#adjusted gross income for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns, total", "measure_id": "adjusted_gross_income", "measure_label": "Adjusted gross income", "measure_ordinal": 1, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "adjusted_gross_income", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:7f8e9d3c64fec2670b571c3b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.adjusted_gross_income", "source_cell_keys": ["ledger.source_cell.v1:c3d349e26856fb6224303675"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.adjusted_gross_income", "source_measure_id": "adjusted_gross_income", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "usd"}, "observed_measure_key": "ledger.observed_measure.v2:ac9c3cef252ef7ead0066cb1", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "package_dimension_value_labels": {"us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "schema_version": "chronicle.consumer_fact.v5", "semantic_fact_key": "ledger.semantic_fact.v2:b83082b8e69c1ddc6fdebed2", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 15286017359000, "value_type": "integer"} +{"aggregate_fact_key": "ledger.aggregate_fact.v2:0fa0e8c17cfe0ebd844c4f95", "aggregation": {"method": "sum"}, "assertion": "observation", "dimension_labels": {"filing_status": "Filing status", "income_range": "Income range", "us:statutes/26/62#adjusted_gross_income": "Adjusted gross income"}, "dimension_set_key": "ledger.dimension_set.v2:a55266d8b1d53a8a7e49927d", "dimension_value_labels": {"filing_status": {"all": "All filing statuses"}, "income_range": {"all": "All income ranges"}, "us:statutes/26/62#adjusted_gross_income": {"all": "All returns, total"}}, "dimensions": {"filing_status": "all", "income_range": "all"}, "entity": {"name": "tax_unit", "role": "filing_unit"}, "geography": {"id": "0100000US", "level": "country", "name": "United States", "vintage": "2020_census"}, "label": "United States 2023 tax year sum irs soi total income tax for tax unit (filing status=all, income range=all) [irs_soi Publication 1304 Table 1.1 23in11si.xls tax_year_2023]", "layout": {"groupby_dimension": "us:statutes/26/62#adjusted_gross_income", "groupby_dimension_label": "Adjusted gross income", "groupby_ordinal": 0, "groupby_value_id": "all", "groupby_value_label": "All returns, total", "measure_id": "total_income_tax", "measure_label": "Total income tax", "measure_ordinal": 2, "record_set_id": "irs_soi.ty2023.table_1_1", "record_set_spec_hash": "a1ecc4d67a3d281548a4660e", "record_set_spec_id": "irs_soi.table_1_1.v1", "source_column_id": "total_income_tax", "source_row_id": "all", "table_record_kind": "total"}, "legacy_fact_key": "ledger.fact.v1:ec48b42ac43674a2a2206b8b", "lineage": {"source_record_id": "irs_soi.ty2023.table_1_1.all.total_income_tax", "source_cell_keys": ["ledger.source_cell.v1:f38c48e6e2f23204eb997143"], "source_row_keys": []}, "observed_measure": {"source_concept": "irs_soi.total_income_tax", "source_measure_id": "total_income_tax", "source_name": "irs_soi", "source_table": "Publication 1304 Table 1.1", "unit": "usd"}, "observed_measure_key": "ledger.observed_measure.v2:c11ff13c2dfe8af0795a1194", "period": {"type": "tax_year", "value": 2023}, "provenance_class": "administrative", "package_dimension_value_labels": {"us:statutes/26/62#adjusted_gross_income": {"all": "All returns"}}, "schema_version": "chronicle.consumer_fact.v5", "semantic_fact_key": "ledger.semantic_fact.v2:f8b2801687c0ba560054ed36", "source": {"extracted_at": "2026-05-04", "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", "raw_r2_bucket": "ledger-raw", "raw_r2_key": "raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "raw_r2_uri": "r2://ledger-raw/raw/irs_soi/soi-table-1-1/2023/842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17/23in11si.xls", "source_file": "23in11si.xls", "source_name": "irs_soi", "source_sha256": "842da11fe09123796c861e919d16011ef161338de1c172d39064bdfa666aca17", "source_size_bytes": 54272, "source_table": "Publication 1304 Table 1.1", "url": "https://www.irs.gov/pub/irs-soi/23in11si.xls", "vintage": "tax_year_2023"}, "source_release_key": "ledger.source_release.v2:e418de50f65dc3f88fbcdd6c", "source_series_key": "ledger.source_series.v2:ac7ef40c4f9adbd84a82f24c", "universe_constraint_set_key": "ledger.universe_constraint_set.v2:18df86d027cde55a3f217b77", "universe_constraints": {"domain": "all_individual_income_tax_returns"}, "value": 2147909818000, "value_type": "integer"} diff --git a/chronicle/source_package.py b/chronicle/source_package.py index 214a4e2d..f439acd3 100644 --- a/chronicle/source_package.py +++ b/chronicle/source_package.py @@ -34,9 +34,11 @@ build_label, ) from chronicle.dimension_labels import ( - ONE_LABEL_PER_VALUE, + REGISTER_DISPLAY_PATH, canonical_value_label, + declared_value_label_issues, dimension_value_id, + in_package_value_label_remedy, label_facts, unused_label_declarations, ) @@ -2366,7 +2368,7 @@ def _row_value_label_issues( Each row's ``label:`` becomes its facts' value label unless Chronicle's register lists the value or the package declares one label for it in ``dimension_value_labels``; the rows' wording is then kept as the - package's own, in ``publisher_dimension_value_labels``. Otherwise rows + package's own, in ``package_dimension_value_labels``. Otherwise rows that word one value differently would give its facts two labels, which ``build-bundle`` refuses. This finds it from the package file alone, at any build year. @@ -2398,7 +2400,7 @@ def _row_value_label_issues( message=( f"Dimension {dimension_id!r} value {value_id!r} is labelled " f"{sorted(labels)!r} by the rows of this package{where}. " - f"{ONE_LABEL_PER_VALUE}" + f"{in_package_value_label_remedy(dimension_id, value_id)}" ), record_set_id=record_sets_by_value[(dimension_id, value_id)][0], row_id=value_id, @@ -2408,6 +2410,38 @@ def _row_value_label_issues( ] +def _declared_value_label_register_issues( + declared_value_labels: Mapping[str, Mapping[str, Any]], + *, + package_path: Path | None = None, +) -> list[SourcePackageIssue]: + """Refuse a declaration that Chronicle's register contradicts (chronicle#376). + + Every fact takes the register's label for a value it lists, so a package + declaration with other text would never reach a fact and would mislead + whoever reads the package file. The register is the one source of those + labels; the package's rows keep their own wording. + """ + + where = f" in {_display_package_path(package_path)}" if package_path else "" + return [ + SourcePackageIssue( + code="declared_value_label_differs_from_register", + message=( + f"dimension_value_labels.{dimension_id}.{value_id} declares " + f"{declared!r}{where}, but Chronicle's register " + f"({REGISTER_DISPLAY_PATH}) labels this value {canonical!r}, " + "and every fact takes the register's label (chronicle#376). " + f"Change the declaration to {canonical!r}." + ), + row_id=value_id, + ) + for dimension_id, value_id, declared, canonical in ( + declared_value_label_issues(declared_value_labels) + ) + ] + + def _display_package_path(package_path: str | Path) -> str: """Return a package file's path relative to the repository when it can.""" @@ -2527,6 +2561,12 @@ def validate_source_package( package_path=package.package_path, ) ) + errors.extend( + _declared_value_label_register_issues( + package.dimension_value_labels or {}, + package_path=package.package_path, + ) + ) for record_set_id, indices in record_set_ids.items(): if len(indices) > 1: errors.append( diff --git a/chronicle/store.py b/chronicle/store.py index 2ae31a0a..9feb3e07 100644 --- a/chronicle/store.py +++ b/chronicle/store.py @@ -92,10 +92,10 @@ def fact_from_mapping(payload: dict[str, Any]) -> AggregateFact: "dimension_value_labels", {} ).items() }, - publisher_dimension_value_labels={ + package_dimension_value_labels={ dimension_id: dict(value_labels) for dimension_id, value_labels in payload.get( - "publisher_dimension_value_labels", {} + "package_dimension_value_labels", {} ).items() }, ) @@ -110,7 +110,7 @@ def fact_to_mapping(fact: AggregateFact) -> dict[str, Any]: for label_field in ( "dimension_labels", "dimension_value_labels", - "publisher_dimension_value_labels", + "package_dimension_value_labels", ): if not payload[label_field]: payload.pop(label_field) diff --git a/docs/agent-source-package-harness.md b/docs/agent-source-package-harness.md index a6a27c6b..59fe229d 100644 --- a/docs/agent-source-package-harness.md +++ b/docs/agent-source-package-harness.md @@ -864,13 +864,21 @@ more than one way. When a package is built, at any year, each value the register lists takes the register's label in `dimension_value_labels` and, for the groupby value, `layout.groupby_value_label`; the package's own wording (the row's `label:`, else its declaration, else its source-row text) is kept in -`publisher_dimension_value_labels` where it differs, the way -`geography.publisher_name` keeps a publisher's area name (chronicle#281). So a -row's `label:` stays the publisher's text, and the row's guards still check it -against the source: the IRS filing-season week-47 table heads its below-$1 -column "No adjusted gross income and deficit", the package's rows say so, and +`package_dimension_value_labels` where it differs. That field holds the +package's wording, not necessarily the publisher's, and nothing checks it +against the publisher's file. The guards compare a row's `expected_row_header` +and `expected_column_header`, which are fields separate from its `label:`, with +the source cells (a row's `label:` is compared with the row-header cell only +when the row sets no `expected_row_header`). So a row's `label:` is the +publisher's text only where it equals the text a guard compares: the IRS +filing-season week-47 table heads its below-$1 column "No adjusted gross income +and deficit", the rows' `label:` and `expected_column_header` both say so, and their facts carry the register's "No adjusted gross income" with the IRS text -beside it. +beside it. Elsewhere the package's wording is often Chronicle's: a parser's +constant (Census B01001's "Female 20 years"), a value id in an extract Chronicle +prepared (Statbel's "65_plus"), or a package author's phrase (Eurostat's "First +income decile cut-point", whose guard checks the code `D1`). The publisher's +cells stay reachable through each fact's lineage. A value the register does not list is one only one package emits, so its package's label is its one label. A value labelled two ways is an error, with no @@ -885,11 +893,25 @@ list of exceptions: `conflicting_value_label_across_packages`; add the value to the register with one label. +A package may not declare a label in `dimension_value_labels` for a value the +register lists unless it is the register's label: `validate-package` reports +`declared_value_label_differs_from_register`, so the register is the one source +of those labels. + A new package that emits a value another package already emits should check the register: the value is either listed, and takes the listed label, or needs an entry. To choose a register label for a value its packages word differently, take the wording most packages use, ties going to the national statistics -office's wording, and keep each package's text in its rows. +office's wording, without footnote markers or words that name the measure, and +keep each package's text in its rows. The register is generated by +`scripts/build_dimension_value_register.py`, which holds the chosen labels; +add a chosen label there, rerun it, and commit both files. Its `--static-check` +mode compares the register with the package files without building them (every +value two packages' files name must be listed, every listed value must be named +by a package file, and no declaration may contradict the register), and +`tests/test_dimension_value_register.py` runs it in CI. The script without +`--static-check` builds every default-bundle package at its build year; with +`--check` it fails if the register differs from what those builds give. Every fact also carries the publisher's name for its geography (chronicle#266), taken from the record set's or the row's `geography_name`. `build-bundle` diff --git a/docs/schemas/consumer_fact.v5.schema.json b/docs/schemas/consumer_fact.v5.schema.json index d267ef80..59ff1389 100644 --- a/docs/schemas/consumer_fact.v5.schema.json +++ b/docs/schemas/consumer_fact.v5.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://policyengine.org/chronicle/schemas/consumer_fact.v5.schema.json", "title": "Chronicle consumer fact contract row", - "description": "The v4 contract row with one label per dimension value (chronicle#376). Where Chronicle's register of dimension value labels carries a value, dimension_value_labels and layout.groupby_value_label carry the register's label, and publisher_dimension_value_labels carries the source package's own wording of that value when it differs; elsewhere the labels are the package's, as in v4. Both stay display metadata: neither enters a fact key, and a v4 row restamped with this schema_version is a valid v5 row.", + "description": "The v4 contract row with one label per dimension value (chronicle#376). Where Chronicle's register of dimension value labels carries a value, dimension_value_labels and layout.groupby_value_label carry the register's label, and package_dimension_value_labels carries the source package's own wording of that value when it differs, which is not checked against the publisher's file and may be text Chronicle wrote; elsewhere the labels are the package's, as in v4. Both stay display metadata: neither enters a fact key, and a v4 row restamped with this schema_version is a valid v5 row.", "type": "object", "additionalProperties": false, "required": [ @@ -241,9 +241,9 @@ } } }, - "publisher_dimension_value_labels": { + "package_dimension_value_labels": { "type": "object", - "description": "The source package's own wording of a dimension value, keyed dimension id then value id like dimension_value_labels: the publisher's row or column text the package reads, present only where it differs from the label in dimension_value_labels.", + "description": "The source package's own wording of a dimension value, present only where it differs from the label this row carries for that value: the package row's label, else the package's dimension_value_labels declaration, else the text in the matching column of the parsed source row. This text is not checked against the publisher's file. It may be the publisher's wording (for example a row label that the row's guard compares with a header cell) or text Chronicle wrote (a parser's constant, a value id in an extract Chronicle prepared, or a package author's phrase); the publisher's own cells are reached through lineage.source_cell_keys. Keyed dimension id then value id like dimension_value_labels; the groupby value of a groupby axis that is also a filter with a different value is keyed by layout.groupby_value_id and differs from layout.groupby_value_label.", "additionalProperties": { "type": "object", "additionalProperties": { diff --git a/packages/dft/nts0601_age_mode_trips_2025/source_package.yaml b/packages/dft/nts0601_age_mode_trips_2025/source_package.yaml index 1f48ee09..0002cce2 100644 --- a/packages/dft/nts0601_age_mode_trips_2025/source_package.yaml +++ b/packages/dft/nts0601_age_mode_trips_2025/source_package.yaml @@ -24,8 +24,10 @@ dimension_value_labels: '0 to 16': 'Age 0 to 16' '17 to 20': 'Age 17 to 20' '21 to 29': 'Age 21 to 29' - '30 to 39': 'Age 30 to 39' - '40 to 49': 'Age 40 to 49' + # Chronicle's register gives these two labels, DfT's own row headers, + # because ONS ASHE packages emit the same values (chronicle#376). + '30 to 39': '30 to 39' + '40 to 49': '40 to 49' '50 to 59': 'Age 50 to 59' '60 to 69': 'Age 60 to 69' '70 and over': 'Age 70 and over' diff --git a/packages/irs_soi/filing_season_week47_2024/source_package.yaml b/packages/irs_soi/filing_season_week47_2024/source_package.yaml index 271c1efa..8d101291 100644 --- a/packages/irs_soi/filing_season_week47_2024/source_package.yaml +++ b/packages/irs_soi/filing_season_week47_2024/source_package.yaml @@ -150,7 +150,7 @@ record_sets: # income": adjusted gross income below $1, deficits included (column C's # AGI is -$120.2 billion, and the 16 columns add up to all returns). # Facts take Chronicle's register label for under_1 and keep this text - # in publisher_dimension_value_labels (chronicle#376). + # in package_dimension_value_labels (chronicle#376). - value_id: under_1 label: No adjusted gross income and deficit ordinal: 0 diff --git a/packages/nisra/mye_2025_lgd_single_year_age/source_package.yaml b/packages/nisra/mye_2025_lgd_single_year_age/source_package.yaml index 46927307..7cd66a76 100644 --- a/packages/nisra/mye_2025_lgd_single_year_age/source_package.yaml +++ b/packages/nisra/mye_2025_lgd_single_year_age/source_package.yaml @@ -7,8 +7,10 @@ dimension_labels: age: Age dimension_value_labels: sex: - female: Females - male: Males + # Chronicle's register gives these labels (chronicle#376); NISRA's own + # "Females" and "Males" stay in the source rows. + female: Female + male: Male age: 90_plus: 90 and over artifact: diff --git a/packages/nrs/mye_2025_council_area/source_package.yaml b/packages/nrs/mye_2025_council_area/source_package.yaml index 1e13d0bf..e2eb496c 100644 --- a/packages/nrs/mye_2025_council_area/source_package.yaml +++ b/packages/nrs/mye_2025_council_area/source_package.yaml @@ -7,8 +7,10 @@ dimension_labels: sex: Sex dimension_value_labels: sex: - female: Females - male: Males + # Chronicle's register gives these labels (chronicle#376); NRS's own + # "Females" and "Males" are still compared by the guard cells below. + female: Female + male: Male age: 90_plus: 90 and over artifact: diff --git a/packages/nrs/pcon24_population_by_age_2024_revised_2026/source_package.yaml b/packages/nrs/pcon24_population_by_age_2024_revised_2026/source_package.yaml index 62ece4f6..ab01fa76 100644 --- a/packages/nrs/pcon24_population_by_age_2024_revised_2026/source_package.yaml +++ b/packages/nrs/pcon24_population_by_age_2024_revised_2026/source_package.yaml @@ -7,8 +7,10 @@ dimension_labels: sex: Sex dimension_value_labels: sex: - female: Females - male: Males + # Chronicle's register gives these labels (chronicle#376); NRS's own + # "Females" and "Males" are still compared by the guard cells below. + female: Female + male: Male age: 90_plus: 90 and over artifact: diff --git a/packages/ons/hpssa_pcon24_year_ending_march_2026/source_package.yaml b/packages/ons/hpssa_pcon24_year_ending_march_2026/source_package.yaml index e41df0a7..947bf9d0 100644 --- a/packages/ons/hpssa_pcon24_year_ending_march_2026/source_package.yaml +++ b/packages/ons/hpssa_pcon24_year_ending_march_2026/source_package.yaml @@ -35,9 +35,10 @@ dimension_labels: property_type: Property Type dimension_value_labels: property_type: - detached: Detached houses - semi_detached: Semi-detached houses - terraced: Terraced houses + # Chronicle's register gives these three labels (chronicle#376). + detached: Detached + semi_detached: Semi detached + terraced: Terraced flat_maisonette: Flats/maisonettes artifact: source_name: ons diff --git a/packages/ons/pipr_rents_by_area_august_2026/source_package.yaml b/packages/ons/pipr_rents_by_area_august_2026/source_package.yaml index ee43fd0c..116ba328 100644 --- a/packages/ons/pipr_rents_by_area_august_2026/source_package.yaml +++ b/packages/ons/pipr_rents_by_area_august_2026/source_package.yaml @@ -13,9 +13,11 @@ dimension_value_labels: three bed: three bed four or more bed: four or more bed property_type: - detached: detached + # Chronicle's register gives detached and terraced their labels + # (chronicle#376). + detached: Detached semidetached: semidetached - terraced: terraced + terraced: Terraced flat maisonette: flat maisonette artifact: source_name: ons diff --git a/policyengine_chronicle/consumer.py b/policyengine_chronicle/consumer.py index 7f05f30c..786c9658 100644 --- a/policyengine_chronicle/consumer.py +++ b/policyengine_chronicle/consumer.py @@ -288,7 +288,7 @@ def _recompute_aggregate_fact_key(row: dict[str, Any]) -> str: Labels are display metadata and never enter the payload: ``dimension_labels``, ``dimension_value_labels``, ``layout`` and the v5 - ``publisher_dimension_value_labels`` (chronicle#376) are not read here, and + ``package_dimension_value_labels`` (chronicle#376) are not read here, and the geography's display names are dropped by :func:`_geography_key_payload`. """ declared_key = row.get("aggregate_fact_key") diff --git a/policyengine_chronicle/schemas/consumer_fact.v5.schema.json b/policyengine_chronicle/schemas/consumer_fact.v5.schema.json index d267ef80..59ff1389 100644 --- a/policyengine_chronicle/schemas/consumer_fact.v5.schema.json +++ b/policyengine_chronicle/schemas/consumer_fact.v5.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://policyengine.org/chronicle/schemas/consumer_fact.v5.schema.json", "title": "Chronicle consumer fact contract row", - "description": "The v4 contract row with one label per dimension value (chronicle#376). Where Chronicle's register of dimension value labels carries a value, dimension_value_labels and layout.groupby_value_label carry the register's label, and publisher_dimension_value_labels carries the source package's own wording of that value when it differs; elsewhere the labels are the package's, as in v4. Both stay display metadata: neither enters a fact key, and a v4 row restamped with this schema_version is a valid v5 row.", + "description": "The v4 contract row with one label per dimension value (chronicle#376). Where Chronicle's register of dimension value labels carries a value, dimension_value_labels and layout.groupby_value_label carry the register's label, and package_dimension_value_labels carries the source package's own wording of that value when it differs, which is not checked against the publisher's file and may be text Chronicle wrote; elsewhere the labels are the package's, as in v4. Both stay display metadata: neither enters a fact key, and a v4 row restamped with this schema_version is a valid v5 row.", "type": "object", "additionalProperties": false, "required": [ @@ -241,9 +241,9 @@ } } }, - "publisher_dimension_value_labels": { + "package_dimension_value_labels": { "type": "object", - "description": "The source package's own wording of a dimension value, keyed dimension id then value id like dimension_value_labels: the publisher's row or column text the package reads, present only where it differs from the label in dimension_value_labels.", + "description": "The source package's own wording of a dimension value, present only where it differs from the label this row carries for that value: the package row's label, else the package's dimension_value_labels declaration, else the text in the matching column of the parsed source row. This text is not checked against the publisher's file. It may be the publisher's wording (for example a row label that the row's guard compares with a header cell) or text Chronicle wrote (a parser's constant, a value id in an extract Chronicle prepared, or a package author's phrase); the publisher's own cells are reached through lineage.source_cell_keys. Keyed dimension id then value id like dimension_value_labels; the groupby value of a groupby axis that is also a filter with a different value is keyed by layout.groupby_value_id and differs from layout.groupby_value_label.", "additionalProperties": { "type": "object", "additionalProperties": { diff --git a/scripts/build_dimension_value_register.py b/scripts/build_dimension_value_register.py new file mode 100644 index 00000000..4997122b --- /dev/null +++ b/scripts/build_dimension_value_register.py @@ -0,0 +1,467 @@ +"""Build and check Chronicle's register of dimension value labels (chronicle#376). + +``chronicle/dimension_values/canonical_labels.json`` gives one label to every +(dimension id, value id) that two or more default-bundle packages emit, and to +every value its packages word more than one way. Each fact a package builds +takes the register's label for a value it lists, so a consumer that merges +packages built for different years sees one label per value. + +The register is Chronicle's display text, not a publisher's file. A value with +one wording keeps it. A value worded more than one way takes the label in +``CHOSEN_LABELS`` below, chosen by the rule in +``docs/agent-source-package-harness.md`` (the wording most packages use, a tie +going to the national statistics office's wording) or by the maintainer. + +Usage: + + # Rebuild every default-bundle package at its build year and rewrite the + # register (slow: every package is built). + uv run python scripts/build_dimension_value_register.py + + # The same, but fail instead of writing when the register differs. + uv run python scripts/build_dimension_value_register.py --check + + # Read already-built packages (directories of /consumer_facts.jsonl, + # as build-suite writes them) instead of building them. + uv run python scripts/build_dimension_value_register.py \\ + --from-consumer-facts BUILDS_DIR [BUILDS_DIR ...] [--check] + + # Compare the register with the package files only, without building: + # every value two packages' files name must be listed, every listed value + # must be named by some package file, and no package may declare a label + # the register contradicts. Runs in CI through + # tests/test_dimension_value_register.py. + uv run python scripts/build_dimension_value_register.py --static-check + +The static check reads groupby rows' value ids, filters and declarations from +each package file compiled at its build year. It cannot see a value a package +only takes from its source rows at build time, so the full ``--check`` is the +complete comparison. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from collections import defaultdict +from collections.abc import Iterable, Mapping +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from chronicle.dimension_labels import ( # noqa: E402 + REGISTER_DISPLAY_PATH, + REGISTER_PATH, + dimension_value_id, +) + +SCRIPT_DISPLAY_PATH = "scripts/build_dimension_value_register.py" +SCHEMA_VERSION = "chronicle.dimension_value_labels.v1" +#: The Chronicle commit whose package builds the register was generated from. +BUILT_AT_CHRONICLE_COMMIT = "aa288c3" +DEFAULT_BUILD_YEAR = 2023 +#: Default-bundle packages that do not build at DEFAULT_BUILD_YEAR. +BUILD_YEARS = { + "census-acs-s0101-congressional-district-age-2024": 2024, + "census-acs-s0101-national-age-2024": 2024, + "census-acs-s0101-state-age-2024": 2024, + "census-acs-s2201-congressional-district-snap-2024": 2024, + "cms-aca-effectuated-enrollment-2022": 2022, + "cms-aca-oep-state-level": 2024, + "cms-aca-oep-state-level-2022": 2022, + "cms-aca-oep-state-level-2025": 2025, + "jct-obbba-revenue-estimates-2025": 2026, + "jct-tax-expenditures-2024": 2024, +} + +AGI = "us:statutes/26/62#adjusted_gross_income" +#: The label of each value its packages word more than one way. The three US +#: labels are the maintainer's choice; the other 40 were proposed in +#: chronicle#377 and approved on 2026-10-21, coicop_02 with ONS's overview +#: wording after review. +CHOSEN_LABELS: dict[tuple[str, str], str] = { + (AGI, "under_1"): "No adjusted gross income", + (AGI, "all"): "All returns, total", + ("age", "85_plus"): "Age 85 plus", + ("age", "age_20"): "Age 20", + ("age", "age_21"): "Age 21", + ("age", "age_25_to_29"): "Age 25 to 29", + ("age", "age_30_to_34"): "Age 30 to 34", + ("age", "age_35_to_39"): "Age 35 to 39", + ("age", "age_40_to_44"): "Age 40 to 44", + ("age_band", "30 to 39"): "30 to 39", + ("age_band", "40 to 49"): "40 to 49", + ( + "early_year_award_line", + "new_returning_total_returning_31_33_35", + ): "Total returning [31][33][35]", + ("groeipakket.component", "pleegzorgtoeslag"): "Foster care supplement", + ("groeipakket.component", "sociale_toeslag"): "Social supplement", + ("groeipakket.component", "wezentoeslag"): "Orphan supplement", + ("groeipakket.component", "zorgtoeslag"): "Care supplement", + ( + "household_type", + "couple_3_plus_children_households", + ): "3 or more dependent children", + ( + "ons.household_type", + "couple_3_plus_children_households", + ): "3 or more dependent children", + ("measure", "country_total"): "Country total", + ( + "na_item", + "belgium_household_disposable_income_gross", + ): "Disposable income, gross (B.6G)", + ("na_item", "belgium_household_mixed_income_gross"): "Mixed income, gross (B.3G)", + ( + "na_item", + "belgium_household_operating_surplus_gross", + ): "Operating surplus, gross (B.2G)", + # ONS's detailed-worksheet wording without its footnote marker, except + # where noted. + ("ons.coicop", "coicop_01"): "Food and non-alcoholic beverages", + # ONS's overview (0CN) wording: the total, CDID ADFL, includes narcotics + # (ADFL = ADFM + ADFN + MNC2), which the 02CN column header leaves out. + ("ons.coicop", "coicop_02"): "Alcohol, tobacco and narcotics", + ("ons.coicop", "coicop_04"): "Housing, water, electricity, gas and other fuel", + # The overview (0CN) wording; 05CN abbreviates it. + ( + "ons.coicop", + "coicop_05", + ): "Furnishings, household equipment and routine maintenance of the house", + # The overview (0CN) wording; 11CN writes "Restaurant and hotels". + ("ons.coicop", "coicop_11"): "Restaurants and hotels", + ("ons.coicop", "coicop_12"): "Miscellaneous goods and services", + ("person.age_band", "65_plus"): "65+", + ("property_type", "detached"): "Detached", + ("property_type", "semi_detached"): "Semi detached", + ("property_type", "terraced"): "Terraced", + ("quant_inc", "d1"): "First income decile", + ("quant_inc", "d2"): "Second income decile", + ("quant_inc", "d3"): "Third income decile", + ("quant_inc", "d4"): "Fourth income decile", + ("quant_inc", "d5"): "Fifth income decile", + ("quant_inc", "d6"): "Sixth income decile", + ("quant_inc", "d7"): "Seventh income decile", + ("quant_inc", "d8"): "Eighth income decile", + ("quant_inc", "d9"): "Ninth income decile", + ("sex", "female"): "Female", + ("sex", "male"): "Male", +} + +DESCRIPTION = ( + "One label per dimension value (chronicle#376). Every fact built from a " + "source package carries the label listed here for a value in " + "dimension_value_labels and, for its groupby value, " + "layout.groupby_value_label; the package's own wording, where it differs, " + "is kept in package_dimension_value_labels. A value enters this register " + "when two or more packages emit it or when its packages word it more than " + "one way. These are Chronicle's display labels, not a publisher's file: a " + "value with one wording keeps it, and a value worded more than one way " + "takes the label chosen in scripts/build_dimension_value_register.py. A " + "package may not declare a different label for a listed value. A value no " + "entry covers takes its package's label, and build-bundle refuses it if " + "two packages label it differently." +) + +ValueKey = tuple[str, str] +#: (dimension id, value id) -> wording -> package id -> fact count. +Wordings = dict[ValueKey, dict[str, dict[str, int]]] + + +def build_year(package_id: str) -> int: + """Return the year the register builds *package_id* at.""" + + return BUILD_YEARS.get(package_id, DEFAULT_BUILD_YEAR) + + +def wordings_by_value( + rows_by_package: Mapping[str, Iterable[Mapping[str, Any]]], +) -> Wordings: + """Count each package's own wording of each value in its consumer rows. + + A row built before the register carries its package's wording as the + label; a row built after carries the register's label, with the package's + wording in ``package_dimension_value_labels`` where it differs. A value id + equal to the row's geography id names the area, not a dimension value. + """ + + found: Wordings = defaultdict(lambda: defaultdict(lambda: defaultdict(int))) + for package_id, rows in rows_by_package.items(): + for row in rows: + geography_id = str((row.get("geography") or {}).get("id") or "").casefold() + package_wording = row.get("package_dimension_value_labels") or {} + seen: set[tuple[str, str, str]] = set() + for dimension_id, value_labels in ( + row.get("dimension_value_labels") or {} + ).items(): + for value_id, label in value_labels.items(): + wording = (package_wording.get(dimension_id) or {}).get( + value_id, label + ) + seen.add((dimension_id, str(value_id), str(wording).strip())) + layout = row.get("layout") or {} + groupby = layout.get("groupby_dimension") + if groupby and layout.get("groupby_value_id") is not None: + value_id = dimension_value_id(layout["groupby_value_id"]) + wording = (package_wording.get(groupby) or {}).get( + value_id, layout.get("groupby_value_label") or "" + ) + seen.add((groupby, value_id, str(wording).strip())) + for dimension_id, value_id, wording in seen: + if not wording or value_id.casefold() == geography_id: + continue + found[(dimension_id, value_id)][wording][package_id] += 1 + return { + key: {wording: dict(packages) for wording, packages in wordings.items()} + for key, wordings in found.items() + } + + +def register_labels( + wordings: Wordings, + *, + chosen: Mapping[ValueKey, str] = CHOSEN_LABELS, +) -> dict[str, dict[str, str]]: + """Return the register's labels for the package wordings counted.""" + + labels: dict[str, dict[str, str]] = defaultdict(dict) + unresolved = [] + for (dimension_id, value_id), by_wording in sorted(wordings.items()): + packages = {package for counts in by_wording.values() for package in counts} + if len(packages) < 2 and len(by_wording) < 2: + continue + if (dimension_id, value_id) in chosen: + label = chosen[(dimension_id, value_id)] + elif len(by_wording) == 1: + label = next(iter(by_wording)) + else: + unresolved.append(f"{dimension_id}={value_id}: {sorted(by_wording)}") + continue + labels[dimension_id][value_id] = label + if unresolved: + raise SystemExit( + "Values worded more than one way need a label in CHOSEN_LABELS " + f"({SCRIPT_DISPLAY_PATH}), chosen by the rule in " + "docs/agent-source-package-harness.md:\n " + "\n ".join(unresolved) + ) + return { + dimension_id: dict(sorted(values.items())) + for dimension_id, values in sorted(labels.items()) + } + + +def register_payload(labels: Mapping[str, Mapping[str, str]]) -> dict[str, Any]: + """Return the register file's content for *labels*.""" + + return { + "schema_version": SCHEMA_VERSION, + "description": DESCRIPTION, + "sources": { + "script": SCRIPT_DISPLAY_PATH, + "packages": "chronicle.bundle.DEFAULT_BUNDLE_SOURCES", + "built_at_chronicle_commit": BUILT_AT_CHRONICLE_COMMIT, + "default_build_year": DEFAULT_BUILD_YEAR, + "build_years": dict(sorted(BUILD_YEARS.items())), + "chosen_labels": sorted( + f"{dimension_id}={value_id}" for dimension_id, value_id in CHOSEN_LABELS + ), + }, + "labels": { + dimension_id: dict(sorted(values.items())) + for dimension_id, values in sorted(labels.items()) + }, + } + + +def register_text(labels: Mapping[str, Mapping[str, str]]) -> str: + return json.dumps(register_payload(labels), indent=2, ensure_ascii=False) + "\n" + + +def _stored_rows(builds_dirs: Iterable[Path]) -> dict[str, list[dict[str, Any]]]: + rows_by_package: dict[str, list[dict[str, Any]]] = {} + for builds_dir in builds_dirs: + for package_dir in sorted(Path(builds_dir).iterdir()): + facts_path = package_dir / "consumer_facts.jsonl" + if not facts_path.is_file(): + continue + with facts_path.open(encoding="utf-8") as stream: + rows_by_package[package_dir.name] = [ + json.loads(line) for line in stream if line.strip() + ] + return rows_by_package + + +def _built_rows() -> dict[str, list[dict[str, Any]]]: + from chronicle.bundle import DEFAULT_BUNDLE_SOURCES + from chronicle.consumer_contract import consumer_fact_rows + from chronicle.source_package import load_source_package + + rows_by_package = {} + for source in DEFAULT_BUNDLE_SOURCES: + package = load_source_package(source) + year = build_year(package.package_id) + facts = package.build_facts(year, cells=package.build_source_cells(year)) + rows_by_package[package.package_id] = consumer_fact_rows(facts) + print(f"built {package.package_id} at {year}", file=sys.stderr) + return rows_by_package + + +# The static check: package files only. + + +def static_package_values() -> tuple[ + dict[ValueKey, set[str]], dict[tuple[str, str, str], str] +]: + """Return the values each package file names, and its declarations. + + Each default-bundle package is compiled at its build year without reading + its source file. A value is a groupby row's value id or a filter value + (record set, row or measure), skipping a value equal to the row's + geography id; declarations are the package's ``dimension_value_labels``. + """ + + from chronicle.bundle import DEFAULT_BUNDLE_SOURCES + from chronicle.source_package import load_source_package + + emitters: dict[ValueKey, set[str]] = defaultdict(set) + declarations: dict[tuple[str, str, str], str] = {} + for source in DEFAULT_BUNDLE_SOURCES: + package = load_source_package(source) + package_id = package.package_id + record_sets = package._compile_record_set_specs(build_year(package_id)) + for record_set in record_sets: + groupby = (record_set.groupby_dimension or "").strip() + for row in record_set.rows: + geography_id = str( + row.geography_id or record_set.geography_id or "" + ).casefold() + named = [] + if groupby and row.value_id is not None: + named.append((groupby, row.value_id)) + named.extend((record_set.shared_filters or {}).items()) + named.extend((row.filters or {}).items()) + for measure in record_set.measures: + named.extend((measure.filters or {}).items()) + for dimension_id, value in named: + if value is None: + continue + value_id = dimension_value_id(value) + if value_id.casefold() == geography_id: + continue + emitters[(str(dimension_id), value_id)].add(package_id) + for dimension_id, value_labels in ( + package.dimension_value_labels or {} + ).items(): + for value_id, label in (value_labels or {}).items(): + emitters[(str(dimension_id), str(value_id))].add(package_id) + declarations[(package_id, str(dimension_id), str(value_id))] = str( + label + ).strip() + return dict(emitters), declarations + + +def register_problems( + *, + register: Mapping[str, Mapping[str, str]], + emitters: Mapping[ValueKey, set[str]], + declarations: Mapping[tuple[str, str, str], str], +) -> list[str]: + """Compare the register with the values and declarations package files name.""" + + listed = { + (dimension_id, value_id) + for dimension_id, values in register.items() + for value_id in values + } + problems = [] + for (dimension_id, value_id), packages in sorted(emitters.items()): + if len(packages) >= 2 and (dimension_id, value_id) not in listed: + problems.append( + f"{dimension_id}={value_id} is emitted by {len(packages)} packages " + f"({', '.join(sorted(packages))}) but the register does not list " + f"it; add it to {REGISTER_DISPLAY_PATH} with one label." + ) + for dimension_id, value_id in sorted(listed - set(emitters)): + problems.append( + f"{dimension_id}={value_id} is in the register but no package emits " + f"it; remove it from {REGISTER_DISPLAY_PATH}." + ) + for (package_id, dimension_id, value_id), declared in sorted(declarations.items()): + canonical = register.get(dimension_id, {}).get(value_id) + if canonical is not None and declared != canonical: + problems.append( + f"{package_id} declares dimension_value_labels.{dimension_id}." + f"{value_id} {declared!r}, but the register labels it " + f"{canonical!r}; change the declaration to {canonical!r}." + ) + return problems + + +def committed_register() -> dict[str, dict[str, str]]: + return json.loads(REGISTER_PATH.read_text(encoding="utf-8"))["labels"] + + +def static_check_problems() -> list[str]: + """Run the static check against the committed register.""" + + emitters, declarations = static_package_values() + return register_problems( + register=committed_register(), + emitters=emitters, + declarations=declarations, + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0]) + parser.add_argument("--check", action="store_true") + parser.add_argument("--static-check", action="store_true") + parser.add_argument("--from-consumer-facts", nargs="+", type=Path) + parser.add_argument("--output", type=Path, default=REGISTER_PATH) + arguments = parser.parse_args(argv) + + if arguments.static_check: + problems = static_check_problems() + for problem in problems: + print(problem, file=sys.stderr) + print( + f"static check: {len(problems)} problem(s) in {REGISTER_DISPLAY_PATH}", + file=sys.stderr, + ) + return 1 if problems else 0 + + rows_by_package = ( + _stored_rows(arguments.from_consumer_facts) + if arguments.from_consumer_facts + else _built_rows() + ) + text = register_text(register_labels(wordings_by_value(rows_by_package))) + if arguments.check: + current = REGISTER_PATH.read_text(encoding="utf-8") + if current != text: + print( + f"{REGISTER_DISPLAY_PATH} differs from the register the packages " + f"give; rerun {SCRIPT_DISPLAY_PATH} without --check and review " + "the change.", + file=sys.stderr, + ) + return 1 + print(f"{REGISTER_DISPLAY_PATH} matches the packages.", file=sys.stderr) + return 0 + arguments.output.write_text(text, encoding="utf-8") + labels = json.loads(text)["labels"] + print( + f"wrote {sum(len(values) for values in labels.values())} values in " + f"{len(labels)} dimensions to {arguments.output}", + file=sys.stderr, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_chronicle_core.py b/tests/test_chronicle_core.py index b8ca10a1..53692820 100644 --- a/tests/test_chronicle_core.py +++ b/tests/test_chronicle_core.py @@ -472,10 +472,11 @@ def test_frozen_fixture_bytes_are_unchanged(): # because the sample's own package now declares its dimension # labels, chronicle#281 again for the v4 contract, and chronicle#376 # for v5: Table 1.1's "All returns" row now exports the register's - # "All returns, total" and keeps the IRS text in - # package_dimension_value_labels. The keys and values in them - # are unchanged throughout. - "d14e2bd4b66d46dd765e82805713f03e46141c479368bc7f78a73ef9cf9cc0a9" + # "All returns, total" and keeps the package's row label in + # package_dimension_value_labels (a name chronicle#377's review + # gave the field). The keys and values in them are unchanged + # throughout. + "450a22c689b7a086cfc03522aa4492d491efc6875a4661dbd851a2015506897a" ), fixture_root / "source_cells" / "soi_table_1_1_2023_cells.jsonl": ( "615639f21ee63e54595c677e24c3eddff484c00a795a2f91b45a8575f021c7e2" diff --git a/tests/test_issue_376_one_label_per_value.py b/tests/test_issue_376_one_label_per_value.py index 82f46032..8880832a 100644 --- a/tests/test_issue_376_one_label_per_value.py +++ b/tests/test_issue_376_one_label_per_value.py @@ -487,14 +487,24 @@ def test_validate_package_refuses_a_declaration_the_register_contradicts(tmp_pat ("nisra/mye_2025_lgd_single_year_age", "sex", "male", "Male"), ("nrs/pcon24_population_by_age_2024_revised_2026", "sex", "female", "Female"), ("nrs/pcon24_population_by_age_2024_revised_2026", "sex", "male", "Male"), - ("ons/hpssa_pcon24_year_ending_march_2026", "property_type", "detached", "Detached"), + ( + "ons/hpssa_pcon24_year_ending_march_2026", + "property_type", + "detached", + "Detached", + ), ( "ons/hpssa_pcon24_year_ending_march_2026", "property_type", "semi_detached", "Semi detached", ), - ("ons/hpssa_pcon24_year_ending_march_2026", "property_type", "terraced", "Terraced"), + ( + "ons/hpssa_pcon24_year_ending_march_2026", + "property_type", + "terraced", + "Terraced", + ), ("ons/pipr_rents_by_area_august_2026", "property_type", "detached", "Detached"), ("ons/pipr_rents_by_area_august_2026", "property_type", "terraced", "Terraced"), ("dft/nts0601_age_mode_trips_2025", "age_band", "30 to 39", "30 to 39"), @@ -524,7 +534,13 @@ def test_the_declarations_the_register_overrode_now_agree_with_it( _CHRONICLE_WRITTEN_WORDINGS = [ # The Statbel ZIP has single-year CD_AGE only; "65_plus" is the curated # CSV's value id. - ("statbel-population-structure-2025", 2025, "person.age_band", "65_plus", "65_plus"), + ( + "statbel-population-structure-2025", + 2025, + "person.age_band", + "65_plus", + "65_plus", + ), # The B01001 JSON holds variable codes and numbers; the text is # B01001_FEMALE_AGE_COLUMNS and the parser's sex column. ("census-b01001-female-age-2023", 2023, "age", "age_20", "Female 20 years"), @@ -582,8 +598,6 @@ def test_the_declarations_the_register_overrode_now_agree_with_it( "sociale_toeslag", "Social supplement child caseload", ), - # DfT's row header, which the guard checks, is "30 to 39". - ("dft-nts0601-age-mode-trips-2025", 2023, "age_band", "30 to 39", "Age 30 to 39"), ] @@ -622,6 +636,27 @@ def test_text_chronicle_wrote_is_kept_as_the_packages_wording_not_the_publishers assert "text Chronicle wrote" in description +def test_nts0601_age_bands_now_carry_dfts_header_and_no_package_wording(): + """NTS0601's "Age 30 to 39" came from its declaration, now the register's. + + DfT's row header, which the guard checks, is "30 to 39"; the declaration + "Age 30 to 39" was Chronicle's text in the field at e29da32. + """ + package = load_source_package("dft-nts0601-age-mode-trips-2025") + facts = package.build_facts(2023, cells=package.build_source_cells(2023)) + banded = [ + fact + for fact in facts + if fact.filters.get("age_band") in {"30 to 39", "40 to 49"} + ] + + assert len(banded) == 18 + for fact in banded: + value_id = fact.filters["age_band"] + assert fact.dimension_value_labels["age_band"] == {value_id: value_id} + assert "age_band" not in fact.package_dimension_value_labels + + def test_a_groupby_value_whose_axis_is_also_a_filter_is_keyed_by_its_row_value(): """Eurostat ilc_di01: quant_inc is the filter D1 and the groupby row d1.""" package = load_source_package("eurostat-ilc-di01") @@ -671,9 +706,7 @@ def test_the_ons_coicop_02_total_includes_narcotics(): cdid_row = next(row for row in sheet_rows if row[0] == "CDID identifier code") column_by_cdid = {cdid: index for index, cdid in enumerate(cdid_row) if cdid} annual = [ - row - for row in sheet_rows - if isinstance(row[0], int) and 1997 <= row[0] <= 2100 + row for row in sheet_rows if isinstance(row[0], int) and 1997 <= row[0] <= 2100 ] assert len(annual) >= 29 diff --git a/tests/test_policyengine_chronicle_schema.py b/tests/test_policyengine_chronicle_schema.py index 92a03d87..45c60684 100644 --- a/tests/test_policyengine_chronicle_schema.py +++ b/tests/test_policyengine_chronicle_schema.py @@ -46,7 +46,7 @@ # chronicle#356 adds gas_year to period.type's vocabulary description. _V4: "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", # chronicle#376: v4 plus the source package's own wording of a value. - _V5: "c4d2579cdf0e4a8ea0b43ec74c75f198ea12916f443073af29adcd2e670eb7a6", + _V5: "acd33a091cd8d192be5d264e3e94c13e00afb6fd1972e57d4576ac55a54f6970", } _TOP_LEVEL_KEY_DOMAINS = { From 1cd9ce171cfcda4db4c2770497039a6312fe746a Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:07:38 +0400 Subject: [PATCH 07/13] Re-pin the two Eurostat fact digests and check what moved them (#376) `test_eurostat_prior_vintage_facts_remain_byte_stable` hashes every `AggregateFact` field of `eurostat-gov-10a-taxag` and `eurostat-spr-exp-func`. The new `package_dimension_value_labels` field is an empty mapping on every fact of both packages, and `asdict` includes it, so the digests moved (CI run 39685539423, "Chronicle surface"). With that key removed, the facts hash to the old digests exactly. The test now pins the new full-fact digests (516a28c5..., e0231b78...) and also checks the reason: the field is `{}` on every fact, and with it removed the facts still hash to 89b50ea0... and b57eca7e.... #263 recorded the same kind of strip-and-compare result only in the comment; here it is asserted. One commit, not a red and a green: the code is already correct and only the pinned constants were stale. A test-only commit with the new assertions and the old pins fails exactly as the PR head does, on the full-fact digest, while the strip-and-compare assertions already pass at `d24819c`; no code change turns it green, only the re-pin in this file. Co-Authored-By: Claude Opus 5.5 --- tests/test_etl_eurostat.py | 40 ++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/tests/test_etl_eurostat.py b/tests/test_etl_eurostat.py index 2c46fa89..b3a9a4bf 100644 --- a/tests/test_etl_eurostat.py +++ b/tests/test_etl_eurostat.py @@ -62,7 +62,19 @@ # constraints happened to carry. With the three label fields stripped, these # facts still hash to the pre-label 9db298bc... and 51768608... : only the # labels moved. +# chronicle#376 adds AggregateFact.package_dimension_value_labels. It is an +# empty mapping on every fact of these two packages; with that key removed, +# the facts still hash to the digests before it (below): only the empty +# field moved. PRIOR_VINTAGE_FACT_DIGESTS = { + "eurostat-gov-10a-taxag": ( + "516a28c598db59fa4aafd0abef2fd3339a22495e3f2143a0b137e7290672f9d5" + ), + "eurostat-spr-exp-func": ( + "e0231b789fb8142751a78aeeac09548f3dd6dba86d2031abf160b8fe63cd101b" + ), +} +FACT_DIGESTS_BEFORE_PACKAGE_WORDING = { "eurostat-gov-10a-taxag": ( "89b50ea09e046680f1acd6f3a5e7778a249a0e671a31dfe74b5245a12c31fb8a" ), @@ -421,16 +433,28 @@ def test_eurostat_production_values_match_publisher_bytes(): assert isinstance(be_disability[0].value, int) +def _fact_payload_digest(fact_payloads): + return hashlib.sha256( + json.dumps(fact_payloads, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + + @pytest.mark.parametrize("alias", PRIOR_VINTAGE_FACT_DIGESTS) def test_eurostat_prior_vintage_facts_remain_byte_stable(alias): - facts = _package_outputs(alias)[3] - canonical = json.dumps( - [asdict(fact) for fact in facts], - sort_keys=True, - separators=(",", ":"), - ).encode() - - assert hashlib.sha256(canonical).hexdigest() == PRIOR_VINTAGE_FACT_DIGESTS[alias] + fact_payloads = [asdict(fact) for fact in _package_outputs(alias)[3]] + + assert _fact_payload_digest(fact_payloads) == PRIOR_VINTAGE_FACT_DIGESTS[alias] + # Only the new, empty package-wording field moved the bytes (chronicle#376). + assert all( + payload["package_dimension_value_labels"] == {} + for payload in fact_payloads + ) + for payload in fact_payloads: + del payload["package_dimension_value_labels"] + assert ( + _fact_payload_digest(fact_payloads) + == FACT_DIGESTS_BEFORE_PACKAGE_WORDING[alias] + ) def test_eurostat_2025_tax_vintage_adds_only_new_coordinates(): From eaf16123d50964f6c21225f791f7fa1f2e6b49e4 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:08:16 +0400 Subject: [PATCH 08/13] Test the remedy's script step and the label module's wording (#376) Two Low findings from the re-check of `d24819c`: - N2: the error for a value two packages label differently tells the author to add a register entry and run `--static-check`, but not to add the chosen label to `CHOSEN_LABELS` in `scripts/build_dimension_value_register.py`. Such a value always has two wordings, so without that entry the script no longer reproduces the register. The message test now asserts that step, and that a listed value's message does not mention it. - N3: the `chronicle.dimension_labels` docstring still calls the row label "the fact's publisher row label" and the source-row text "the publisher's text". A new test asserts it names them as the package's row label and the source-row text instead. Both tests fail here (2 failed) and pass with the next commit. Co-Authored-By: Claude Opus 5.5 --- tests/test_issue_376_one_label_per_value.py | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_issue_376_one_label_per_value.py b/tests/test_issue_376_one_label_per_value.py index 8880832a..28e097f5 100644 --- a/tests/test_issue_376_one_label_per_value.py +++ b/tests/test_issue_376_one_label_per_value.py @@ -316,6 +316,13 @@ def test_a_value_two_packages_label_differently_is_refused_with_its_remedy(): assert "the wording most of these packages use" in unlisted assert "national statistics office" in unlisted assert "scripts/build_dimension_value_register.py --static-check" in unlisted + # Two packages word it differently, so the script that regenerates the + # register needs the chosen label too, or it no longer reproduces the + # register (chronicle#377 re-check, N2). + assert ( + "add that label to CHOSEN_LABELS in " + "scripts/build_dimension_value_register.py" + ) in unlisted assert "dimension_value_labels" not in unlisted.replace( "package_dimension_value_labels", "" ) @@ -323,6 +330,7 @@ def test_a_value_two_packages_label_differently_is_refused_with_its_remedy(): assert "Chronicle's register labels this value 'Male'" in listed assert "rebuild" in listed assert "Add it to" not in listed + assert "CHOSEN_LABELS" not in listed # One label inside one package, refused as early as validate-package. @@ -686,6 +694,22 @@ def test_the_harness_doc_says_which_fields_the_guards_check(): ) in text +def test_the_label_module_does_not_call_package_wording_the_publishers(): + """The label-source list names the package's row label and source-row text. + + A row's ``label:`` and the text Chronicle's parser reads into a source row + are the package's wording, which nothing checks against the publisher's + file (chronicle#377 re-check, N3). + """ + docstring = " ".join(dimension_labels.__doc__.split()) + + assert "publisher row label" not in docstring + assert "the publisher's text in the matching column" not in docstring + assert ( + "the package's row label for the fact (``layout.groupby_value_label``" + ) in docstring + + # ONS COICOP 02: the total includes narcotics (chronicle#377 review). From c74db6227f2a7a895467958c3e4d83b2f56e672e Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:09:20 +0400 Subject: [PATCH 09/13] Name CHOSEN_LABELS in the remedy; call package text the package's (#376) - N2: `cross_package_value_label_remedy` now also tells the author to add the chosen label to `CHOSEN_LABELS` in `scripts/build_dimension_value_register.py`, so the script still reproduces the register, before running `--static-check`. The script path is a new constant, `REGISTER_SCRIPT_DISPLAY_PATH`, which `REGISTER_STATIC_CHECK` uses too. A listed value's message is unchanged. - N3: the `chronicle.dimension_labels` docstring's label-source list now names item 3 as the package's row label for the fact (the record-set row's `label:`) and item 4 as the source-row text as Chronicle's parser read it, from the publisher's file or from an extract Chronicle prepared. The local variable `publisher_labels` in `label_facts` and `_groupby_value_label`, which holds that wording, is renamed `package_wordings`. No label, fact or field changes. - Formatting only: `ruff format` joins one assertion in `tests/test_etl_eurostat.py` and one string in `tests/test_issue_376_one_label_per_value.py`. Co-Authored-By: Claude Opus 5.5 --- chronicle/dimension_labels.py | 42 ++++++++++++--------- tests/test_etl_eurostat.py | 3 +- tests/test_issue_376_one_label_per_value.py | 3 +- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/chronicle/dimension_labels.py b/chronicle/dimension_labels.py index 5bb649ab..121bedd1 100644 --- a/chronicle/dimension_labels.py +++ b/chronicle/dimension_labels.py @@ -25,13 +25,14 @@ value that two or more packages emit, or that its packages word more than one way, with one label; 2. the package's ``dimension_value_labels`` declaration; -3. the fact's publisher row label (``layout.groupby_value_label``) when the - dimension is the record set's groupby axis (the same id, or the same leaf - name source-filter canonicalization matches on) and the value is the row's - value id; -4. the publisher's text in the matching column of the fact's source row, when - the row-semantic acceptance matches the filter to it (for ``all``, the - publisher's aggregate item, such as Stat-Xplore's ``Total``); +3. the package's row label for the fact (``layout.groupby_value_label``, the + record-set row's ``label:``) when the dimension is the record set's groupby + axis (the same id, or the same leaf name source-filter canonicalization + matches on) and the value is the row's value id; +4. the text in the matching column of the fact's source row, as Chronicle's + parser read it (from the publisher's file or from an extract Chronicle + prepared), when the row-semantic acceptance matches the filter to it (for + ``all``, the source's aggregate item, such as Stat-Xplore's ``Total``); 5. a number or an ISO date, which labels itself. That label goes on ``dimension_value_labels`` and, for the groupby value, on @@ -82,10 +83,10 @@ REGISTER_DISPLAY_PATH = "chronicle/dimension_values/canonical_labels.json" #: Where the rule for choosing a register label is written down. LABEL_RULE_DISPLAY_PATH = "docs/agent-source-package-harness.md" +#: The script that regenerates the register, as error messages name it. +REGISTER_SCRIPT_DISPLAY_PATH = "scripts/build_dimension_value_register.py" #: The command that compares the register with the package files. -REGISTER_STATIC_CHECK = ( - "uv run python scripts/build_dimension_value_register.py --static-check" -) +REGISTER_STATIC_CHECK = f"uv run python {REGISTER_SCRIPT_DISPLAY_PATH} --static-check" def _register_label_remedy(canonical: str) -> str: @@ -120,7 +121,9 @@ def cross_package_value_label_remedy(dimension_id: str, value_id: str) -> str: Two packages emit the value, so a package declaration cannot settle it: the remedy is a register entry, and the message says how to choose its - label. A value the register lists already has its label. + label. The packages word it differently, so the script that regenerates + the register needs the same label in its ``CHOSEN_LABELS``, or it no longer + reproduces the register. A value the register lists already has its label. """ canonical = canonical_value_label(dimension_id, value_id) @@ -130,7 +133,9 @@ def cross_package_value_label_remedy(dimension_id: str, value_id: str) -> str: f"Add it to {REGISTER_DISPLAY_PATH} with one label (chronicle#376), " f"chosen by the rule in {LABEL_RULE_DISPLAY_PATH}: the wording most of " "these packages use, a tie going to the national statistics office's " - "wording, without footnote markers or words that name the measure. " + "wording, without footnote markers or words that name the measure, " + f"and add that label to CHOSEN_LABELS in {REGISTER_SCRIPT_DISPLAY_PATH}, " + "so the script still reproduces the register. " f"Then run `{REGISTER_STATIC_CHECK}`." ) @@ -253,7 +258,7 @@ def label_facts( if key in rows_by_key ] value_labels: dict[str, dict[str, str]] = defaultdict(dict) - publisher_labels: dict[str, dict[str, str]] = defaultdict(dict) + package_wordings: dict[str, dict[str, str]] = defaultdict(dict) def resolve( dimension_id: str, @@ -279,7 +284,7 @@ def resolve( # publisher's file, and it is often Chronicle's text. package_wording = row_label or declared_label or source_text if package_wording and package_wording != label: - publisher_labels[dimension_id][value_id] = package_wording + package_wordings[dimension_id][value_id] = package_wording for dimension_id, value in dimensions.items(): value_id = dimension_value_id(value) @@ -307,7 +312,7 @@ def resolve( layout = replace( layout, groupby_value_label=_groupby_value_label( - layout, value_labels, publisher_labels, declared_values + layout, value_labels, package_wordings, declared_values ), ) @@ -316,7 +321,7 @@ def resolve( fact, dimension_labels=labels, dimension_value_labels=dict(value_labels), - package_dimension_value_labels=dict(publisher_labels), + package_dimension_value_labels=dict(package_wordings), layout=layout, ) ) @@ -326,7 +331,7 @@ def resolve( def _groupby_value_label( layout: SourceRecordLayout, value_labels: Mapping[str, Mapping[str, str]], - publisher_labels: dict[str, dict[str, str]], + package_wordings: dict[str, dict[str, str]], declared_values: Mapping[str, Mapping[str, str]], ) -> str | None: """Return the one label of a fact's groupby value (chronicle#376). @@ -349,7 +354,7 @@ def _groupby_value_label( or row_label ) if row_label and label and row_label != label: - publisher_labels.setdefault(groupby, {})[value_id] = row_label + package_wordings.setdefault(groupby, {})[value_id] = row_label return label @@ -704,6 +709,7 @@ def _count( "LABEL_RULE_DISPLAY_PATH", "REGISTER_DISPLAY_PATH", "REGISTER_PATH", + "REGISTER_SCRIPT_DISPLAY_PATH", "REGISTER_STATIC_CHECK", "DimensionLabelIssue", "canonical_value_label", diff --git a/tests/test_etl_eurostat.py b/tests/test_etl_eurostat.py index b3a9a4bf..ffa1be21 100644 --- a/tests/test_etl_eurostat.py +++ b/tests/test_etl_eurostat.py @@ -446,8 +446,7 @@ def test_eurostat_prior_vintage_facts_remain_byte_stable(alias): assert _fact_payload_digest(fact_payloads) == PRIOR_VINTAGE_FACT_DIGESTS[alias] # Only the new, empty package-wording field moved the bytes (chronicle#376). assert all( - payload["package_dimension_value_labels"] == {} - for payload in fact_payloads + payload["package_dimension_value_labels"] == {} for payload in fact_payloads ) for payload in fact_payloads: del payload["package_dimension_value_labels"] diff --git a/tests/test_issue_376_one_label_per_value.py b/tests/test_issue_376_one_label_per_value.py index 28e097f5..1b90b1f9 100644 --- a/tests/test_issue_376_one_label_per_value.py +++ b/tests/test_issue_376_one_label_per_value.py @@ -320,8 +320,7 @@ def test_a_value_two_packages_label_differently_is_refused_with_its_remedy(): # register needs the chosen label too, or it no longer reproduces the # register (chronicle#377 re-check, N2). assert ( - "add that label to CHOSEN_LABELS in " - "scripts/build_dimension_value_register.py" + "add that label to CHOSEN_LABELS in scripts/build_dimension_value_register.py" ) in unlisted assert "dimension_value_labels" not in unlisted.replace( "package_dimension_value_labels", "" From 35514b4fd6ca0b6c0edecc2b3fa24944becd84da Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:23:01 +0400 Subject: [PATCH 10/13] Test that the bundle's label report calls row labels the package's (#376) F1 from the review of the fix round: the `_dimension_label_reports` docstring in `chronicle/bundle.py` still says "publisher row labels that differ across one groupby value's rows". Those are the record-set rows' `label:` values, package text that nothing checks against the publisher's file, the same wording N3 removed from `chronicle.dimension_labels`. The N3 test read only that module's docstring. A sibling test now asserts that the bundle docstring does not say "publisher row label" and names the package's row labels. It fails here (1 failed; the N3 test still passes) and passes with the next commit. Co-Authored-By: Claude Opus 5.5 --- tests/test_issue_376_one_label_per_value.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_issue_376_one_label_per_value.py b/tests/test_issue_376_one_label_per_value.py index 1b90b1f9..7209126f 100644 --- a/tests/test_issue_376_one_label_per_value.py +++ b/tests/test_issue_376_one_label_per_value.py @@ -709,6 +709,19 @@ def test_the_label_module_does_not_call_package_wording_the_publishers(): ) in docstring +def test_the_bundle_label_report_does_not_call_package_wording_the_publishers(): + """``build-bundle``'s label report names row labels as the package's. + + The labels that can differ across one groupby value's rows are the + record-set rows' ``label:`` values, package text that nothing checks + against the publisher's file (chronicle#377 fix-round review, F1). + """ + docstring = " ".join(bundle._dimension_label_reports.__doc__.split()) + + assert "publisher row label" not in docstring + assert "the package's row labels" in docstring + + # ONS COICOP 02: the total includes narcotics (chronicle#377 review). From 74fb2b86164d765a31f32eb06ae612ac5dbd26d5 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:23:15 +0400 Subject: [PATCH 11/13] Call the bundle label report's row labels the package's (#376) F1: the `_dimension_label_reports` docstring now says "the package's row labels (its record-set rows' `label:`) that differ across one groupby value's rows" instead of "publisher row labels". Docstring only; no message, label, fact or field changes. Co-Authored-By: Claude Opus 5.5 --- chronicle/bundle.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/chronicle/bundle.py b/chronicle/bundle.py index a1675068..8fbc3fcc 100644 --- a/chronicle/bundle.py +++ b/chronicle/bundle.py @@ -764,9 +764,10 @@ def _dimension_label_reports( Every package must label every dimension and value it emits, whichever country it publishes for: the row contract is one contract, and a consumer reads the labels the same way everywhere. A value one package labels two - ways, publisher row labels that differ across one groupby value's rows - included, is an error too: a consumer would see one value under two names - (chronicle#376). Nothing about labels is only a warning. + ways, the package's row labels (its record-set rows' ``label:``) that + differ across one groupby value's rows included, is an error too: a + consumer would see one value under two names (chronicle#376). Nothing + about labels is only a warning. """ errors: list[BuildBundleIssue] = [] warnings: list[BuildBundleIssue] = [] From 7954d1b9c796b305a83bca8904c6c19b01f3ddab Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:58:11 +0400 Subject: [PATCH 12/13] List the values main's new packages share in the label register (#376) After merging main (dece5e9), 3,238 dimension values are emitted by two or more default-bundle packages and worded one way, and the register did not list them. Each takes its one wording, as the register's rule says; no existing entry changes or goes: - census_acs.county: 3,222 county codes from the seven ACS 5-year county packages (#386); - statistic estimate and margin_of_error (ACS county packages, ONS families and households); - NZ QES and PREFU earnings_component, earnings_denominator, earnings_sector, earnings_series_reference and seasonal_adjustment (#382); - UK published_unit (gbp_million, million_sticks, thousand_kilograms) and accounting_basis cash (#384); - age_band age_25_to_34 and age_65_to_74 (GSGB 2025 and HMRC CGT by age); - time 2025 (the two 2025 Eurostat packages). The register now holds 8,062 values in 155 dimensions (4,824 in 147 before). BUILD_YEARS adds the 14 new packages with no 2023 artifact (the eight ACS packages at 2024, the six FSA portfolio packages at 2026), and the register records dece5e9 as the commit its packages were built at. Four values worded more than one way still need a chosen label, so the script cannot yet reproduce this file and --static-check reports two declarations the register contradicts: freq=A, income_range=all, gambling_commission.gsgb_activity=national_lottery_draws_in_person and salary_sacrifice_threshold_position=all. Co-Authored-By: Claude Opus 5.5 --- .../dimension_values/canonical_labels.json | 3272 ++++++++++++++++- scripts/build_dimension_value_register.py | 16 +- tests/test_dimension_value_register.py | 16 +- 3 files changed, 3300 insertions(+), 4 deletions(-) diff --git a/chronicle/dimension_values/canonical_labels.json b/chronicle/dimension_values/canonical_labels.json index c8fe0441..fb85aa7b 100644 --- a/chronicle/dimension_values/canonical_labels.json +++ b/chronicle/dimension_values/canonical_labels.json @@ -4,17 +4,31 @@ "sources": { "script": "scripts/build_dimension_value_register.py", "packages": "chronicle.bundle.DEFAULT_BUNDLE_SOURCES", - "built_at_chronicle_commit": "aa288c3", + "built_at_chronicle_commit": "dece5e9", "default_build_year": 2023, "build_years": { "census-acs-s0101-congressional-district-age-2024": 2024, "census-acs-s0101-national-age-2024": 2024, "census-acs-s0101-state-age-2024": 2024, + "census-acs-s1501-state-attainment-2024": 2024, "census-acs-s2201-congressional-district-snap-2024": 2024, + "census-acs5-b01003-county-2024": 2024, + "census-acs5-b11001-county-2024": 2024, + "census-acs5-b19001-county-2024": 2024, + "census-acs5-b19013-county-2024": 2024, + "census-acs5-b19025-county-2024": 2024, + "census-acs5-b19051-county-2024": 2024, + "census-acs5-b19061-county-2024": 2024, "cms-aca-effectuated-enrollment-2022": 2022, "cms-aca-oep-state-level": 2024, "cms-aca-oep-state-level-2022": 2022, "cms-aca-oep-state-level-2025": 2025, + "ed-fsa-dl-portfolio-by-repayment-plan-fy2026-q3": 2026, + "ed-fsa-portfolio-by-age-fy2026-q2": 2026, + "ed-fsa-portfolio-by-debt-size-fy2026-q2": 2026, + "ed-fsa-portfolio-by-loan-type-fy2026-q3": 2026, + "ed-fsa-portfolio-by-location-fy2026-q2": 2026, + "ed-fsa-portfolio-summary-fy2026-q3": 2026, "jct-obbba-revenue-estimates-2025": 2026, "jct-tax-expenditures-2024": 2024 }, @@ -3276,6 +3290,9 @@ "Female": "Female", "Male": "Male" }, + "accounting_basis": { + "cash": "Cash basis" + }, "administering_entity": { "state_programs": "State programs" }, @@ -3433,9 +3450,11 @@ "80-84": "80-84", "85-89": "85-89", "90 and over": "90 and over", + "age_25_to_34": "25 to 34", "age_35_to_44": "35 to 44", "age_45_to_54": "45 to 54", "age_55_to_64": "55 to 64", + "age_65_to_74": "65 to 74", "age_66_69": "Aged 66-69", "age_70_74": "Aged 70-74", "age_75_79": "Aged 75-79", @@ -3561,6 +3580,3230 @@ "Pre-2016 State Pension": "Pre-2016 State Pension", "all": "Total" }, + "census_acs.county": { + "01001": "Autauga County, Alabama", + "01003": "Baldwin County, Alabama", + "01005": "Barbour County, Alabama", + "01007": "Bibb County, Alabama", + "01009": "Blount County, Alabama", + "01011": "Bullock County, Alabama", + "01013": "Butler County, Alabama", + "01015": "Calhoun County, Alabama", + "01017": "Chambers County, Alabama", + "01019": "Cherokee County, Alabama", + "01021": "Chilton County, Alabama", + "01023": "Choctaw County, Alabama", + "01025": "Clarke County, Alabama", + "01027": "Clay County, Alabama", + "01029": "Cleburne County, Alabama", + "01031": "Coffee County, Alabama", + "01033": "Colbert County, Alabama", + "01035": "Conecuh County, Alabama", + "01037": "Coosa County, Alabama", + "01039": "Covington County, Alabama", + "01041": "Crenshaw County, Alabama", + "01043": "Cullman County, Alabama", + "01045": "Dale County, Alabama", + "01047": "Dallas County, Alabama", + "01049": "DeKalb County, Alabama", + "01051": "Elmore County, Alabama", + "01053": "Escambia County, Alabama", + "01055": "Etowah County, Alabama", + "01057": "Fayette County, Alabama", + "01059": "Franklin County, Alabama", + "01061": "Geneva County, Alabama", + "01063": "Greene County, Alabama", + "01065": "Hale County, Alabama", + "01067": "Henry County, Alabama", + "01069": "Houston County, Alabama", + "01071": "Jackson County, Alabama", + "01073": "Jefferson County, Alabama", + "01075": "Lamar County, Alabama", + "01077": "Lauderdale County, Alabama", + "01079": "Lawrence County, Alabama", + "01081": "Lee County, Alabama", + "01083": "Limestone County, Alabama", + "01085": "Lowndes County, Alabama", + "01087": "Macon County, Alabama", + "01089": "Madison County, Alabama", + "01091": "Marengo County, Alabama", + "01093": "Marion County, Alabama", + "01095": "Marshall County, Alabama", + "01097": "Mobile County, Alabama", + "01099": "Monroe County, Alabama", + "01101": "Montgomery County, Alabama", + "01103": "Morgan County, Alabama", + "01105": "Perry County, Alabama", + "01107": "Pickens County, Alabama", + "01109": "Pike County, Alabama", + "01111": "Randolph County, Alabama", + "01113": "Russell County, Alabama", + "01115": "St. Clair County, Alabama", + "01117": "Shelby County, Alabama", + "01119": "Sumter County, Alabama", + "01121": "Talladega County, Alabama", + "01123": "Tallapoosa County, Alabama", + "01125": "Tuscaloosa County, Alabama", + "01127": "Walker County, Alabama", + "01129": "Washington County, Alabama", + "01131": "Wilcox County, Alabama", + "01133": "Winston County, Alabama", + "02013": "Aleutians East Borough, Alaska", + "02016": "Aleutians West Census Area, Alaska", + "02020": "Anchorage Municipality, Alaska", + "02050": "Bethel Census Area, Alaska", + "02060": "Bristol Bay Borough, Alaska", + "02063": "Chugach Census Area, Alaska", + "02066": "Copper River Census Area, Alaska", + "02068": "Denali Borough, Alaska", + "02070": "Dillingham Census Area, Alaska", + "02090": "Fairbanks North Star Borough, Alaska", + "02100": "Haines Borough, Alaska", + "02105": "Hoonah-Angoon Census Area, Alaska", + "02110": "Juneau City and Borough, Alaska", + "02122": "Kenai Peninsula Borough, Alaska", + "02130": "Ketchikan Gateway Borough, Alaska", + "02150": "Kodiak Island Borough, Alaska", + "02158": "Kusilvak Census Area, Alaska", + "02164": "Lake and Peninsula Borough, Alaska", + "02170": "Matanuska-Susitna Borough, Alaska", + "02180": "Nome Census Area, Alaska", + "02185": "North Slope Borough, Alaska", + "02188": "Northwest Arctic Borough, Alaska", + "02195": "Petersburg Borough, Alaska", + "02198": "Prince of Wales-Hyder Census Area, Alaska", + "02220": "Sitka City and Borough, Alaska", + "02230": "Skagway Municipality, Alaska", + "02240": "Southeast Fairbanks Census Area, Alaska", + "02275": "Wrangell City and Borough, Alaska", + "02282": "Yakutat City and Borough, Alaska", + "02290": "Yukon-Koyukuk Census Area, Alaska", + "04001": "Apache County, Arizona", + "04003": "Cochise County, Arizona", + "04005": "Coconino County, Arizona", + "04007": "Gila County, Arizona", + "04009": "Graham County, Arizona", + "04011": "Greenlee County, Arizona", + "04012": "La Paz County, Arizona", + "04013": "Maricopa County, Arizona", + "04015": "Mohave County, Arizona", + "04017": "Navajo County, Arizona", + "04019": "Pima County, Arizona", + "04021": "Pinal County, Arizona", + "04023": "Santa Cruz County, Arizona", + "04025": "Yavapai County, Arizona", + "04027": "Yuma County, Arizona", + "05001": "Arkansas County, Arkansas", + "05003": "Ashley County, Arkansas", + "05005": "Baxter County, Arkansas", + "05007": "Benton County, Arkansas", + "05009": "Boone County, Arkansas", + "05011": "Bradley County, Arkansas", + "05013": "Calhoun County, Arkansas", + "05015": "Carroll County, Arkansas", + "05017": "Chicot County, Arkansas", + "05019": "Clark County, Arkansas", + "05021": "Clay County, Arkansas", + "05023": "Cleburne County, Arkansas", + "05025": "Cleveland County, Arkansas", + "05027": "Columbia County, Arkansas", + "05029": "Conway County, Arkansas", + "05031": "Craighead County, Arkansas", + "05033": "Crawford County, Arkansas", + "05035": "Crittenden County, Arkansas", + "05037": "Cross County, Arkansas", + "05039": "Dallas County, Arkansas", + "05041": "Desha County, Arkansas", + "05043": "Drew County, Arkansas", + "05045": "Faulkner County, Arkansas", + "05047": "Franklin County, Arkansas", + "05049": "Fulton County, Arkansas", + "05051": "Garland County, Arkansas", + "05053": "Grant County, Arkansas", + "05055": "Greene County, Arkansas", + "05057": "Hempstead County, Arkansas", + "05059": "Hot Spring County, Arkansas", + "05061": "Howard County, Arkansas", + "05063": "Independence County, Arkansas", + "05065": "Izard County, Arkansas", + "05067": "Jackson County, Arkansas", + "05069": "Jefferson County, Arkansas", + "05071": "Johnson County, Arkansas", + "05073": "Lafayette County, Arkansas", + "05075": "Lawrence County, Arkansas", + "05077": "Lee County, Arkansas", + "05079": "Lincoln County, Arkansas", + "05081": "Little River County, Arkansas", + "05083": "Logan County, Arkansas", + "05085": "Lonoke County, Arkansas", + "05087": "Madison County, Arkansas", + "05089": "Marion County, Arkansas", + "05091": "Miller County, Arkansas", + "05093": "Mississippi County, Arkansas", + "05095": "Monroe County, Arkansas", + "05097": "Montgomery County, Arkansas", + "05099": "Nevada County, Arkansas", + "05101": "Newton County, Arkansas", + "05103": "Ouachita County, Arkansas", + "05105": "Perry County, Arkansas", + "05107": "Phillips County, Arkansas", + "05109": "Pike County, Arkansas", + "05111": "Poinsett County, Arkansas", + "05113": "Polk County, Arkansas", + "05115": "Pope County, Arkansas", + "05117": "Prairie County, Arkansas", + "05119": "Pulaski County, Arkansas", + "05121": "Randolph County, Arkansas", + "05123": "St. Francis County, Arkansas", + "05125": "Saline County, Arkansas", + "05127": "Scott County, Arkansas", + "05129": "Searcy County, Arkansas", + "05131": "Sebastian County, Arkansas", + "05133": "Sevier County, Arkansas", + "05135": "Sharp County, Arkansas", + "05137": "Stone County, Arkansas", + "05139": "Union County, Arkansas", + "05141": "Van Buren County, Arkansas", + "05143": "Washington County, Arkansas", + "05145": "White County, Arkansas", + "05147": "Woodruff County, Arkansas", + "05149": "Yell County, Arkansas", + "06001": "Alameda County, California", + "06003": "Alpine County, California", + "06005": "Amador County, California", + "06007": "Butte County, California", + "06009": "Calaveras County, California", + "06011": "Colusa County, California", + "06013": "Contra Costa County, California", + "06015": "Del Norte County, California", + "06017": "El Dorado County, California", + "06019": "Fresno County, California", + "06021": "Glenn County, California", + "06023": "Humboldt County, California", + "06025": "Imperial County, California", + "06027": "Inyo County, California", + "06029": "Kern County, California", + "06031": "Kings County, California", + "06033": "Lake County, California", + "06035": "Lassen County, California", + "06037": "Los Angeles County, California", + "06039": "Madera County, California", + "06041": "Marin County, California", + "06043": "Mariposa County, California", + "06045": "Mendocino County, California", + "06047": "Merced County, California", + "06049": "Modoc County, California", + "06051": "Mono County, California", + "06053": "Monterey County, California", + "06055": "Napa County, California", + "06057": "Nevada County, California", + "06059": "Orange County, California", + "06061": "Placer County, California", + "06063": "Plumas County, California", + "06065": "Riverside County, California", + "06067": "Sacramento County, California", + "06069": "San Benito County, California", + "06071": "San Bernardino County, California", + "06073": "San Diego County, California", + "06075": "San Francisco County, California", + "06077": "San Joaquin County, California", + "06079": "San Luis Obispo County, California", + "06081": "San Mateo County, California", + "06083": "Santa Barbara County, California", + "06085": "Santa Clara County, California", + "06087": "Santa Cruz County, California", + "06089": "Shasta County, California", + "06091": "Sierra County, California", + "06093": "Siskiyou County, California", + "06095": "Solano County, California", + "06097": "Sonoma County, California", + "06099": "Stanislaus County, California", + "06101": "Sutter County, California", + "06103": "Tehama County, California", + "06105": "Trinity County, California", + "06107": "Tulare County, California", + "06109": "Tuolumne County, California", + "06111": "Ventura County, California", + "06113": "Yolo County, California", + "06115": "Yuba County, California", + "08001": "Adams County, Colorado", + "08003": "Alamosa County, Colorado", + "08005": "Arapahoe County, Colorado", + "08007": "Archuleta County, Colorado", + "08009": "Baca County, Colorado", + "08011": "Bent County, Colorado", + "08013": "Boulder County, Colorado", + "08014": "Broomfield County, Colorado", + "08015": "Chaffee County, Colorado", + "08017": "Cheyenne County, Colorado", + "08019": "Clear Creek County, Colorado", + "08021": "Conejos County, Colorado", + "08023": "Costilla County, Colorado", + "08025": "Crowley County, Colorado", + "08027": "Custer County, Colorado", + "08029": "Delta County, Colorado", + "08031": "Denver County, Colorado", + "08033": "Dolores County, Colorado", + "08035": "Douglas County, Colorado", + "08037": "Eagle County, Colorado", + "08039": "Elbert County, Colorado", + "08041": "El Paso County, Colorado", + "08043": "Fremont County, Colorado", + "08045": "Garfield County, Colorado", + "08047": "Gilpin County, Colorado", + "08049": "Grand County, Colorado", + "08051": "Gunnison County, Colorado", + "08053": "Hinsdale County, Colorado", + "08055": "Huerfano County, Colorado", + "08057": "Jackson County, Colorado", + "08059": "Jefferson County, Colorado", + "08061": "Kiowa County, Colorado", + "08063": "Kit Carson County, Colorado", + "08065": "Lake County, Colorado", + "08067": "La Plata County, Colorado", + "08069": "Larimer County, Colorado", + "08071": "Las Animas County, Colorado", + "08073": "Lincoln County, Colorado", + "08075": "Logan County, Colorado", + "08077": "Mesa County, Colorado", + "08079": "Mineral County, Colorado", + "08081": "Moffat County, Colorado", + "08083": "Montezuma County, Colorado", + "08085": "Montrose County, Colorado", + "08087": "Morgan County, Colorado", + "08089": "Otero County, Colorado", + "08091": "Ouray County, Colorado", + "08093": "Park County, Colorado", + "08095": "Phillips County, Colorado", + "08097": "Pitkin County, Colorado", + "08099": "Prowers County, Colorado", + "08101": "Pueblo County, Colorado", + "08103": "Rio Blanco County, Colorado", + "08105": "Rio Grande County, Colorado", + "08107": "Routt County, Colorado", + "08109": "Saguache County, Colorado", + "08111": "San Juan County, Colorado", + "08113": "San Miguel County, Colorado", + "08115": "Sedgwick County, Colorado", + "08117": "Summit County, Colorado", + "08119": "Teller County, Colorado", + "08121": "Washington County, Colorado", + "08123": "Weld County, Colorado", + "08125": "Yuma County, Colorado", + "09110": "Capitol Planning Region, Connecticut", + "09120": "Greater Bridgeport Planning Region, Connecticut", + "09130": "Lower Connecticut River Valley Planning Region, Connecticut", + "09140": "Naugatuck Valley Planning Region, Connecticut", + "09150": "Northeastern Connecticut Planning Region, Connecticut", + "09160": "Northwest Hills Planning Region, Connecticut", + "09170": "South Central Connecticut Planning Region, Connecticut", + "09180": "Southeastern Connecticut Planning Region, Connecticut", + "09190": "Western Connecticut Planning Region, Connecticut", + "10001": "Kent County, Delaware", + "10003": "New Castle County, Delaware", + "10005": "Sussex County, Delaware", + "11001": "District of Columbia, District of Columbia", + "12001": "Alachua County, Florida", + "12003": "Baker County, Florida", + "12005": "Bay County, Florida", + "12007": "Bradford County, Florida", + "12009": "Brevard County, Florida", + "12011": "Broward County, Florida", + "12013": "Calhoun County, Florida", + "12015": "Charlotte County, Florida", + "12017": "Citrus County, Florida", + "12019": "Clay County, Florida", + "12021": "Collier County, Florida", + "12023": "Columbia County, Florida", + "12027": "DeSoto County, Florida", + "12029": "Dixie County, Florida", + "12031": "Duval County, Florida", + "12033": "Escambia County, Florida", + "12035": "Flagler County, Florida", + "12037": "Franklin County, Florida", + "12039": "Gadsden County, Florida", + "12041": "Gilchrist County, Florida", + "12043": "Glades County, Florida", + "12045": "Gulf County, Florida", + "12047": "Hamilton County, Florida", + "12049": "Hardee County, Florida", + "12051": "Hendry County, Florida", + "12053": "Hernando County, Florida", + "12055": "Highlands County, Florida", + "12057": "Hillsborough County, Florida", + "12059": "Holmes County, Florida", + "12061": "Indian River County, Florida", + "12063": "Jackson County, Florida", + "12065": "Jefferson County, Florida", + "12067": "Lafayette County, Florida", + "12069": "Lake County, Florida", + "12071": "Lee County, Florida", + "12073": "Leon County, Florida", + "12075": "Levy County, Florida", + "12077": "Liberty County, Florida", + "12079": "Madison County, Florida", + "12081": "Manatee County, Florida", + "12083": "Marion County, Florida", + "12085": "Martin County, Florida", + "12086": "Miami-Dade County, Florida", + "12087": "Monroe County, Florida", + "12089": "Nassau County, Florida", + "12091": "Okaloosa County, Florida", + "12093": "Okeechobee County, Florida", + "12095": "Orange County, Florida", + "12097": "Osceola County, Florida", + "12099": "Palm Beach County, Florida", + "12101": "Pasco County, Florida", + "12103": "Pinellas County, Florida", + "12105": "Polk County, Florida", + "12107": "Putnam County, Florida", + "12109": "St. Johns County, Florida", + "12111": "St. Lucie County, Florida", + "12113": "Santa Rosa County, Florida", + "12115": "Sarasota County, Florida", + "12117": "Seminole County, Florida", + "12119": "Sumter County, Florida", + "12121": "Suwannee County, Florida", + "12123": "Taylor County, Florida", + "12125": "Union County, Florida", + "12127": "Volusia County, Florida", + "12129": "Wakulla County, Florida", + "12131": "Walton County, Florida", + "12133": "Washington County, Florida", + "13001": "Appling County, Georgia", + "13003": "Atkinson County, Georgia", + "13005": "Bacon County, Georgia", + "13007": "Baker County, Georgia", + "13009": "Baldwin County, Georgia", + "13011": "Banks County, Georgia", + "13013": "Barrow County, Georgia", + "13015": "Bartow County, Georgia", + "13017": "Ben Hill County, Georgia", + "13019": "Berrien County, Georgia", + "13021": "Bibb County, Georgia", + "13023": "Bleckley County, Georgia", + "13025": "Brantley County, Georgia", + "13027": "Brooks County, Georgia", + "13029": "Bryan County, Georgia", + "13031": "Bulloch County, Georgia", + "13033": "Burke County, Georgia", + "13035": "Butts County, Georgia", + "13037": "Calhoun County, Georgia", + "13039": "Camden County, Georgia", + "13043": "Candler County, Georgia", + "13045": "Carroll County, Georgia", + "13047": "Catoosa County, Georgia", + "13049": "Charlton County, Georgia", + "13051": "Chatham County, Georgia", + "13053": "Chattahoochee County, Georgia", + "13055": "Chattooga County, Georgia", + "13057": "Cherokee County, Georgia", + "13059": "Clarke County, Georgia", + "13061": "Clay County, Georgia", + "13063": "Clayton County, Georgia", + "13065": "Clinch County, Georgia", + "13067": "Cobb County, Georgia", + "13069": "Coffee County, Georgia", + "13071": "Colquitt County, Georgia", + "13073": "Columbia County, Georgia", + "13075": "Cook County, Georgia", + "13077": "Coweta County, Georgia", + "13079": "Crawford County, Georgia", + "13081": "Crisp County, Georgia", + "13083": "Dade County, Georgia", + "13085": "Dawson County, Georgia", + "13087": "Decatur County, Georgia", + "13089": "DeKalb County, Georgia", + "13091": "Dodge County, Georgia", + "13093": "Dooly County, Georgia", + "13095": "Dougherty County, Georgia", + "13097": "Douglas County, Georgia", + "13099": "Early County, Georgia", + "13101": "Echols County, Georgia", + "13103": "Effingham County, Georgia", + "13105": "Elbert County, Georgia", + "13107": "Emanuel County, Georgia", + "13109": "Evans County, Georgia", + "13111": "Fannin County, Georgia", + "13113": "Fayette County, Georgia", + "13115": "Floyd County, Georgia", + "13117": "Forsyth County, Georgia", + "13119": "Franklin County, Georgia", + "13121": "Fulton County, Georgia", + "13123": "Gilmer County, Georgia", + "13125": "Glascock County, Georgia", + "13127": "Glynn County, Georgia", + "13129": "Gordon County, Georgia", + "13131": "Grady County, Georgia", + "13133": "Greene County, Georgia", + "13135": "Gwinnett County, Georgia", + "13137": "Habersham County, Georgia", + "13139": "Hall County, Georgia", + "13141": "Hancock County, Georgia", + "13143": "Haralson County, Georgia", + "13145": "Harris County, Georgia", + "13147": "Hart County, Georgia", + "13149": "Heard County, Georgia", + "13151": "Henry County, Georgia", + "13153": "Houston County, Georgia", + "13155": "Irwin County, Georgia", + "13157": "Jackson County, Georgia", + "13159": "Jasper County, Georgia", + "13161": "Jeff Davis County, Georgia", + "13163": "Jefferson County, Georgia", + "13165": "Jenkins County, Georgia", + "13167": "Johnson County, Georgia", + "13169": "Jones County, Georgia", + "13171": "Lamar County, Georgia", + "13173": "Lanier County, Georgia", + "13175": "Laurens County, Georgia", + "13177": "Lee County, Georgia", + "13179": "Liberty County, Georgia", + "13181": "Lincoln County, Georgia", + "13183": "Long County, Georgia", + "13185": "Lowndes County, Georgia", + "13187": "Lumpkin County, Georgia", + "13189": "McDuffie County, Georgia", + "13191": "McIntosh County, Georgia", + "13193": "Macon County, Georgia", + "13195": "Madison County, Georgia", + "13197": "Marion County, Georgia", + "13199": "Meriwether County, Georgia", + "13201": "Miller County, Georgia", + "13205": "Mitchell County, Georgia", + "13207": "Monroe County, Georgia", + "13209": "Montgomery County, Georgia", + "13211": "Morgan County, Georgia", + "13213": "Murray County, Georgia", + "13215": "Muscogee County, Georgia", + "13217": "Newton County, Georgia", + "13219": "Oconee County, Georgia", + "13221": "Oglethorpe County, Georgia", + "13223": "Paulding County, Georgia", + "13225": "Peach County, Georgia", + "13227": "Pickens County, Georgia", + "13229": "Pierce County, Georgia", + "13231": "Pike County, Georgia", + "13233": "Polk County, Georgia", + "13235": "Pulaski County, Georgia", + "13237": "Putnam County, Georgia", + "13239": "Quitman County, Georgia", + "13241": "Rabun County, Georgia", + "13243": "Randolph County, Georgia", + "13245": "Richmond County, Georgia", + "13247": "Rockdale County, Georgia", + "13249": "Schley County, Georgia", + "13251": "Screven County, Georgia", + "13253": "Seminole County, Georgia", + "13255": "Spalding County, Georgia", + "13257": "Stephens County, Georgia", + "13259": "Stewart County, Georgia", + "13261": "Sumter County, Georgia", + "13263": "Talbot County, Georgia", + "13265": "Taliaferro County, Georgia", + "13267": "Tattnall County, Georgia", + "13269": "Taylor County, Georgia", + "13271": "Telfair County, Georgia", + "13273": "Terrell County, Georgia", + "13275": "Thomas County, Georgia", + "13277": "Tift County, Georgia", + "13279": "Toombs County, Georgia", + "13281": "Towns County, Georgia", + "13283": "Treutlen County, Georgia", + "13285": "Troup County, Georgia", + "13287": "Turner County, Georgia", + "13289": "Twiggs County, Georgia", + "13291": "Union County, Georgia", + "13293": "Upson County, Georgia", + "13295": "Walker County, Georgia", + "13297": "Walton County, Georgia", + "13299": "Ware County, Georgia", + "13301": "Warren County, Georgia", + "13303": "Washington County, Georgia", + "13305": "Wayne County, Georgia", + "13307": "Webster County, Georgia", + "13309": "Wheeler County, Georgia", + "13311": "White County, Georgia", + "13313": "Whitfield County, Georgia", + "13315": "Wilcox County, Georgia", + "13317": "Wilkes County, Georgia", + "13319": "Wilkinson County, Georgia", + "13321": "Worth County, Georgia", + "15001": "Hawaii County, Hawaii", + "15003": "Honolulu County, Hawaii", + "15005": "Kalawao County, Hawaii", + "15007": "Kauai County, Hawaii", + "15009": "Maui County, Hawaii", + "16001": "Ada County, Idaho", + "16003": "Adams County, Idaho", + "16005": "Bannock County, Idaho", + "16007": "Bear Lake County, Idaho", + "16009": "Benewah County, Idaho", + "16011": "Bingham County, Idaho", + "16013": "Blaine County, Idaho", + "16015": "Boise County, Idaho", + "16017": "Bonner County, Idaho", + "16019": "Bonneville County, Idaho", + "16021": "Boundary County, Idaho", + "16023": "Butte County, Idaho", + "16025": "Camas County, Idaho", + "16027": "Canyon County, Idaho", + "16029": "Caribou County, Idaho", + "16031": "Cassia County, Idaho", + "16033": "Clark County, Idaho", + "16035": "Clearwater County, Idaho", + "16037": "Custer County, Idaho", + "16039": "Elmore County, Idaho", + "16041": "Franklin County, Idaho", + "16043": "Fremont County, Idaho", + "16045": "Gem County, Idaho", + "16047": "Gooding County, Idaho", + "16049": "Idaho County, Idaho", + "16051": "Jefferson County, Idaho", + "16053": "Jerome County, Idaho", + "16055": "Kootenai County, Idaho", + "16057": "Latah County, Idaho", + "16059": "Lemhi County, Idaho", + "16061": "Lewis County, Idaho", + "16063": "Lincoln County, Idaho", + "16065": "Madison County, Idaho", + "16067": "Minidoka County, Idaho", + "16069": "Nez Perce County, Idaho", + "16071": "Oneida County, Idaho", + "16073": "Owyhee County, Idaho", + "16075": "Payette County, Idaho", + "16077": "Power County, Idaho", + "16079": "Shoshone County, Idaho", + "16081": "Teton County, Idaho", + "16083": "Twin Falls County, Idaho", + "16085": "Valley County, Idaho", + "16087": "Washington County, Idaho", + "17001": "Adams County, Illinois", + "17003": "Alexander County, Illinois", + "17005": "Bond County, Illinois", + "17007": "Boone County, Illinois", + "17009": "Brown County, Illinois", + "17011": "Bureau County, Illinois", + "17013": "Calhoun County, Illinois", + "17015": "Carroll County, Illinois", + "17017": "Cass County, Illinois", + "17019": "Champaign County, Illinois", + "17021": "Christian County, Illinois", + "17023": "Clark County, Illinois", + "17025": "Clay County, Illinois", + "17027": "Clinton County, Illinois", + "17029": "Coles County, Illinois", + "17031": "Cook County, Illinois", + "17033": "Crawford County, Illinois", + "17035": "Cumberland County, Illinois", + "17037": "DeKalb County, Illinois", + "17039": "De Witt County, Illinois", + "17041": "Douglas County, Illinois", + "17043": "DuPage County, Illinois", + "17045": "Edgar County, Illinois", + "17047": "Edwards County, Illinois", + "17049": "Effingham County, Illinois", + "17051": "Fayette County, Illinois", + "17053": "Ford County, Illinois", + "17055": "Franklin County, Illinois", + "17057": "Fulton County, Illinois", + "17059": "Gallatin County, Illinois", + "17061": "Greene County, Illinois", + "17063": "Grundy County, Illinois", + "17065": "Hamilton County, Illinois", + "17067": "Hancock County, Illinois", + "17069": "Hardin County, Illinois", + "17071": "Henderson County, Illinois", + "17073": "Henry County, Illinois", + "17075": "Iroquois County, Illinois", + "17077": "Jackson County, Illinois", + "17079": "Jasper County, Illinois", + "17081": "Jefferson County, Illinois", + "17083": "Jersey County, Illinois", + "17085": "Jo Daviess County, Illinois", + "17087": "Johnson County, Illinois", + "17089": "Kane County, Illinois", + "17091": "Kankakee County, Illinois", + "17093": "Kendall County, Illinois", + "17095": "Knox County, Illinois", + "17097": "Lake County, Illinois", + "17099": "LaSalle County, Illinois", + "17101": "Lawrence County, Illinois", + "17103": "Lee County, Illinois", + "17105": "Livingston County, Illinois", + "17107": "Logan County, Illinois", + "17109": "McDonough County, Illinois", + "17111": "McHenry County, Illinois", + "17113": "McLean County, Illinois", + "17115": "Macon County, Illinois", + "17117": "Macoupin County, Illinois", + "17119": "Madison County, Illinois", + "17121": "Marion County, Illinois", + "17123": "Marshall County, Illinois", + "17125": "Mason County, Illinois", + "17127": "Massac County, Illinois", + "17129": "Menard County, Illinois", + "17131": "Mercer County, Illinois", + "17133": "Monroe County, Illinois", + "17135": "Montgomery County, Illinois", + "17137": "Morgan County, Illinois", + "17139": "Moultrie County, Illinois", + "17141": "Ogle County, Illinois", + "17143": "Peoria County, Illinois", + "17145": "Perry County, Illinois", + "17147": "Piatt County, Illinois", + "17149": "Pike County, Illinois", + "17151": "Pope County, Illinois", + "17153": "Pulaski County, Illinois", + "17155": "Putnam County, Illinois", + "17157": "Randolph County, Illinois", + "17159": "Richland County, Illinois", + "17161": "Rock Island County, Illinois", + "17163": "St. Clair County, Illinois", + "17165": "Saline County, Illinois", + "17167": "Sangamon County, Illinois", + "17169": "Schuyler County, Illinois", + "17171": "Scott County, Illinois", + "17173": "Shelby County, Illinois", + "17175": "Stark County, Illinois", + "17177": "Stephenson County, Illinois", + "17179": "Tazewell County, Illinois", + "17181": "Union County, Illinois", + "17183": "Vermilion County, Illinois", + "17185": "Wabash County, Illinois", + "17187": "Warren County, Illinois", + "17189": "Washington County, Illinois", + "17191": "Wayne County, Illinois", + "17193": "White County, Illinois", + "17195": "Whiteside County, Illinois", + "17197": "Will County, Illinois", + "17199": "Williamson County, Illinois", + "17201": "Winnebago County, Illinois", + "17203": "Woodford County, Illinois", + "18001": "Adams County, Indiana", + "18003": "Allen County, Indiana", + "18005": "Bartholomew County, Indiana", + "18007": "Benton County, Indiana", + "18009": "Blackford County, Indiana", + "18011": "Boone County, Indiana", + "18013": "Brown County, Indiana", + "18015": "Carroll County, Indiana", + "18017": "Cass County, Indiana", + "18019": "Clark County, Indiana", + "18021": "Clay County, Indiana", + "18023": "Clinton County, Indiana", + "18025": "Crawford County, Indiana", + "18027": "Daviess County, Indiana", + "18029": "Dearborn County, Indiana", + "18031": "Decatur County, Indiana", + "18033": "DeKalb County, Indiana", + "18035": "Delaware County, Indiana", + "18037": "Dubois County, Indiana", + "18039": "Elkhart County, Indiana", + "18041": "Fayette County, Indiana", + "18043": "Floyd County, Indiana", + "18045": "Fountain County, Indiana", + "18047": "Franklin County, Indiana", + "18049": "Fulton County, Indiana", + "18051": "Gibson County, Indiana", + "18053": "Grant County, Indiana", + "18055": "Greene County, Indiana", + "18057": "Hamilton County, Indiana", + "18059": "Hancock County, Indiana", + "18061": "Harrison County, Indiana", + "18063": "Hendricks County, Indiana", + "18065": "Henry County, Indiana", + "18067": "Howard County, Indiana", + "18069": "Huntington County, Indiana", + "18071": "Jackson County, Indiana", + "18073": "Jasper County, Indiana", + "18075": "Jay County, Indiana", + "18077": "Jefferson County, Indiana", + "18079": "Jennings County, Indiana", + "18081": "Johnson County, Indiana", + "18083": "Knox County, Indiana", + "18085": "Kosciusko County, Indiana", + "18087": "LaGrange County, Indiana", + "18089": "Lake County, Indiana", + "18091": "LaPorte County, Indiana", + "18093": "Lawrence County, Indiana", + "18095": "Madison County, Indiana", + "18097": "Marion County, Indiana", + "18099": "Marshall County, Indiana", + "18101": "Martin County, Indiana", + "18103": "Miami County, Indiana", + "18105": "Monroe County, Indiana", + "18107": "Montgomery County, Indiana", + "18109": "Morgan County, Indiana", + "18111": "Newton County, Indiana", + "18113": "Noble County, Indiana", + "18115": "Ohio County, Indiana", + "18117": "Orange County, Indiana", + "18119": "Owen County, Indiana", + "18121": "Parke County, Indiana", + "18123": "Perry County, Indiana", + "18125": "Pike County, Indiana", + "18127": "Porter County, Indiana", + "18129": "Posey County, Indiana", + "18131": "Pulaski County, Indiana", + "18133": "Putnam County, Indiana", + "18135": "Randolph County, Indiana", + "18137": "Ripley County, Indiana", + "18139": "Rush County, Indiana", + "18141": "St. Joseph County, Indiana", + "18143": "Scott County, Indiana", + "18145": "Shelby County, Indiana", + "18147": "Spencer County, Indiana", + "18149": "Starke County, Indiana", + "18151": "Steuben County, Indiana", + "18153": "Sullivan County, Indiana", + "18155": "Switzerland County, Indiana", + "18157": "Tippecanoe County, Indiana", + "18159": "Tipton County, Indiana", + "18161": "Union County, Indiana", + "18163": "Vanderburgh County, Indiana", + "18165": "Vermillion County, Indiana", + "18167": "Vigo County, Indiana", + "18169": "Wabash County, Indiana", + "18171": "Warren County, Indiana", + "18173": "Warrick County, Indiana", + "18175": "Washington County, Indiana", + "18177": "Wayne County, Indiana", + "18179": "Wells County, Indiana", + "18181": "White County, Indiana", + "18183": "Whitley County, Indiana", + "19001": "Adair County, Iowa", + "19003": "Adams County, Iowa", + "19005": "Allamakee County, Iowa", + "19007": "Appanoose County, Iowa", + "19009": "Audubon County, Iowa", + "19011": "Benton County, Iowa", + "19013": "Black Hawk County, Iowa", + "19015": "Boone County, Iowa", + "19017": "Bremer County, Iowa", + "19019": "Buchanan County, Iowa", + "19021": "Buena Vista County, Iowa", + "19023": "Butler County, Iowa", + "19025": "Calhoun County, Iowa", + "19027": "Carroll County, Iowa", + "19029": "Cass County, Iowa", + "19031": "Cedar County, Iowa", + "19033": "Cerro Gordo County, Iowa", + "19035": "Cherokee County, Iowa", + "19037": "Chickasaw County, Iowa", + "19039": "Clarke County, Iowa", + "19041": "Clay County, Iowa", + "19043": "Clayton County, Iowa", + "19045": "Clinton County, Iowa", + "19047": "Crawford County, Iowa", + "19049": "Dallas County, Iowa", + "19051": "Davis County, Iowa", + "19053": "Decatur County, Iowa", + "19055": "Delaware County, Iowa", + "19057": "Des Moines County, Iowa", + "19059": "Dickinson County, Iowa", + "19061": "Dubuque County, Iowa", + "19063": "Emmet County, Iowa", + "19065": "Fayette County, Iowa", + "19067": "Floyd County, Iowa", + "19069": "Franklin County, Iowa", + "19071": "Fremont County, Iowa", + "19073": "Greene County, Iowa", + "19075": "Grundy County, Iowa", + "19077": "Guthrie County, Iowa", + "19079": "Hamilton County, Iowa", + "19081": "Hancock County, Iowa", + "19083": "Hardin County, Iowa", + "19085": "Harrison County, Iowa", + "19087": "Henry County, Iowa", + "19089": "Howard County, Iowa", + "19091": "Humboldt County, Iowa", + "19093": "Ida County, Iowa", + "19095": "Iowa County, Iowa", + "19097": "Jackson County, Iowa", + "19099": "Jasper County, Iowa", + "19101": "Jefferson County, Iowa", + "19103": "Johnson County, Iowa", + "19105": "Jones County, Iowa", + "19107": "Keokuk County, Iowa", + "19109": "Kossuth County, Iowa", + "19111": "Lee County, Iowa", + "19113": "Linn County, Iowa", + "19115": "Louisa County, Iowa", + "19117": "Lucas County, Iowa", + "19119": "Lyon County, Iowa", + "19121": "Madison County, Iowa", + "19123": "Mahaska County, Iowa", + "19125": "Marion County, Iowa", + "19127": "Marshall County, Iowa", + "19129": "Mills County, Iowa", + "19131": "Mitchell County, Iowa", + "19133": "Monona County, Iowa", + "19135": "Monroe County, Iowa", + "19137": "Montgomery County, Iowa", + "19139": "Muscatine County, Iowa", + "19141": "O'Brien County, Iowa", + "19143": "Osceola County, Iowa", + "19145": "Page County, Iowa", + "19147": "Palo Alto County, Iowa", + "19149": "Plymouth County, Iowa", + "19151": "Pocahontas County, Iowa", + "19153": "Polk County, Iowa", + "19155": "Pottawattamie County, Iowa", + "19157": "Poweshiek County, Iowa", + "19159": "Ringgold County, Iowa", + "19161": "Sac County, Iowa", + "19163": "Scott County, Iowa", + "19165": "Shelby County, Iowa", + "19167": "Sioux County, Iowa", + "19169": "Story County, Iowa", + "19171": "Tama County, Iowa", + "19173": "Taylor County, Iowa", + "19175": "Union County, Iowa", + "19177": "Van Buren County, Iowa", + "19179": "Wapello County, Iowa", + "19181": "Warren County, Iowa", + "19183": "Washington County, Iowa", + "19185": "Wayne County, Iowa", + "19187": "Webster County, Iowa", + "19189": "Winnebago County, Iowa", + "19191": "Winneshiek County, Iowa", + "19193": "Woodbury County, Iowa", + "19195": "Worth County, Iowa", + "19197": "Wright County, Iowa", + "20001": "Allen County, Kansas", + "20003": "Anderson County, Kansas", + "20005": "Atchison County, Kansas", + "20007": "Barber County, Kansas", + "20009": "Barton County, Kansas", + "20011": "Bourbon County, Kansas", + "20013": "Brown County, Kansas", + "20015": "Butler County, Kansas", + "20017": "Chase County, Kansas", + "20019": "Chautauqua County, Kansas", + "20021": "Cherokee County, Kansas", + "20023": "Cheyenne County, Kansas", + "20025": "Clark County, Kansas", + "20027": "Clay County, Kansas", + "20029": "Cloud County, Kansas", + "20031": "Coffey County, Kansas", + "20033": "Comanche County, Kansas", + "20035": "Cowley County, Kansas", + "20037": "Crawford County, Kansas", + "20039": "Decatur County, Kansas", + "20041": "Dickinson County, Kansas", + "20043": "Doniphan County, Kansas", + "20045": "Douglas County, Kansas", + "20047": "Edwards County, Kansas", + "20049": "Elk County, Kansas", + "20051": "Ellis County, Kansas", + "20053": "Ellsworth County, Kansas", + "20055": "Finney County, Kansas", + "20057": "Ford County, Kansas", + "20059": "Franklin County, Kansas", + "20061": "Geary County, Kansas", + "20063": "Gove County, Kansas", + "20065": "Graham County, Kansas", + "20067": "Grant County, Kansas", + "20069": "Gray County, Kansas", + "20071": "Greeley County, Kansas", + "20073": "Greenwood County, Kansas", + "20075": "Hamilton County, Kansas", + "20077": "Harper County, Kansas", + "20079": "Harvey County, Kansas", + "20081": "Haskell County, Kansas", + "20083": "Hodgeman County, Kansas", + "20085": "Jackson County, Kansas", + "20087": "Jefferson County, Kansas", + "20089": "Jewell County, Kansas", + "20091": "Johnson County, Kansas", + "20093": "Kearny County, Kansas", + "20095": "Kingman County, Kansas", + "20097": "Kiowa County, Kansas", + "20099": "Labette County, Kansas", + "20101": "Lane County, Kansas", + "20103": "Leavenworth County, Kansas", + "20105": "Lincoln County, Kansas", + "20107": "Linn County, Kansas", + "20109": "Logan County, Kansas", + "20111": "Lyon County, Kansas", + "20113": "McPherson County, Kansas", + "20115": "Marion County, Kansas", + "20117": "Marshall County, Kansas", + "20119": "Meade County, Kansas", + "20121": "Miami County, Kansas", + "20123": "Mitchell County, Kansas", + "20125": "Montgomery County, Kansas", + "20127": "Morris County, Kansas", + "20129": "Morton County, Kansas", + "20131": "Nemaha County, Kansas", + "20133": "Neosho County, Kansas", + "20135": "Ness County, Kansas", + "20137": "Norton County, Kansas", + "20139": "Osage County, Kansas", + "20141": "Osborne County, Kansas", + "20143": "Ottawa County, Kansas", + "20145": "Pawnee County, Kansas", + "20147": "Phillips County, Kansas", + "20149": "Pottawatomie County, Kansas", + "20151": "Pratt County, Kansas", + "20153": "Rawlins County, Kansas", + "20155": "Reno County, Kansas", + "20157": "Republic County, Kansas", + "20159": "Rice County, Kansas", + "20161": "Riley County, Kansas", + "20163": "Rooks County, Kansas", + "20165": "Rush County, Kansas", + "20167": "Russell County, Kansas", + "20169": "Saline County, Kansas", + "20171": "Scott County, Kansas", + "20173": "Sedgwick County, Kansas", + "20175": "Seward County, Kansas", + "20177": "Shawnee County, Kansas", + "20179": "Sheridan County, Kansas", + "20181": "Sherman County, Kansas", + "20183": "Smith County, Kansas", + "20185": "Stafford County, Kansas", + "20187": "Stanton County, Kansas", + "20189": "Stevens County, Kansas", + "20191": "Sumner County, Kansas", + "20193": "Thomas County, Kansas", + "20195": "Trego County, Kansas", + "20197": "Wabaunsee County, Kansas", + "20199": "Wallace County, Kansas", + "20201": "Washington County, Kansas", + "20203": "Wichita County, Kansas", + "20205": "Wilson County, Kansas", + "20207": "Woodson County, Kansas", + "20209": "Wyandotte County, Kansas", + "21001": "Adair County, Kentucky", + "21003": "Allen County, Kentucky", + "21005": "Anderson County, Kentucky", + "21007": "Ballard County, Kentucky", + "21009": "Barren County, Kentucky", + "21011": "Bath County, Kentucky", + "21013": "Bell County, Kentucky", + "21015": "Boone County, Kentucky", + "21017": "Bourbon County, Kentucky", + "21019": "Boyd County, Kentucky", + "21021": "Boyle County, Kentucky", + "21023": "Bracken County, Kentucky", + "21025": "Breathitt County, Kentucky", + "21027": "Breckinridge County, Kentucky", + "21029": "Bullitt County, Kentucky", + "21031": "Butler County, Kentucky", + "21033": "Caldwell County, Kentucky", + "21035": "Calloway County, Kentucky", + "21037": "Campbell County, Kentucky", + "21039": "Carlisle County, Kentucky", + "21041": "Carroll County, Kentucky", + "21043": "Carter County, Kentucky", + "21045": "Casey County, Kentucky", + "21047": "Christian County, Kentucky", + "21049": "Clark County, Kentucky", + "21051": "Clay County, Kentucky", + "21053": "Clinton County, Kentucky", + "21055": "Crittenden County, Kentucky", + "21057": "Cumberland County, Kentucky", + "21059": "Daviess County, Kentucky", + "21061": "Edmonson County, Kentucky", + "21063": "Elliott County, Kentucky", + "21065": "Estill County, Kentucky", + "21067": "Fayette County, Kentucky", + "21069": "Fleming County, Kentucky", + "21071": "Floyd County, Kentucky", + "21073": "Franklin County, Kentucky", + "21075": "Fulton County, Kentucky", + "21077": "Gallatin County, Kentucky", + "21079": "Garrard County, Kentucky", + "21081": "Grant County, Kentucky", + "21083": "Graves County, Kentucky", + "21085": "Grayson County, Kentucky", + "21087": "Green County, Kentucky", + "21089": "Greenup County, Kentucky", + "21091": "Hancock County, Kentucky", + "21093": "Hardin County, Kentucky", + "21095": "Harlan County, Kentucky", + "21097": "Harrison County, Kentucky", + "21099": "Hart County, Kentucky", + "21101": "Henderson County, Kentucky", + "21103": "Henry County, Kentucky", + "21105": "Hickman County, Kentucky", + "21107": "Hopkins County, Kentucky", + "21109": "Jackson County, Kentucky", + "21111": "Jefferson County, Kentucky", + "21113": "Jessamine County, Kentucky", + "21115": "Johnson County, Kentucky", + "21117": "Kenton County, Kentucky", + "21119": "Knott County, Kentucky", + "21121": "Knox County, Kentucky", + "21123": "Larue County, Kentucky", + "21125": "Laurel County, Kentucky", + "21127": "Lawrence County, Kentucky", + "21129": "Lee County, Kentucky", + "21131": "Leslie County, Kentucky", + "21133": "Letcher County, Kentucky", + "21135": "Lewis County, Kentucky", + "21137": "Lincoln County, Kentucky", + "21139": "Livingston County, Kentucky", + "21141": "Logan County, Kentucky", + "21143": "Lyon County, Kentucky", + "21145": "McCracken County, Kentucky", + "21147": "McCreary County, Kentucky", + "21149": "McLean County, Kentucky", + "21151": "Madison County, Kentucky", + "21153": "Magoffin County, Kentucky", + "21155": "Marion County, Kentucky", + "21157": "Marshall County, Kentucky", + "21159": "Martin County, Kentucky", + "21161": "Mason County, Kentucky", + "21163": "Meade County, Kentucky", + "21165": "Menifee County, Kentucky", + "21167": "Mercer County, Kentucky", + "21169": "Metcalfe County, Kentucky", + "21171": "Monroe County, Kentucky", + "21173": "Montgomery County, Kentucky", + "21175": "Morgan County, Kentucky", + "21177": "Muhlenberg County, Kentucky", + "21179": "Nelson County, Kentucky", + "21181": "Nicholas County, Kentucky", + "21183": "Ohio County, Kentucky", + "21185": "Oldham County, Kentucky", + "21187": "Owen County, Kentucky", + "21189": "Owsley County, Kentucky", + "21191": "Pendleton County, Kentucky", + "21193": "Perry County, Kentucky", + "21195": "Pike County, Kentucky", + "21197": "Powell County, Kentucky", + "21199": "Pulaski County, Kentucky", + "21201": "Robertson County, Kentucky", + "21203": "Rockcastle County, Kentucky", + "21205": "Rowan County, Kentucky", + "21207": "Russell County, Kentucky", + "21209": "Scott County, Kentucky", + "21211": "Shelby County, Kentucky", + "21213": "Simpson County, Kentucky", + "21215": "Spencer County, Kentucky", + "21217": "Taylor County, Kentucky", + "21219": "Todd County, Kentucky", + "21221": "Trigg County, Kentucky", + "21223": "Trimble County, Kentucky", + "21225": "Union County, Kentucky", + "21227": "Warren County, Kentucky", + "21229": "Washington County, Kentucky", + "21231": "Wayne County, Kentucky", + "21233": "Webster County, Kentucky", + "21235": "Whitley County, Kentucky", + "21237": "Wolfe County, Kentucky", + "21239": "Woodford County, Kentucky", + "22001": "Acadia Parish, Louisiana", + "22003": "Allen Parish, Louisiana", + "22005": "Ascension Parish, Louisiana", + "22007": "Assumption Parish, Louisiana", + "22009": "Avoyelles Parish, Louisiana", + "22011": "Beauregard Parish, Louisiana", + "22013": "Bienville Parish, Louisiana", + "22015": "Bossier Parish, Louisiana", + "22017": "Caddo Parish, Louisiana", + "22019": "Calcasieu Parish, Louisiana", + "22021": "Caldwell Parish, Louisiana", + "22023": "Cameron Parish, Louisiana", + "22025": "Catahoula Parish, Louisiana", + "22027": "Claiborne Parish, Louisiana", + "22029": "Concordia Parish, Louisiana", + "22031": "De Soto Parish, Louisiana", + "22033": "East Baton Rouge Parish, Louisiana", + "22035": "East Carroll Parish, Louisiana", + "22037": "East Feliciana Parish, Louisiana", + "22039": "Evangeline Parish, Louisiana", + "22041": "Franklin Parish, Louisiana", + "22043": "Grant Parish, Louisiana", + "22045": "Iberia Parish, Louisiana", + "22047": "Iberville Parish, Louisiana", + "22049": "Jackson Parish, Louisiana", + "22051": "Jefferson Parish, Louisiana", + "22053": "Jefferson Davis Parish, Louisiana", + "22055": "Lafayette Parish, Louisiana", + "22057": "Lafourche Parish, Louisiana", + "22059": "LaSalle Parish, Louisiana", + "22061": "Lincoln Parish, Louisiana", + "22063": "Livingston Parish, Louisiana", + "22065": "Madison Parish, Louisiana", + "22067": "Morehouse Parish, Louisiana", + "22069": "Natchitoches Parish, Louisiana", + "22071": "Orleans Parish, Louisiana", + "22073": "Ouachita Parish, Louisiana", + "22075": "Plaquemines Parish, Louisiana", + "22077": "Pointe Coupee Parish, Louisiana", + "22079": "Rapides Parish, Louisiana", + "22081": "Red River Parish, Louisiana", + "22083": "Richland Parish, Louisiana", + "22085": "Sabine Parish, Louisiana", + "22087": "St. Bernard Parish, Louisiana", + "22089": "St. Charles Parish, Louisiana", + "22091": "St. Helena Parish, Louisiana", + "22093": "St. James Parish, Louisiana", + "22095": "St. John the Baptist Parish, Louisiana", + "22097": "St. Landry Parish, Louisiana", + "22099": "St. Martin Parish, Louisiana", + "22101": "St. Mary Parish, Louisiana", + "22103": "St. Tammany Parish, Louisiana", + "22105": "Tangipahoa Parish, Louisiana", + "22107": "Tensas Parish, Louisiana", + "22109": "Terrebonne Parish, Louisiana", + "22111": "Union Parish, Louisiana", + "22113": "Vermilion Parish, Louisiana", + "22115": "Vernon Parish, Louisiana", + "22117": "Washington Parish, Louisiana", + "22119": "Webster Parish, Louisiana", + "22121": "West Baton Rouge Parish, Louisiana", + "22123": "West Carroll Parish, Louisiana", + "22125": "West Feliciana Parish, Louisiana", + "22127": "Winn Parish, Louisiana", + "23001": "Androscoggin County, Maine", + "23003": "Aroostook County, Maine", + "23005": "Cumberland County, Maine", + "23007": "Franklin County, Maine", + "23009": "Hancock County, Maine", + "23011": "Kennebec County, Maine", + "23013": "Knox County, Maine", + "23015": "Lincoln County, Maine", + "23017": "Oxford County, Maine", + "23019": "Penobscot County, Maine", + "23021": "Piscataquis County, Maine", + "23023": "Sagadahoc County, Maine", + "23025": "Somerset County, Maine", + "23027": "Waldo County, Maine", + "23029": "Washington County, Maine", + "23031": "York County, Maine", + "24001": "Allegany County, Maryland", + "24003": "Anne Arundel County, Maryland", + "24005": "Baltimore County, Maryland", + "24009": "Calvert County, Maryland", + "24011": "Caroline County, Maryland", + "24013": "Carroll County, Maryland", + "24015": "Cecil County, Maryland", + "24017": "Charles County, Maryland", + "24019": "Dorchester County, Maryland", + "24021": "Frederick County, Maryland", + "24023": "Garrett County, Maryland", + "24025": "Harford County, Maryland", + "24027": "Howard County, Maryland", + "24029": "Kent County, Maryland", + "24031": "Montgomery County, Maryland", + "24033": "Prince George's County, Maryland", + "24035": "Queen Anne's County, Maryland", + "24037": "St. Mary's County, Maryland", + "24039": "Somerset County, Maryland", + "24041": "Talbot County, Maryland", + "24043": "Washington County, Maryland", + "24045": "Wicomico County, Maryland", + "24047": "Worcester County, Maryland", + "24510": "Baltimore city, Maryland", + "25001": "Barnstable County, Massachusetts", + "25003": "Berkshire County, Massachusetts", + "25005": "Bristol County, Massachusetts", + "25007": "Dukes County, Massachusetts", + "25009": "Essex County, Massachusetts", + "25011": "Franklin County, Massachusetts", + "25013": "Hampden County, Massachusetts", + "25015": "Hampshire County, Massachusetts", + "25017": "Middlesex County, Massachusetts", + "25019": "Nantucket County, Massachusetts", + "25021": "Norfolk County, Massachusetts", + "25023": "Plymouth County, Massachusetts", + "25025": "Suffolk County, Massachusetts", + "25027": "Worcester County, Massachusetts", + "26001": "Alcona County, Michigan", + "26003": "Alger County, Michigan", + "26005": "Allegan County, Michigan", + "26007": "Alpena County, Michigan", + "26009": "Antrim County, Michigan", + "26011": "Arenac County, Michigan", + "26013": "Baraga County, Michigan", + "26015": "Barry County, Michigan", + "26017": "Bay County, Michigan", + "26019": "Benzie County, Michigan", + "26021": "Berrien County, Michigan", + "26023": "Branch County, Michigan", + "26025": "Calhoun County, Michigan", + "26027": "Cass County, Michigan", + "26029": "Charlevoix County, Michigan", + "26031": "Cheboygan County, Michigan", + "26033": "Chippewa County, Michigan", + "26035": "Clare County, Michigan", + "26037": "Clinton County, Michigan", + "26039": "Crawford County, Michigan", + "26041": "Delta County, Michigan", + "26043": "Dickinson County, Michigan", + "26045": "Eaton County, Michigan", + "26047": "Emmet County, Michigan", + "26049": "Genesee County, Michigan", + "26051": "Gladwin County, Michigan", + "26053": "Gogebic County, Michigan", + "26055": "Grand Traverse County, Michigan", + "26057": "Gratiot County, Michigan", + "26059": "Hillsdale County, Michigan", + "26061": "Houghton County, Michigan", + "26063": "Huron County, Michigan", + "26065": "Ingham County, Michigan", + "26067": "Ionia County, Michigan", + "26069": "Iosco County, Michigan", + "26071": "Iron County, Michigan", + "26073": "Isabella County, Michigan", + "26075": "Jackson County, Michigan", + "26077": "Kalamazoo County, Michigan", + "26079": "Kalkaska County, Michigan", + "26081": "Kent County, Michigan", + "26083": "Keweenaw County, Michigan", + "26085": "Lake County, Michigan", + "26087": "Lapeer County, Michigan", + "26089": "Leelanau County, Michigan", + "26091": "Lenawee County, Michigan", + "26093": "Livingston County, Michigan", + "26095": "Luce County, Michigan", + "26097": "Mackinac County, Michigan", + "26099": "Macomb County, Michigan", + "26101": "Manistee County, Michigan", + "26103": "Marquette County, Michigan", + "26105": "Mason County, Michigan", + "26107": "Mecosta County, Michigan", + "26109": "Menominee County, Michigan", + "26111": "Midland County, Michigan", + "26113": "Missaukee County, Michigan", + "26115": "Monroe County, Michigan", + "26117": "Montcalm County, Michigan", + "26119": "Montmorency County, Michigan", + "26121": "Muskegon County, Michigan", + "26123": "Newaygo County, Michigan", + "26125": "Oakland County, Michigan", + "26127": "Oceana County, Michigan", + "26129": "Ogemaw County, Michigan", + "26131": "Ontonagon County, Michigan", + "26133": "Osceola County, Michigan", + "26135": "Oscoda County, Michigan", + "26137": "Otsego County, Michigan", + "26139": "Ottawa County, Michigan", + "26141": "Presque Isle County, Michigan", + "26143": "Roscommon County, Michigan", + "26145": "Saginaw County, Michigan", + "26147": "St. Clair County, Michigan", + "26149": "St. Joseph County, Michigan", + "26151": "Sanilac County, Michigan", + "26153": "Schoolcraft County, Michigan", + "26155": "Shiawassee County, Michigan", + "26157": "Tuscola County, Michigan", + "26159": "Van Buren County, Michigan", + "26161": "Washtenaw County, Michigan", + "26163": "Wayne County, Michigan", + "26165": "Wexford County, Michigan", + "27001": "Aitkin County, Minnesota", + "27003": "Anoka County, Minnesota", + "27005": "Becker County, Minnesota", + "27007": "Beltrami County, Minnesota", + "27009": "Benton County, Minnesota", + "27011": "Big Stone County, Minnesota", + "27013": "Blue Earth County, Minnesota", + "27015": "Brown County, Minnesota", + "27017": "Carlton County, Minnesota", + "27019": "Carver County, Minnesota", + "27021": "Cass County, Minnesota", + "27023": "Chippewa County, Minnesota", + "27025": "Chisago County, Minnesota", + "27027": "Clay County, Minnesota", + "27029": "Clearwater County, Minnesota", + "27031": "Cook County, Minnesota", + "27033": "Cottonwood County, Minnesota", + "27035": "Crow Wing County, Minnesota", + "27037": "Dakota County, Minnesota", + "27039": "Dodge County, Minnesota", + "27041": "Douglas County, Minnesota", + "27043": "Faribault County, Minnesota", + "27045": "Fillmore County, Minnesota", + "27047": "Freeborn County, Minnesota", + "27049": "Goodhue County, Minnesota", + "27051": "Grant County, Minnesota", + "27053": "Hennepin County, Minnesota", + "27055": "Houston County, Minnesota", + "27057": "Hubbard County, Minnesota", + "27059": "Isanti County, Minnesota", + "27061": "Itasca County, Minnesota", + "27063": "Jackson County, Minnesota", + "27065": "Kanabec County, Minnesota", + "27067": "Kandiyohi County, Minnesota", + "27069": "Kittson County, Minnesota", + "27071": "Koochiching County, Minnesota", + "27073": "Lac qui Parle County, Minnesota", + "27075": "Lake County, Minnesota", + "27077": "Lake of the Woods County, Minnesota", + "27079": "Le Sueur County, Minnesota", + "27081": "Lincoln County, Minnesota", + "27083": "Lyon County, Minnesota", + "27085": "McLeod County, Minnesota", + "27087": "Mahnomen County, Minnesota", + "27089": "Marshall County, Minnesota", + "27091": "Martin County, Minnesota", + "27093": "Meeker County, Minnesota", + "27095": "Mille Lacs County, Minnesota", + "27097": "Morrison County, Minnesota", + "27099": "Mower County, Minnesota", + "27101": "Murray County, Minnesota", + "27103": "Nicollet County, Minnesota", + "27105": "Nobles County, Minnesota", + "27107": "Norman County, Minnesota", + "27109": "Olmsted County, Minnesota", + "27111": "Otter Tail County, Minnesota", + "27113": "Pennington County, Minnesota", + "27115": "Pine County, Minnesota", + "27117": "Pipestone County, Minnesota", + "27119": "Polk County, Minnesota", + "27121": "Pope County, Minnesota", + "27123": "Ramsey County, Minnesota", + "27125": "Red Lake County, Minnesota", + "27127": "Redwood County, Minnesota", + "27129": "Renville County, Minnesota", + "27131": "Rice County, Minnesota", + "27133": "Rock County, Minnesota", + "27135": "Roseau County, Minnesota", + "27137": "St. Louis County, Minnesota", + "27139": "Scott County, Minnesota", + "27141": "Sherburne County, Minnesota", + "27143": "Sibley County, Minnesota", + "27145": "Stearns County, Minnesota", + "27147": "Steele County, Minnesota", + "27149": "Stevens County, Minnesota", + "27151": "Swift County, Minnesota", + "27153": "Todd County, Minnesota", + "27155": "Traverse County, Minnesota", + "27157": "Wabasha County, Minnesota", + "27159": "Wadena County, Minnesota", + "27161": "Waseca County, Minnesota", + "27163": "Washington County, Minnesota", + "27165": "Watonwan County, Minnesota", + "27167": "Wilkin County, Minnesota", + "27169": "Winona County, Minnesota", + "27171": "Wright County, Minnesota", + "27173": "Yellow Medicine County, Minnesota", + "28001": "Adams County, Mississippi", + "28003": "Alcorn County, Mississippi", + "28005": "Amite County, Mississippi", + "28007": "Attala County, Mississippi", + "28009": "Benton County, Mississippi", + "28011": "Bolivar County, Mississippi", + "28013": "Calhoun County, Mississippi", + "28015": "Carroll County, Mississippi", + "28017": "Chickasaw County, Mississippi", + "28019": "Choctaw County, Mississippi", + "28021": "Claiborne County, Mississippi", + "28023": "Clarke County, Mississippi", + "28025": "Clay County, Mississippi", + "28027": "Coahoma County, Mississippi", + "28029": "Copiah County, Mississippi", + "28031": "Covington County, Mississippi", + "28033": "DeSoto County, Mississippi", + "28035": "Forrest County, Mississippi", + "28037": "Franklin County, Mississippi", + "28039": "George County, Mississippi", + "28041": "Greene County, Mississippi", + "28043": "Grenada County, Mississippi", + "28045": "Hancock County, Mississippi", + "28047": "Harrison County, Mississippi", + "28049": "Hinds County, Mississippi", + "28051": "Holmes County, Mississippi", + "28053": "Humphreys County, Mississippi", + "28055": "Issaquena County, Mississippi", + "28057": "Itawamba County, Mississippi", + "28059": "Jackson County, Mississippi", + "28061": "Jasper County, Mississippi", + "28063": "Jefferson County, Mississippi", + "28065": "Jefferson Davis County, Mississippi", + "28067": "Jones County, Mississippi", + "28069": "Kemper County, Mississippi", + "28071": "Lafayette County, Mississippi", + "28073": "Lamar County, Mississippi", + "28075": "Lauderdale County, Mississippi", + "28077": "Lawrence County, Mississippi", + "28079": "Leake County, Mississippi", + "28081": "Lee County, Mississippi", + "28083": "Leflore County, Mississippi", + "28085": "Lincoln County, Mississippi", + "28087": "Lowndes County, Mississippi", + "28089": "Madison County, Mississippi", + "28091": "Marion County, Mississippi", + "28093": "Marshall County, Mississippi", + "28095": "Monroe County, Mississippi", + "28097": "Montgomery County, Mississippi", + "28099": "Neshoba County, Mississippi", + "28101": "Newton County, Mississippi", + "28103": "Noxubee County, Mississippi", + "28105": "Oktibbeha County, Mississippi", + "28107": "Panola County, Mississippi", + "28109": "Pearl River County, Mississippi", + "28111": "Perry County, Mississippi", + "28113": "Pike County, Mississippi", + "28115": "Pontotoc County, Mississippi", + "28117": "Prentiss County, Mississippi", + "28119": "Quitman County, Mississippi", + "28121": "Rankin County, Mississippi", + "28123": "Scott County, Mississippi", + "28125": "Sharkey County, Mississippi", + "28127": "Simpson County, Mississippi", + "28129": "Smith County, Mississippi", + "28131": "Stone County, Mississippi", + "28133": "Sunflower County, Mississippi", + "28135": "Tallahatchie County, Mississippi", + "28137": "Tate County, Mississippi", + "28139": "Tippah County, Mississippi", + "28141": "Tishomingo County, Mississippi", + "28143": "Tunica County, Mississippi", + "28145": "Union County, Mississippi", + "28147": "Walthall County, Mississippi", + "28149": "Warren County, Mississippi", + "28151": "Washington County, Mississippi", + "28153": "Wayne County, Mississippi", + "28155": "Webster County, Mississippi", + "28157": "Wilkinson County, Mississippi", + "28159": "Winston County, Mississippi", + "28161": "Yalobusha County, Mississippi", + "28163": "Yazoo County, Mississippi", + "29001": "Adair County, Missouri", + "29003": "Andrew County, Missouri", + "29005": "Atchison County, Missouri", + "29007": "Audrain County, Missouri", + "29009": "Barry County, Missouri", + "29011": "Barton County, Missouri", + "29013": "Bates County, Missouri", + "29015": "Benton County, Missouri", + "29017": "Bollinger County, Missouri", + "29019": "Boone County, Missouri", + "29021": "Buchanan County, Missouri", + "29023": "Butler County, Missouri", + "29025": "Caldwell County, Missouri", + "29027": "Callaway County, Missouri", + "29029": "Camden County, Missouri", + "29031": "Cape Girardeau County, Missouri", + "29033": "Carroll County, Missouri", + "29035": "Carter County, Missouri", + "29037": "Cass County, Missouri", + "29039": "Cedar County, Missouri", + "29041": "Chariton County, Missouri", + "29043": "Christian County, Missouri", + "29045": "Clark County, Missouri", + "29047": "Clay County, Missouri", + "29049": "Clinton County, Missouri", + "29051": "Cole County, Missouri", + "29053": "Cooper County, Missouri", + "29055": "Crawford County, Missouri", + "29057": "Dade County, Missouri", + "29059": "Dallas County, Missouri", + "29061": "Daviess County, Missouri", + "29063": "DeKalb County, Missouri", + "29065": "Dent County, Missouri", + "29067": "Douglas County, Missouri", + "29069": "Dunklin County, Missouri", + "29071": "Franklin County, Missouri", + "29073": "Gasconade County, Missouri", + "29075": "Gentry County, Missouri", + "29077": "Greene County, Missouri", + "29079": "Grundy County, Missouri", + "29081": "Harrison County, Missouri", + "29083": "Henry County, Missouri", + "29085": "Hickory County, Missouri", + "29087": "Holt County, Missouri", + "29089": "Howard County, Missouri", + "29091": "Howell County, Missouri", + "29093": "Iron County, Missouri", + "29095": "Jackson County, Missouri", + "29097": "Jasper County, Missouri", + "29099": "Jefferson County, Missouri", + "29101": "Johnson County, Missouri", + "29103": "Knox County, Missouri", + "29105": "Laclede County, Missouri", + "29107": "Lafayette County, Missouri", + "29109": "Lawrence County, Missouri", + "29111": "Lewis County, Missouri", + "29113": "Lincoln County, Missouri", + "29115": "Linn County, Missouri", + "29117": "Livingston County, Missouri", + "29119": "McDonald County, Missouri", + "29121": "Macon County, Missouri", + "29123": "Madison County, Missouri", + "29125": "Maries County, Missouri", + "29127": "Marion County, Missouri", + "29129": "Mercer County, Missouri", + "29131": "Miller County, Missouri", + "29133": "Mississippi County, Missouri", + "29135": "Moniteau County, Missouri", + "29137": "Monroe County, Missouri", + "29139": "Montgomery County, Missouri", + "29141": "Morgan County, Missouri", + "29143": "New Madrid County, Missouri", + "29145": "Newton County, Missouri", + "29147": "Nodaway County, Missouri", + "29149": "Oregon County, Missouri", + "29151": "Osage County, Missouri", + "29153": "Ozark County, Missouri", + "29155": "Pemiscot County, Missouri", + "29157": "Perry County, Missouri", + "29159": "Pettis County, Missouri", + "29161": "Phelps County, Missouri", + "29163": "Pike County, Missouri", + "29165": "Platte County, Missouri", + "29167": "Polk County, Missouri", + "29169": "Pulaski County, Missouri", + "29171": "Putnam County, Missouri", + "29173": "Ralls County, Missouri", + "29175": "Randolph County, Missouri", + "29177": "Ray County, Missouri", + "29179": "Reynolds County, Missouri", + "29181": "Ripley County, Missouri", + "29183": "St. Charles County, Missouri", + "29185": "St. Clair County, Missouri", + "29186": "Ste. Genevieve County, Missouri", + "29187": "St. Francois County, Missouri", + "29189": "St. Louis County, Missouri", + "29195": "Saline County, Missouri", + "29197": "Schuyler County, Missouri", + "29199": "Scotland County, Missouri", + "29201": "Scott County, Missouri", + "29203": "Shannon County, Missouri", + "29205": "Shelby County, Missouri", + "29207": "Stoddard County, Missouri", + "29209": "Stone County, Missouri", + "29211": "Sullivan County, Missouri", + "29213": "Taney County, Missouri", + "29215": "Texas County, Missouri", + "29217": "Vernon County, Missouri", + "29219": "Warren County, Missouri", + "29221": "Washington County, Missouri", + "29223": "Wayne County, Missouri", + "29225": "Webster County, Missouri", + "29227": "Worth County, Missouri", + "29229": "Wright County, Missouri", + "29510": "St. Louis city, Missouri", + "30001": "Beaverhead County, Montana", + "30003": "Big Horn County, Montana", + "30005": "Blaine County, Montana", + "30007": "Broadwater County, Montana", + "30009": "Carbon County, Montana", + "30011": "Carter County, Montana", + "30013": "Cascade County, Montana", + "30015": "Chouteau County, Montana", + "30017": "Custer County, Montana", + "30019": "Daniels County, Montana", + "30021": "Dawson County, Montana", + "30023": "Deer Lodge County, Montana", + "30025": "Fallon County, Montana", + "30027": "Fergus County, Montana", + "30029": "Flathead County, Montana", + "30031": "Gallatin County, Montana", + "30033": "Garfield County, Montana", + "30035": "Glacier County, Montana", + "30037": "Golden Valley County, Montana", + "30039": "Granite County, Montana", + "30041": "Hill County, Montana", + "30043": "Jefferson County, Montana", + "30045": "Judith Basin County, Montana", + "30047": "Lake County, Montana", + "30049": "Lewis and Clark County, Montana", + "30051": "Liberty County, Montana", + "30053": "Lincoln County, Montana", + "30055": "McCone County, Montana", + "30057": "Madison County, Montana", + "30059": "Meagher County, Montana", + "30061": "Mineral County, Montana", + "30063": "Missoula County, Montana", + "30065": "Musselshell County, Montana", + "30067": "Park County, Montana", + "30069": "Petroleum County, Montana", + "30071": "Phillips County, Montana", + "30073": "Pondera County, Montana", + "30075": "Powder River County, Montana", + "30077": "Powell County, Montana", + "30079": "Prairie County, Montana", + "30081": "Ravalli County, Montana", + "30083": "Richland County, Montana", + "30085": "Roosevelt County, Montana", + "30087": "Rosebud County, Montana", + "30089": "Sanders County, Montana", + "30091": "Sheridan County, Montana", + "30093": "Silver Bow County, Montana", + "30095": "Stillwater County, Montana", + "30097": "Sweet Grass County, Montana", + "30099": "Teton County, Montana", + "30101": "Toole County, Montana", + "30103": "Treasure County, Montana", + "30105": "Valley County, Montana", + "30107": "Wheatland County, Montana", + "30109": "Wibaux County, Montana", + "30111": "Yellowstone County, Montana", + "31001": "Adams County, Nebraska", + "31003": "Antelope County, Nebraska", + "31005": "Arthur County, Nebraska", + "31007": "Banner County, Nebraska", + "31009": "Blaine County, Nebraska", + "31011": "Boone County, Nebraska", + "31013": "Box Butte County, Nebraska", + "31015": "Boyd County, Nebraska", + "31017": "Brown County, Nebraska", + "31019": "Buffalo County, Nebraska", + "31021": "Burt County, Nebraska", + "31023": "Butler County, Nebraska", + "31025": "Cass County, Nebraska", + "31027": "Cedar County, Nebraska", + "31029": "Chase County, Nebraska", + "31031": "Cherry County, Nebraska", + "31033": "Cheyenne County, Nebraska", + "31035": "Clay County, Nebraska", + "31037": "Colfax County, Nebraska", + "31039": "Cuming County, Nebraska", + "31041": "Custer County, Nebraska", + "31043": "Dakota County, Nebraska", + "31045": "Dawes County, Nebraska", + "31047": "Dawson County, Nebraska", + "31049": "Deuel County, Nebraska", + "31051": "Dixon County, Nebraska", + "31053": "Dodge County, Nebraska", + "31055": "Douglas County, Nebraska", + "31057": "Dundy County, Nebraska", + "31059": "Fillmore County, Nebraska", + "31061": "Franklin County, Nebraska", + "31063": "Frontier County, Nebraska", + "31065": "Furnas County, Nebraska", + "31067": "Gage County, Nebraska", + "31069": "Garden County, Nebraska", + "31071": "Garfield County, Nebraska", + "31073": "Gosper County, Nebraska", + "31075": "Grant County, Nebraska", + "31077": "Greeley County, Nebraska", + "31079": "Hall County, Nebraska", + "31081": "Hamilton County, Nebraska", + "31083": "Harlan County, Nebraska", + "31085": "Hayes County, Nebraska", + "31087": "Hitchcock County, Nebraska", + "31089": "Holt County, Nebraska", + "31091": "Hooker County, Nebraska", + "31093": "Howard County, Nebraska", + "31095": "Jefferson County, Nebraska", + "31097": "Johnson County, Nebraska", + "31099": "Kearney County, Nebraska", + "31101": "Keith County, Nebraska", + "31103": "Keya Paha County, Nebraska", + "31105": "Kimball County, Nebraska", + "31107": "Knox County, Nebraska", + "31109": "Lancaster County, Nebraska", + "31111": "Lincoln County, Nebraska", + "31113": "Logan County, Nebraska", + "31115": "Loup County, Nebraska", + "31117": "McPherson County, Nebraska", + "31119": "Madison County, Nebraska", + "31121": "Merrick County, Nebraska", + "31123": "Morrill County, Nebraska", + "31125": "Nance County, Nebraska", + "31127": "Nemaha County, Nebraska", + "31129": "Nuckolls County, Nebraska", + "31131": "Otoe County, Nebraska", + "31133": "Pawnee County, Nebraska", + "31135": "Perkins County, Nebraska", + "31137": "Phelps County, Nebraska", + "31139": "Pierce County, Nebraska", + "31141": "Platte County, Nebraska", + "31143": "Polk County, Nebraska", + "31145": "Red Willow County, Nebraska", + "31147": "Richardson County, Nebraska", + "31149": "Rock County, Nebraska", + "31151": "Saline County, Nebraska", + "31153": "Sarpy County, Nebraska", + "31155": "Saunders County, Nebraska", + "31157": "Scotts Bluff County, Nebraska", + "31159": "Seward County, Nebraska", + "31161": "Sheridan County, Nebraska", + "31163": "Sherman County, Nebraska", + "31165": "Sioux County, Nebraska", + "31167": "Stanton County, Nebraska", + "31169": "Thayer County, Nebraska", + "31171": "Thomas County, Nebraska", + "31173": "Thurston County, Nebraska", + "31175": "Valley County, Nebraska", + "31177": "Washington County, Nebraska", + "31179": "Wayne County, Nebraska", + "31181": "Webster County, Nebraska", + "31183": "Wheeler County, Nebraska", + "31185": "York County, Nebraska", + "32001": "Churchill County, Nevada", + "32003": "Clark County, Nevada", + "32005": "Douglas County, Nevada", + "32007": "Elko County, Nevada", + "32009": "Esmeralda County, Nevada", + "32011": "Eureka County, Nevada", + "32013": "Humboldt County, Nevada", + "32015": "Lander County, Nevada", + "32017": "Lincoln County, Nevada", + "32019": "Lyon County, Nevada", + "32021": "Mineral County, Nevada", + "32023": "Nye County, Nevada", + "32027": "Pershing County, Nevada", + "32029": "Storey County, Nevada", + "32031": "Washoe County, Nevada", + "32033": "White Pine County, Nevada", + "32510": "Carson City, Nevada", + "33001": "Belknap County, New Hampshire", + "33003": "Carroll County, New Hampshire", + "33005": "Cheshire County, New Hampshire", + "33007": "Coos County, New Hampshire", + "33009": "Grafton County, New Hampshire", + "33011": "Hillsborough County, New Hampshire", + "33013": "Merrimack County, New Hampshire", + "33015": "Rockingham County, New Hampshire", + "33017": "Strafford County, New Hampshire", + "33019": "Sullivan County, New Hampshire", + "34001": "Atlantic County, New Jersey", + "34003": "Bergen County, New Jersey", + "34005": "Burlington County, New Jersey", + "34007": "Camden County, New Jersey", + "34009": "Cape May County, New Jersey", + "34011": "Cumberland County, New Jersey", + "34013": "Essex County, New Jersey", + "34015": "Gloucester County, New Jersey", + "34017": "Hudson County, New Jersey", + "34019": "Hunterdon County, New Jersey", + "34021": "Mercer County, New Jersey", + "34023": "Middlesex County, New Jersey", + "34025": "Monmouth County, New Jersey", + "34027": "Morris County, New Jersey", + "34029": "Ocean County, New Jersey", + "34031": "Passaic County, New Jersey", + "34033": "Salem County, New Jersey", + "34035": "Somerset County, New Jersey", + "34037": "Sussex County, New Jersey", + "34039": "Union County, New Jersey", + "34041": "Warren County, New Jersey", + "35001": "Bernalillo County, New Mexico", + "35003": "Catron County, New Mexico", + "35005": "Chaves County, New Mexico", + "35006": "Cibola County, New Mexico", + "35007": "Colfax County, New Mexico", + "35009": "Curry County, New Mexico", + "35011": "De Baca County, New Mexico", + "35013": "Doña Ana County, New Mexico", + "35015": "Eddy County, New Mexico", + "35017": "Grant County, New Mexico", + "35019": "Guadalupe County, New Mexico", + "35021": "Harding County, New Mexico", + "35023": "Hidalgo County, New Mexico", + "35025": "Lea County, New Mexico", + "35027": "Lincoln County, New Mexico", + "35028": "Los Alamos County, New Mexico", + "35029": "Luna County, New Mexico", + "35031": "McKinley County, New Mexico", + "35033": "Mora County, New Mexico", + "35035": "Otero County, New Mexico", + "35037": "Quay County, New Mexico", + "35039": "Rio Arriba County, New Mexico", + "35041": "Roosevelt County, New Mexico", + "35043": "Sandoval County, New Mexico", + "35045": "San Juan County, New Mexico", + "35047": "San Miguel County, New Mexico", + "35049": "Santa Fe County, New Mexico", + "35051": "Sierra County, New Mexico", + "35053": "Socorro County, New Mexico", + "35055": "Taos County, New Mexico", + "35057": "Torrance County, New Mexico", + "35059": "Union County, New Mexico", + "35061": "Valencia County, New Mexico", + "36001": "Albany County, New York", + "36003": "Allegany County, New York", + "36005": "Bronx County, New York", + "36007": "Broome County, New York", + "36009": "Cattaraugus County, New York", + "36011": "Cayuga County, New York", + "36013": "Chautauqua County, New York", + "36015": "Chemung County, New York", + "36017": "Chenango County, New York", + "36019": "Clinton County, New York", + "36021": "Columbia County, New York", + "36023": "Cortland County, New York", + "36025": "Delaware County, New York", + "36027": "Dutchess County, New York", + "36029": "Erie County, New York", + "36031": "Essex County, New York", + "36033": "Franklin County, New York", + "36035": "Fulton County, New York", + "36037": "Genesee County, New York", + "36039": "Greene County, New York", + "36041": "Hamilton County, New York", + "36043": "Herkimer County, New York", + "36045": "Jefferson County, New York", + "36047": "Kings County, New York", + "36049": "Lewis County, New York", + "36051": "Livingston County, New York", + "36053": "Madison County, New York", + "36055": "Monroe County, New York", + "36057": "Montgomery County, New York", + "36059": "Nassau County, New York", + "36061": "New York County, New York", + "36063": "Niagara County, New York", + "36065": "Oneida County, New York", + "36067": "Onondaga County, New York", + "36069": "Ontario County, New York", + "36071": "Orange County, New York", + "36073": "Orleans County, New York", + "36075": "Oswego County, New York", + "36077": "Otsego County, New York", + "36079": "Putnam County, New York", + "36081": "Queens County, New York", + "36083": "Rensselaer County, New York", + "36085": "Richmond County, New York", + "36087": "Rockland County, New York", + "36089": "St. Lawrence County, New York", + "36091": "Saratoga County, New York", + "36093": "Schenectady County, New York", + "36095": "Schoharie County, New York", + "36097": "Schuyler County, New York", + "36099": "Seneca County, New York", + "36101": "Steuben County, New York", + "36103": "Suffolk County, New York", + "36105": "Sullivan County, New York", + "36107": "Tioga County, New York", + "36109": "Tompkins County, New York", + "36111": "Ulster County, New York", + "36113": "Warren County, New York", + "36115": "Washington County, New York", + "36117": "Wayne County, New York", + "36119": "Westchester County, New York", + "36121": "Wyoming County, New York", + "36123": "Yates County, New York", + "37001": "Alamance County, North Carolina", + "37003": "Alexander County, North Carolina", + "37005": "Alleghany County, North Carolina", + "37007": "Anson County, North Carolina", + "37009": "Ashe County, North Carolina", + "37011": "Avery County, North Carolina", + "37013": "Beaufort County, North Carolina", + "37015": "Bertie County, North Carolina", + "37017": "Bladen County, North Carolina", + "37019": "Brunswick County, North Carolina", + "37021": "Buncombe County, North Carolina", + "37023": "Burke County, North Carolina", + "37025": "Cabarrus County, North Carolina", + "37027": "Caldwell County, North Carolina", + "37029": "Camden County, North Carolina", + "37031": "Carteret County, North Carolina", + "37033": "Caswell County, North Carolina", + "37035": "Catawba County, North Carolina", + "37037": "Chatham County, North Carolina", + "37039": "Cherokee County, North Carolina", + "37041": "Chowan County, North Carolina", + "37043": "Clay County, North Carolina", + "37045": "Cleveland County, North Carolina", + "37047": "Columbus County, North Carolina", + "37049": "Craven County, North Carolina", + "37051": "Cumberland County, North Carolina", + "37053": "Currituck County, North Carolina", + "37055": "Dare County, North Carolina", + "37057": "Davidson County, North Carolina", + "37059": "Davie County, North Carolina", + "37061": "Duplin County, North Carolina", + "37063": "Durham County, North Carolina", + "37065": "Edgecombe County, North Carolina", + "37067": "Forsyth County, North Carolina", + "37069": "Franklin County, North Carolina", + "37071": "Gaston County, North Carolina", + "37073": "Gates County, North Carolina", + "37075": "Graham County, North Carolina", + "37077": "Granville County, North Carolina", + "37079": "Greene County, North Carolina", + "37081": "Guilford County, North Carolina", + "37083": "Halifax County, North Carolina", + "37085": "Harnett County, North Carolina", + "37087": "Haywood County, North Carolina", + "37089": "Henderson County, North Carolina", + "37091": "Hertford County, North Carolina", + "37093": "Hoke County, North Carolina", + "37095": "Hyde County, North Carolina", + "37097": "Iredell County, North Carolina", + "37099": "Jackson County, North Carolina", + "37101": "Johnston County, North Carolina", + "37103": "Jones County, North Carolina", + "37105": "Lee County, North Carolina", + "37107": "Lenoir County, North Carolina", + "37109": "Lincoln County, North Carolina", + "37111": "McDowell County, North Carolina", + "37113": "Macon County, North Carolina", + "37115": "Madison County, North Carolina", + "37117": "Martin County, North Carolina", + "37119": "Mecklenburg County, North Carolina", + "37121": "Mitchell County, North Carolina", + "37123": "Montgomery County, North Carolina", + "37125": "Moore County, North Carolina", + "37127": "Nash County, North Carolina", + "37129": "New Hanover County, North Carolina", + "37131": "Northampton County, North Carolina", + "37133": "Onslow County, North Carolina", + "37135": "Orange County, North Carolina", + "37137": "Pamlico County, North Carolina", + "37139": "Pasquotank County, North Carolina", + "37141": "Pender County, North Carolina", + "37143": "Perquimans County, North Carolina", + "37145": "Person County, North Carolina", + "37147": "Pitt County, North Carolina", + "37149": "Polk County, North Carolina", + "37151": "Randolph County, North Carolina", + "37153": "Richmond County, North Carolina", + "37155": "Robeson County, North Carolina", + "37157": "Rockingham County, North Carolina", + "37159": "Rowan County, North Carolina", + "37161": "Rutherford County, North Carolina", + "37163": "Sampson County, North Carolina", + "37165": "Scotland County, North Carolina", + "37167": "Stanly County, North Carolina", + "37169": "Stokes County, North Carolina", + "37171": "Surry County, North Carolina", + "37173": "Swain County, North Carolina", + "37175": "Transylvania County, North Carolina", + "37177": "Tyrrell County, North Carolina", + "37179": "Union County, North Carolina", + "37181": "Vance County, North Carolina", + "37183": "Wake County, North Carolina", + "37185": "Warren County, North Carolina", + "37187": "Washington County, North Carolina", + "37189": "Watauga County, North Carolina", + "37191": "Wayne County, North Carolina", + "37193": "Wilkes County, North Carolina", + "37195": "Wilson County, North Carolina", + "37197": "Yadkin County, North Carolina", + "37199": "Yancey County, North Carolina", + "38001": "Adams County, North Dakota", + "38003": "Barnes County, North Dakota", + "38005": "Benson County, North Dakota", + "38007": "Billings County, North Dakota", + "38009": "Bottineau County, North Dakota", + "38011": "Bowman County, North Dakota", + "38013": "Burke County, North Dakota", + "38015": "Burleigh County, North Dakota", + "38017": "Cass County, North Dakota", + "38019": "Cavalier County, North Dakota", + "38021": "Dickey County, North Dakota", + "38023": "Divide County, North Dakota", + "38025": "Dunn County, North Dakota", + "38027": "Eddy County, North Dakota", + "38029": "Emmons County, North Dakota", + "38031": "Foster County, North Dakota", + "38033": "Golden Valley County, North Dakota", + "38035": "Grand Forks County, North Dakota", + "38037": "Grant County, North Dakota", + "38039": "Griggs County, North Dakota", + "38041": "Hettinger County, North Dakota", + "38043": "Kidder County, North Dakota", + "38045": "LaMoure County, North Dakota", + "38047": "Logan County, North Dakota", + "38049": "McHenry County, North Dakota", + "38051": "McIntosh County, North Dakota", + "38053": "McKenzie County, North Dakota", + "38055": "McLean County, North Dakota", + "38057": "Mercer County, North Dakota", + "38059": "Morton County, North Dakota", + "38061": "Mountrail County, North Dakota", + "38063": "Nelson County, North Dakota", + "38065": "Oliver County, North Dakota", + "38067": "Pembina County, North Dakota", + "38069": "Pierce County, North Dakota", + "38071": "Ramsey County, North Dakota", + "38073": "Ransom County, North Dakota", + "38075": "Renville County, North Dakota", + "38077": "Richland County, North Dakota", + "38079": "Rolette County, North Dakota", + "38081": "Sargent County, North Dakota", + "38083": "Sheridan County, North Dakota", + "38085": "Sioux County, North Dakota", + "38087": "Slope County, North Dakota", + "38089": "Stark County, North Dakota", + "38091": "Steele County, North Dakota", + "38093": "Stutsman County, North Dakota", + "38095": "Towner County, North Dakota", + "38097": "Traill County, North Dakota", + "38099": "Walsh County, North Dakota", + "38101": "Ward County, North Dakota", + "38103": "Wells County, North Dakota", + "38105": "Williams County, North Dakota", + "39001": "Adams County, Ohio", + "39003": "Allen County, Ohio", + "39005": "Ashland County, Ohio", + "39007": "Ashtabula County, Ohio", + "39009": "Athens County, Ohio", + "39011": "Auglaize County, Ohio", + "39013": "Belmont County, Ohio", + "39015": "Brown County, Ohio", + "39017": "Butler County, Ohio", + "39019": "Carroll County, Ohio", + "39021": "Champaign County, Ohio", + "39023": "Clark County, Ohio", + "39025": "Clermont County, Ohio", + "39027": "Clinton County, Ohio", + "39029": "Columbiana County, Ohio", + "39031": "Coshocton County, Ohio", + "39033": "Crawford County, Ohio", + "39035": "Cuyahoga County, Ohio", + "39037": "Darke County, Ohio", + "39039": "Defiance County, Ohio", + "39041": "Delaware County, Ohio", + "39043": "Erie County, Ohio", + "39045": "Fairfield County, Ohio", + "39047": "Fayette County, Ohio", + "39049": "Franklin County, Ohio", + "39051": "Fulton County, Ohio", + "39053": "Gallia County, Ohio", + "39055": "Geauga County, Ohio", + "39057": "Greene County, Ohio", + "39059": "Guernsey County, Ohio", + "39061": "Hamilton County, Ohio", + "39063": "Hancock County, Ohio", + "39065": "Hardin County, Ohio", + "39067": "Harrison County, Ohio", + "39069": "Henry County, Ohio", + "39071": "Highland County, Ohio", + "39073": "Hocking County, Ohio", + "39075": "Holmes County, Ohio", + "39077": "Huron County, Ohio", + "39079": "Jackson County, Ohio", + "39081": "Jefferson County, Ohio", + "39083": "Knox County, Ohio", + "39085": "Lake County, Ohio", + "39087": "Lawrence County, Ohio", + "39089": "Licking County, Ohio", + "39091": "Logan County, Ohio", + "39093": "Lorain County, Ohio", + "39095": "Lucas County, Ohio", + "39097": "Madison County, Ohio", + "39099": "Mahoning County, Ohio", + "39101": "Marion County, Ohio", + "39103": "Medina County, Ohio", + "39105": "Meigs County, Ohio", + "39107": "Mercer County, Ohio", + "39109": "Miami County, Ohio", + "39111": "Monroe County, Ohio", + "39113": "Montgomery County, Ohio", + "39115": "Morgan County, Ohio", + "39117": "Morrow County, Ohio", + "39119": "Muskingum County, Ohio", + "39121": "Noble County, Ohio", + "39123": "Ottawa County, Ohio", + "39125": "Paulding County, Ohio", + "39127": "Perry County, Ohio", + "39129": "Pickaway County, Ohio", + "39131": "Pike County, Ohio", + "39133": "Portage County, Ohio", + "39135": "Preble County, Ohio", + "39137": "Putnam County, Ohio", + "39139": "Richland County, Ohio", + "39141": "Ross County, Ohio", + "39143": "Sandusky County, Ohio", + "39145": "Scioto County, Ohio", + "39147": "Seneca County, Ohio", + "39149": "Shelby County, Ohio", + "39151": "Stark County, Ohio", + "39153": "Summit County, Ohio", + "39155": "Trumbull County, Ohio", + "39157": "Tuscarawas County, Ohio", + "39159": "Union County, Ohio", + "39161": "Van Wert County, Ohio", + "39163": "Vinton County, Ohio", + "39165": "Warren County, Ohio", + "39167": "Washington County, Ohio", + "39169": "Wayne County, Ohio", + "39171": "Williams County, Ohio", + "39173": "Wood County, Ohio", + "39175": "Wyandot County, Ohio", + "40001": "Adair County, Oklahoma", + "40003": "Alfalfa County, Oklahoma", + "40005": "Atoka County, Oklahoma", + "40007": "Beaver County, Oklahoma", + "40009": "Beckham County, Oklahoma", + "40011": "Blaine County, Oklahoma", + "40013": "Bryan County, Oklahoma", + "40015": "Caddo County, Oklahoma", + "40017": "Canadian County, Oklahoma", + "40019": "Carter County, Oklahoma", + "40021": "Cherokee County, Oklahoma", + "40023": "Choctaw County, Oklahoma", + "40025": "Cimarron County, Oklahoma", + "40027": "Cleveland County, Oklahoma", + "40029": "Coal County, Oklahoma", + "40031": "Comanche County, Oklahoma", + "40033": "Cotton County, Oklahoma", + "40035": "Craig County, Oklahoma", + "40037": "Creek County, Oklahoma", + "40039": "Custer County, Oklahoma", + "40041": "Delaware County, Oklahoma", + "40043": "Dewey County, Oklahoma", + "40045": "Ellis County, Oklahoma", + "40047": "Garfield County, Oklahoma", + "40049": "Garvin County, Oklahoma", + "40051": "Grady County, Oklahoma", + "40053": "Grant County, Oklahoma", + "40055": "Greer County, Oklahoma", + "40057": "Harmon County, Oklahoma", + "40059": "Harper County, Oklahoma", + "40061": "Haskell County, Oklahoma", + "40063": "Hughes County, Oklahoma", + "40065": "Jackson County, Oklahoma", + "40067": "Jefferson County, Oklahoma", + "40069": "Johnston County, Oklahoma", + "40071": "Kay County, Oklahoma", + "40073": "Kingfisher County, Oklahoma", + "40075": "Kiowa County, Oklahoma", + "40077": "Latimer County, Oklahoma", + "40079": "Le Flore County, Oklahoma", + "40081": "Lincoln County, Oklahoma", + "40083": "Logan County, Oklahoma", + "40085": "Love County, Oklahoma", + "40087": "McClain County, Oklahoma", + "40089": "McCurtain County, Oklahoma", + "40091": "McIntosh County, Oklahoma", + "40093": "Major County, Oklahoma", + "40095": "Marshall County, Oklahoma", + "40097": "Mayes County, Oklahoma", + "40099": "Murray County, Oklahoma", + "40101": "Muskogee County, Oklahoma", + "40103": "Noble County, Oklahoma", + "40105": "Nowata County, Oklahoma", + "40107": "Okfuskee County, Oklahoma", + "40109": "Oklahoma County, Oklahoma", + "40111": "Okmulgee County, Oklahoma", + "40113": "Osage County, Oklahoma", + "40115": "Ottawa County, Oklahoma", + "40117": "Pawnee County, Oklahoma", + "40119": "Payne County, Oklahoma", + "40121": "Pittsburg County, Oklahoma", + "40123": "Pontotoc County, Oklahoma", + "40125": "Pottawatomie County, Oklahoma", + "40127": "Pushmataha County, Oklahoma", + "40129": "Roger Mills County, Oklahoma", + "40131": "Rogers County, Oklahoma", + "40133": "Seminole County, Oklahoma", + "40135": "Sequoyah County, Oklahoma", + "40137": "Stephens County, Oklahoma", + "40139": "Texas County, Oklahoma", + "40141": "Tillman County, Oklahoma", + "40143": "Tulsa County, Oklahoma", + "40145": "Wagoner County, Oklahoma", + "40147": "Washington County, Oklahoma", + "40149": "Washita County, Oklahoma", + "40151": "Woods County, Oklahoma", + "40153": "Woodward County, Oklahoma", + "41001": "Baker County, Oregon", + "41003": "Benton County, Oregon", + "41005": "Clackamas County, Oregon", + "41007": "Clatsop County, Oregon", + "41009": "Columbia County, Oregon", + "41011": "Coos County, Oregon", + "41013": "Crook County, Oregon", + "41015": "Curry County, Oregon", + "41017": "Deschutes County, Oregon", + "41019": "Douglas County, Oregon", + "41021": "Gilliam County, Oregon", + "41023": "Grant County, Oregon", + "41025": "Harney County, Oregon", + "41027": "Hood River County, Oregon", + "41029": "Jackson County, Oregon", + "41031": "Jefferson County, Oregon", + "41033": "Josephine County, Oregon", + "41035": "Klamath County, Oregon", + "41037": "Lake County, Oregon", + "41039": "Lane County, Oregon", + "41041": "Lincoln County, Oregon", + "41043": "Linn County, Oregon", + "41045": "Malheur County, Oregon", + "41047": "Marion County, Oregon", + "41049": "Morrow County, Oregon", + "41051": "Multnomah County, Oregon", + "41053": "Polk County, Oregon", + "41055": "Sherman County, Oregon", + "41057": "Tillamook County, Oregon", + "41059": "Umatilla County, Oregon", + "41061": "Union County, Oregon", + "41063": "Wallowa County, Oregon", + "41065": "Wasco County, Oregon", + "41067": "Washington County, Oregon", + "41069": "Wheeler County, Oregon", + "41071": "Yamhill County, Oregon", + "42001": "Adams County, Pennsylvania", + "42003": "Allegheny County, Pennsylvania", + "42005": "Armstrong County, Pennsylvania", + "42007": "Beaver County, Pennsylvania", + "42009": "Bedford County, Pennsylvania", + "42011": "Berks County, Pennsylvania", + "42013": "Blair County, Pennsylvania", + "42015": "Bradford County, Pennsylvania", + "42017": "Bucks County, Pennsylvania", + "42019": "Butler County, Pennsylvania", + "42021": "Cambria County, Pennsylvania", + "42023": "Cameron County, Pennsylvania", + "42025": "Carbon County, Pennsylvania", + "42027": "Centre County, Pennsylvania", + "42029": "Chester County, Pennsylvania", + "42031": "Clarion County, Pennsylvania", + "42033": "Clearfield County, Pennsylvania", + "42035": "Clinton County, Pennsylvania", + "42037": "Columbia County, Pennsylvania", + "42039": "Crawford County, Pennsylvania", + "42041": "Cumberland County, Pennsylvania", + "42043": "Dauphin County, Pennsylvania", + "42045": "Delaware County, Pennsylvania", + "42047": "Elk County, Pennsylvania", + "42049": "Erie County, Pennsylvania", + "42051": "Fayette County, Pennsylvania", + "42053": "Forest County, Pennsylvania", + "42055": "Franklin County, Pennsylvania", + "42057": "Fulton County, Pennsylvania", + "42059": "Greene County, Pennsylvania", + "42061": "Huntingdon County, Pennsylvania", + "42063": "Indiana County, Pennsylvania", + "42065": "Jefferson County, Pennsylvania", + "42067": "Juniata County, Pennsylvania", + "42069": "Lackawanna County, Pennsylvania", + "42071": "Lancaster County, Pennsylvania", + "42073": "Lawrence County, Pennsylvania", + "42075": "Lebanon County, Pennsylvania", + "42077": "Lehigh County, Pennsylvania", + "42079": "Luzerne County, Pennsylvania", + "42081": "Lycoming County, Pennsylvania", + "42083": "McKean County, Pennsylvania", + "42085": "Mercer County, Pennsylvania", + "42087": "Mifflin County, Pennsylvania", + "42089": "Monroe County, Pennsylvania", + "42091": "Montgomery County, Pennsylvania", + "42093": "Montour County, Pennsylvania", + "42095": "Northampton County, Pennsylvania", + "42097": "Northumberland County, Pennsylvania", + "42099": "Perry County, Pennsylvania", + "42101": "Philadelphia County, Pennsylvania", + "42103": "Pike County, Pennsylvania", + "42105": "Potter County, Pennsylvania", + "42107": "Schuylkill County, Pennsylvania", + "42109": "Snyder County, Pennsylvania", + "42111": "Somerset County, Pennsylvania", + "42113": "Sullivan County, Pennsylvania", + "42115": "Susquehanna County, Pennsylvania", + "42117": "Tioga County, Pennsylvania", + "42119": "Union County, Pennsylvania", + "42121": "Venango County, Pennsylvania", + "42123": "Warren County, Pennsylvania", + "42125": "Washington County, Pennsylvania", + "42127": "Wayne County, Pennsylvania", + "42129": "Westmoreland County, Pennsylvania", + "42131": "Wyoming County, Pennsylvania", + "42133": "York County, Pennsylvania", + "44001": "Bristol County, Rhode Island", + "44003": "Kent County, Rhode Island", + "44005": "Newport County, Rhode Island", + "44007": "Providence County, Rhode Island", + "44009": "Washington County, Rhode Island", + "45001": "Abbeville County, South Carolina", + "45003": "Aiken County, South Carolina", + "45005": "Allendale County, South Carolina", + "45007": "Anderson County, South Carolina", + "45009": "Bamberg County, South Carolina", + "45011": "Barnwell County, South Carolina", + "45013": "Beaufort County, South Carolina", + "45015": "Berkeley County, South Carolina", + "45017": "Calhoun County, South Carolina", + "45019": "Charleston County, South Carolina", + "45021": "Cherokee County, South Carolina", + "45023": "Chester County, South Carolina", + "45025": "Chesterfield County, South Carolina", + "45027": "Clarendon County, South Carolina", + "45029": "Colleton County, South Carolina", + "45031": "Darlington County, South Carolina", + "45033": "Dillon County, South Carolina", + "45035": "Dorchester County, South Carolina", + "45037": "Edgefield County, South Carolina", + "45039": "Fairfield County, South Carolina", + "45041": "Florence County, South Carolina", + "45043": "Georgetown County, South Carolina", + "45045": "Greenville County, South Carolina", + "45047": "Greenwood County, South Carolina", + "45049": "Hampton County, South Carolina", + "45051": "Horry County, South Carolina", + "45053": "Jasper County, South Carolina", + "45055": "Kershaw County, South Carolina", + "45057": "Lancaster County, South Carolina", + "45059": "Laurens County, South Carolina", + "45061": "Lee County, South Carolina", + "45063": "Lexington County, South Carolina", + "45065": "McCormick County, South Carolina", + "45067": "Marion County, South Carolina", + "45069": "Marlboro County, South Carolina", + "45071": "Newberry County, South Carolina", + "45073": "Oconee County, South Carolina", + "45075": "Orangeburg County, South Carolina", + "45077": "Pickens County, South Carolina", + "45079": "Richland County, South Carolina", + "45081": "Saluda County, South Carolina", + "45083": "Spartanburg County, South Carolina", + "45085": "Sumter County, South Carolina", + "45087": "Union County, South Carolina", + "45089": "Williamsburg County, South Carolina", + "45091": "York County, South Carolina", + "46003": "Aurora County, South Dakota", + "46005": "Beadle County, South Dakota", + "46007": "Bennett County, South Dakota", + "46009": "Bon Homme County, South Dakota", + "46011": "Brookings County, South Dakota", + "46013": "Brown County, South Dakota", + "46015": "Brule County, South Dakota", + "46017": "Buffalo County, South Dakota", + "46019": "Butte County, South Dakota", + "46021": "Campbell County, South Dakota", + "46023": "Charles Mix County, South Dakota", + "46025": "Clark County, South Dakota", + "46027": "Clay County, South Dakota", + "46029": "Codington County, South Dakota", + "46031": "Corson County, South Dakota", + "46033": "Custer County, South Dakota", + "46035": "Davison County, South Dakota", + "46037": "Day County, South Dakota", + "46039": "Deuel County, South Dakota", + "46041": "Dewey County, South Dakota", + "46043": "Douglas County, South Dakota", + "46045": "Edmunds County, South Dakota", + "46047": "Fall River County, South Dakota", + "46049": "Faulk County, South Dakota", + "46051": "Grant County, South Dakota", + "46053": "Gregory County, South Dakota", + "46055": "Haakon County, South Dakota", + "46057": "Hamlin County, South Dakota", + "46059": "Hand County, South Dakota", + "46061": "Hanson County, South Dakota", + "46063": "Harding County, South Dakota", + "46065": "Hughes County, South Dakota", + "46067": "Hutchinson County, South Dakota", + "46069": "Hyde County, South Dakota", + "46071": "Jackson County, South Dakota", + "46073": "Jerauld County, South Dakota", + "46075": "Jones County, South Dakota", + "46077": "Kingsbury County, South Dakota", + "46079": "Lake County, South Dakota", + "46081": "Lawrence County, South Dakota", + "46083": "Lincoln County, South Dakota", + "46085": "Lyman County, South Dakota", + "46087": "McCook County, South Dakota", + "46089": "McPherson County, South Dakota", + "46091": "Marshall County, South Dakota", + "46093": "Meade County, South Dakota", + "46095": "Mellette County, South Dakota", + "46097": "Miner County, South Dakota", + "46099": "Minnehaha County, South Dakota", + "46101": "Moody County, South Dakota", + "46102": "Oglala Lakota County, South Dakota", + "46103": "Pennington County, South Dakota", + "46105": "Perkins County, South Dakota", + "46107": "Potter County, South Dakota", + "46109": "Roberts County, South Dakota", + "46111": "Sanborn County, South Dakota", + "46115": "Spink County, South Dakota", + "46117": "Stanley County, South Dakota", + "46119": "Sully County, South Dakota", + "46121": "Todd County, South Dakota", + "46123": "Tripp County, South Dakota", + "46125": "Turner County, South Dakota", + "46127": "Union County, South Dakota", + "46129": "Walworth County, South Dakota", + "46135": "Yankton County, South Dakota", + "46137": "Ziebach County, South Dakota", + "47001": "Anderson County, Tennessee", + "47003": "Bedford County, Tennessee", + "47005": "Benton County, Tennessee", + "47007": "Bledsoe County, Tennessee", + "47009": "Blount County, Tennessee", + "47011": "Bradley County, Tennessee", + "47013": "Campbell County, Tennessee", + "47015": "Cannon County, Tennessee", + "47017": "Carroll County, Tennessee", + "47019": "Carter County, Tennessee", + "47021": "Cheatham County, Tennessee", + "47023": "Chester County, Tennessee", + "47025": "Claiborne County, Tennessee", + "47027": "Clay County, Tennessee", + "47029": "Cocke County, Tennessee", + "47031": "Coffee County, Tennessee", + "47033": "Crockett County, Tennessee", + "47035": "Cumberland County, Tennessee", + "47037": "Davidson County, Tennessee", + "47039": "Decatur County, Tennessee", + "47041": "DeKalb County, Tennessee", + "47043": "Dickson County, Tennessee", + "47045": "Dyer County, Tennessee", + "47047": "Fayette County, Tennessee", + "47049": "Fentress County, Tennessee", + "47051": "Franklin County, Tennessee", + "47053": "Gibson County, Tennessee", + "47055": "Giles County, Tennessee", + "47057": "Grainger County, Tennessee", + "47059": "Greene County, Tennessee", + "47061": "Grundy County, Tennessee", + "47063": "Hamblen County, Tennessee", + "47065": "Hamilton County, Tennessee", + "47067": "Hancock County, Tennessee", + "47069": "Hardeman County, Tennessee", + "47071": "Hardin County, Tennessee", + "47073": "Hawkins County, Tennessee", + "47075": "Haywood County, Tennessee", + "47077": "Henderson County, Tennessee", + "47079": "Henry County, Tennessee", + "47081": "Hickman County, Tennessee", + "47083": "Houston County, Tennessee", + "47085": "Humphreys County, Tennessee", + "47087": "Jackson County, Tennessee", + "47089": "Jefferson County, Tennessee", + "47091": "Johnson County, Tennessee", + "47093": "Knox County, Tennessee", + "47095": "Lake County, Tennessee", + "47097": "Lauderdale County, Tennessee", + "47099": "Lawrence County, Tennessee", + "47101": "Lewis County, Tennessee", + "47103": "Lincoln County, Tennessee", + "47105": "Loudon County, Tennessee", + "47107": "McMinn County, Tennessee", + "47109": "McNairy County, Tennessee", + "47111": "Macon County, Tennessee", + "47113": "Madison County, Tennessee", + "47115": "Marion County, Tennessee", + "47117": "Marshall County, Tennessee", + "47119": "Maury County, Tennessee", + "47121": "Meigs County, Tennessee", + "47123": "Monroe County, Tennessee", + "47125": "Montgomery County, Tennessee", + "47127": "Moore County, Tennessee", + "47129": "Morgan County, Tennessee", + "47131": "Obion County, Tennessee", + "47133": "Overton County, Tennessee", + "47135": "Perry County, Tennessee", + "47137": "Pickett County, Tennessee", + "47139": "Polk County, Tennessee", + "47141": "Putnam County, Tennessee", + "47143": "Rhea County, Tennessee", + "47145": "Roane County, Tennessee", + "47147": "Robertson County, Tennessee", + "47149": "Rutherford County, Tennessee", + "47151": "Scott County, Tennessee", + "47153": "Sequatchie County, Tennessee", + "47155": "Sevier County, Tennessee", + "47157": "Shelby County, Tennessee", + "47159": "Smith County, Tennessee", + "47161": "Stewart County, Tennessee", + "47163": "Sullivan County, Tennessee", + "47165": "Sumner County, Tennessee", + "47167": "Tipton County, Tennessee", + "47169": "Trousdale County, Tennessee", + "47171": "Unicoi County, Tennessee", + "47173": "Union County, Tennessee", + "47175": "Van Buren County, Tennessee", + "47177": "Warren County, Tennessee", + "47179": "Washington County, Tennessee", + "47181": "Wayne County, Tennessee", + "47183": "Weakley County, Tennessee", + "47185": "White County, Tennessee", + "47187": "Williamson County, Tennessee", + "47189": "Wilson County, Tennessee", + "48001": "Anderson County, Texas", + "48003": "Andrews County, Texas", + "48005": "Angelina County, Texas", + "48007": "Aransas County, Texas", + "48009": "Archer County, Texas", + "48011": "Armstrong County, Texas", + "48013": "Atascosa County, Texas", + "48015": "Austin County, Texas", + "48017": "Bailey County, Texas", + "48019": "Bandera County, Texas", + "48021": "Bastrop County, Texas", + "48023": "Baylor County, Texas", + "48025": "Bee County, Texas", + "48027": "Bell County, Texas", + "48029": "Bexar County, Texas", + "48031": "Blanco County, Texas", + "48033": "Borden County, Texas", + "48035": "Bosque County, Texas", + "48037": "Bowie County, Texas", + "48039": "Brazoria County, Texas", + "48041": "Brazos County, Texas", + "48043": "Brewster County, Texas", + "48045": "Briscoe County, Texas", + "48047": "Brooks County, Texas", + "48049": "Brown County, Texas", + "48051": "Burleson County, Texas", + "48053": "Burnet County, Texas", + "48055": "Caldwell County, Texas", + "48057": "Calhoun County, Texas", + "48059": "Callahan County, Texas", + "48061": "Cameron County, Texas", + "48063": "Camp County, Texas", + "48065": "Carson County, Texas", + "48067": "Cass County, Texas", + "48069": "Castro County, Texas", + "48071": "Chambers County, Texas", + "48073": "Cherokee County, Texas", + "48075": "Childress County, Texas", + "48077": "Clay County, Texas", + "48079": "Cochran County, Texas", + "48081": "Coke County, Texas", + "48083": "Coleman County, Texas", + "48085": "Collin County, Texas", + "48087": "Collingsworth County, Texas", + "48089": "Colorado County, Texas", + "48091": "Comal County, Texas", + "48093": "Comanche County, Texas", + "48095": "Concho County, Texas", + "48097": "Cooke County, Texas", + "48099": "Coryell County, Texas", + "48101": "Cottle County, Texas", + "48103": "Crane County, Texas", + "48105": "Crockett County, Texas", + "48107": "Crosby County, Texas", + "48109": "Culberson County, Texas", + "48111": "Dallam County, Texas", + "48113": "Dallas County, Texas", + "48115": "Dawson County, Texas", + "48117": "Deaf Smith County, Texas", + "48119": "Delta County, Texas", + "48121": "Denton County, Texas", + "48123": "DeWitt County, Texas", + "48125": "Dickens County, Texas", + "48127": "Dimmit County, Texas", + "48129": "Donley County, Texas", + "48131": "Duval County, Texas", + "48133": "Eastland County, Texas", + "48135": "Ector County, Texas", + "48137": "Edwards County, Texas", + "48139": "Ellis County, Texas", + "48141": "El Paso County, Texas", + "48143": "Erath County, Texas", + "48145": "Falls County, Texas", + "48147": "Fannin County, Texas", + "48149": "Fayette County, Texas", + "48151": "Fisher County, Texas", + "48153": "Floyd County, Texas", + "48155": "Foard County, Texas", + "48157": "Fort Bend County, Texas", + "48159": "Franklin County, Texas", + "48161": "Freestone County, Texas", + "48163": "Frio County, Texas", + "48165": "Gaines County, Texas", + "48167": "Galveston County, Texas", + "48169": "Garza County, Texas", + "48171": "Gillespie County, Texas", + "48173": "Glasscock County, Texas", + "48175": "Goliad County, Texas", + "48177": "Gonzales County, Texas", + "48179": "Gray County, Texas", + "48181": "Grayson County, Texas", + "48183": "Gregg County, Texas", + "48185": "Grimes County, Texas", + "48187": "Guadalupe County, Texas", + "48189": "Hale County, Texas", + "48191": "Hall County, Texas", + "48193": "Hamilton County, Texas", + "48195": "Hansford County, Texas", + "48197": "Hardeman County, Texas", + "48199": "Hardin County, Texas", + "48201": "Harris County, Texas", + "48203": "Harrison County, Texas", + "48205": "Hartley County, Texas", + "48207": "Haskell County, Texas", + "48209": "Hays County, Texas", + "48211": "Hemphill County, Texas", + "48213": "Henderson County, Texas", + "48215": "Hidalgo County, Texas", + "48217": "Hill County, Texas", + "48219": "Hockley County, Texas", + "48221": "Hood County, Texas", + "48223": "Hopkins County, Texas", + "48225": "Houston County, Texas", + "48227": "Howard County, Texas", + "48229": "Hudspeth County, Texas", + "48231": "Hunt County, Texas", + "48233": "Hutchinson County, Texas", + "48235": "Irion County, Texas", + "48237": "Jack County, Texas", + "48239": "Jackson County, Texas", + "48241": "Jasper County, Texas", + "48243": "Jeff Davis County, Texas", + "48245": "Jefferson County, Texas", + "48247": "Jim Hogg County, Texas", + "48249": "Jim Wells County, Texas", + "48251": "Johnson County, Texas", + "48253": "Jones County, Texas", + "48255": "Karnes County, Texas", + "48257": "Kaufman County, Texas", + "48259": "Kendall County, Texas", + "48261": "Kenedy County, Texas", + "48263": "Kent County, Texas", + "48265": "Kerr County, Texas", + "48267": "Kimble County, Texas", + "48269": "King County, Texas", + "48271": "Kinney County, Texas", + "48273": "Kleberg County, Texas", + "48275": "Knox County, Texas", + "48277": "Lamar County, Texas", + "48279": "Lamb County, Texas", + "48281": "Lampasas County, Texas", + "48283": "La Salle County, Texas", + "48285": "Lavaca County, Texas", + "48287": "Lee County, Texas", + "48289": "Leon County, Texas", + "48291": "Liberty County, Texas", + "48293": "Limestone County, Texas", + "48295": "Lipscomb County, Texas", + "48297": "Live Oak County, Texas", + "48299": "Llano County, Texas", + "48301": "Loving County, Texas", + "48303": "Lubbock County, Texas", + "48305": "Lynn County, Texas", + "48307": "McCulloch County, Texas", + "48309": "McLennan County, Texas", + "48311": "McMullen County, Texas", + "48313": "Madison County, Texas", + "48315": "Marion County, Texas", + "48317": "Martin County, Texas", + "48319": "Mason County, Texas", + "48321": "Matagorda County, Texas", + "48323": "Maverick County, Texas", + "48325": "Medina County, Texas", + "48327": "Menard County, Texas", + "48329": "Midland County, Texas", + "48331": "Milam County, Texas", + "48333": "Mills County, Texas", + "48335": "Mitchell County, Texas", + "48337": "Montague County, Texas", + "48339": "Montgomery County, Texas", + "48341": "Moore County, Texas", + "48343": "Morris County, Texas", + "48345": "Motley County, Texas", + "48347": "Nacogdoches County, Texas", + "48349": "Navarro County, Texas", + "48351": "Newton County, Texas", + "48353": "Nolan County, Texas", + "48355": "Nueces County, Texas", + "48357": "Ochiltree County, Texas", + "48359": "Oldham County, Texas", + "48361": "Orange County, Texas", + "48363": "Palo Pinto County, Texas", + "48365": "Panola County, Texas", + "48367": "Parker County, Texas", + "48369": "Parmer County, Texas", + "48371": "Pecos County, Texas", + "48373": "Polk County, Texas", + "48375": "Potter County, Texas", + "48377": "Presidio County, Texas", + "48379": "Rains County, Texas", + "48381": "Randall County, Texas", + "48383": "Reagan County, Texas", + "48385": "Real County, Texas", + "48387": "Red River County, Texas", + "48389": "Reeves County, Texas", + "48391": "Refugio County, Texas", + "48393": "Roberts County, Texas", + "48395": "Robertson County, Texas", + "48397": "Rockwall County, Texas", + "48399": "Runnels County, Texas", + "48401": "Rusk County, Texas", + "48403": "Sabine County, Texas", + "48405": "San Augustine County, Texas", + "48407": "San Jacinto County, Texas", + "48409": "San Patricio County, Texas", + "48411": "San Saba County, Texas", + "48413": "Schleicher County, Texas", + "48415": "Scurry County, Texas", + "48417": "Shackelford County, Texas", + "48419": "Shelby County, Texas", + "48421": "Sherman County, Texas", + "48423": "Smith County, Texas", + "48425": "Somervell County, Texas", + "48427": "Starr County, Texas", + "48429": "Stephens County, Texas", + "48431": "Sterling County, Texas", + "48433": "Stonewall County, Texas", + "48435": "Sutton County, Texas", + "48437": "Swisher County, Texas", + "48439": "Tarrant County, Texas", + "48441": "Taylor County, Texas", + "48443": "Terrell County, Texas", + "48445": "Terry County, Texas", + "48447": "Throckmorton County, Texas", + "48449": "Titus County, Texas", + "48451": "Tom Green County, Texas", + "48453": "Travis County, Texas", + "48455": "Trinity County, Texas", + "48457": "Tyler County, Texas", + "48459": "Upshur County, Texas", + "48461": "Upton County, Texas", + "48463": "Uvalde County, Texas", + "48465": "Val Verde County, Texas", + "48467": "Van Zandt County, Texas", + "48469": "Victoria County, Texas", + "48471": "Walker County, Texas", + "48473": "Waller County, Texas", + "48475": "Ward County, Texas", + "48477": "Washington County, Texas", + "48479": "Webb County, Texas", + "48481": "Wharton County, Texas", + "48483": "Wheeler County, Texas", + "48485": "Wichita County, Texas", + "48487": "Wilbarger County, Texas", + "48489": "Willacy County, Texas", + "48491": "Williamson County, Texas", + "48493": "Wilson County, Texas", + "48495": "Winkler County, Texas", + "48497": "Wise County, Texas", + "48499": "Wood County, Texas", + "48501": "Yoakum County, Texas", + "48503": "Young County, Texas", + "48505": "Zapata County, Texas", + "48507": "Zavala County, Texas", + "49001": "Beaver County, Utah", + "49003": "Box Elder County, Utah", + "49005": "Cache County, Utah", + "49007": "Carbon County, Utah", + "49009": "Daggett County, Utah", + "49011": "Davis County, Utah", + "49013": "Duchesne County, Utah", + "49015": "Emery County, Utah", + "49017": "Garfield County, Utah", + "49019": "Grand County, Utah", + "49021": "Iron County, Utah", + "49023": "Juab County, Utah", + "49025": "Kane County, Utah", + "49027": "Millard County, Utah", + "49029": "Morgan County, Utah", + "49031": "Piute County, Utah", + "49033": "Rich County, Utah", + "49035": "Salt Lake County, Utah", + "49037": "San Juan County, Utah", + "49039": "Sanpete County, Utah", + "49041": "Sevier County, Utah", + "49043": "Summit County, Utah", + "49045": "Tooele County, Utah", + "49047": "Uintah County, Utah", + "49049": "Utah County, Utah", + "49051": "Wasatch County, Utah", + "49053": "Washington County, Utah", + "49055": "Wayne County, Utah", + "49057": "Weber County, Utah", + "50001": "Addison County, Vermont", + "50003": "Bennington County, Vermont", + "50005": "Caledonia County, Vermont", + "50007": "Chittenden County, Vermont", + "50009": "Essex County, Vermont", + "50011": "Franklin County, Vermont", + "50013": "Grand Isle County, Vermont", + "50015": "Lamoille County, Vermont", + "50017": "Orange County, Vermont", + "50019": "Orleans County, Vermont", + "50021": "Rutland County, Vermont", + "50023": "Washington County, Vermont", + "50025": "Windham County, Vermont", + "50027": "Windsor County, Vermont", + "51001": "Accomack County, Virginia", + "51003": "Albemarle County, Virginia", + "51005": "Alleghany County, Virginia", + "51007": "Amelia County, Virginia", + "51009": "Amherst County, Virginia", + "51011": "Appomattox County, Virginia", + "51013": "Arlington County, Virginia", + "51015": "Augusta County, Virginia", + "51017": "Bath County, Virginia", + "51019": "Bedford County, Virginia", + "51021": "Bland County, Virginia", + "51023": "Botetourt County, Virginia", + "51025": "Brunswick County, Virginia", + "51027": "Buchanan County, Virginia", + "51029": "Buckingham County, Virginia", + "51031": "Campbell County, Virginia", + "51033": "Caroline County, Virginia", + "51035": "Carroll County, Virginia", + "51036": "Charles City County, Virginia", + "51037": "Charlotte County, Virginia", + "51041": "Chesterfield County, Virginia", + "51043": "Clarke County, Virginia", + "51045": "Craig County, Virginia", + "51047": "Culpeper County, Virginia", + "51049": "Cumberland County, Virginia", + "51051": "Dickenson County, Virginia", + "51053": "Dinwiddie County, Virginia", + "51057": "Essex County, Virginia", + "51059": "Fairfax County, Virginia", + "51061": "Fauquier County, Virginia", + "51063": "Floyd County, Virginia", + "51065": "Fluvanna County, Virginia", + "51067": "Franklin County, Virginia", + "51069": "Frederick County, Virginia", + "51071": "Giles County, Virginia", + "51073": "Gloucester County, Virginia", + "51075": "Goochland County, Virginia", + "51077": "Grayson County, Virginia", + "51079": "Greene County, Virginia", + "51081": "Greensville County, Virginia", + "51083": "Halifax County, Virginia", + "51085": "Hanover County, Virginia", + "51087": "Henrico County, Virginia", + "51089": "Henry County, Virginia", + "51091": "Highland County, Virginia", + "51093": "Isle of Wight County, Virginia", + "51095": "James City County, Virginia", + "51097": "King and Queen County, Virginia", + "51099": "King George County, Virginia", + "51101": "King William County, Virginia", + "51103": "Lancaster County, Virginia", + "51105": "Lee County, Virginia", + "51107": "Loudoun County, Virginia", + "51109": "Louisa County, Virginia", + "51111": "Lunenburg County, Virginia", + "51113": "Madison County, Virginia", + "51115": "Mathews County, Virginia", + "51117": "Mecklenburg County, Virginia", + "51119": "Middlesex County, Virginia", + "51121": "Montgomery County, Virginia", + "51125": "Nelson County, Virginia", + "51127": "New Kent County, Virginia", + "51131": "Northampton County, Virginia", + "51133": "Northumberland County, Virginia", + "51135": "Nottoway County, Virginia", + "51137": "Orange County, Virginia", + "51139": "Page County, Virginia", + "51141": "Patrick County, Virginia", + "51143": "Pittsylvania County, Virginia", + "51145": "Powhatan County, Virginia", + "51147": "Prince Edward County, Virginia", + "51149": "Prince George County, Virginia", + "51153": "Prince William County, Virginia", + "51155": "Pulaski County, Virginia", + "51157": "Rappahannock County, Virginia", + "51159": "Richmond County, Virginia", + "51161": "Roanoke County, Virginia", + "51163": "Rockbridge County, Virginia", + "51165": "Rockingham County, Virginia", + "51167": "Russell County, Virginia", + "51169": "Scott County, Virginia", + "51171": "Shenandoah County, Virginia", + "51173": "Smyth County, Virginia", + "51175": "Southampton County, Virginia", + "51177": "Spotsylvania County, Virginia", + "51179": "Stafford County, Virginia", + "51181": "Surry County, Virginia", + "51183": "Sussex County, Virginia", + "51185": "Tazewell County, Virginia", + "51187": "Warren County, Virginia", + "51191": "Washington County, Virginia", + "51193": "Westmoreland County, Virginia", + "51195": "Wise County, Virginia", + "51197": "Wythe County, Virginia", + "51199": "York County, Virginia", + "51510": "Alexandria city, Virginia", + "51520": "Bristol city, Virginia", + "51530": "Buena Vista city, Virginia", + "51540": "Charlottesville city, Virginia", + "51550": "Chesapeake city, Virginia", + "51570": "Colonial Heights city, Virginia", + "51580": "Covington city, Virginia", + "51590": "Danville city, Virginia", + "51595": "Emporia city, Virginia", + "51600": "Fairfax city, Virginia", + "51610": "Falls Church city, Virginia", + "51620": "Franklin city, Virginia", + "51630": "Fredericksburg city, Virginia", + "51640": "Galax city, Virginia", + "51650": "Hampton city, Virginia", + "51660": "Harrisonburg city, Virginia", + "51670": "Hopewell city, Virginia", + "51678": "Lexington city, Virginia", + "51680": "Lynchburg city, Virginia", + "51683": "Manassas city, Virginia", + "51685": "Manassas Park city, Virginia", + "51690": "Martinsville city, Virginia", + "51700": "Newport News city, Virginia", + "51710": "Norfolk city, Virginia", + "51720": "Norton city, Virginia", + "51730": "Petersburg city, Virginia", + "51735": "Poquoson city, Virginia", + "51740": "Portsmouth city, Virginia", + "51750": "Radford city, Virginia", + "51760": "Richmond city, Virginia", + "51770": "Roanoke city, Virginia", + "51775": "Salem city, Virginia", + "51790": "Staunton city, Virginia", + "51800": "Suffolk city, Virginia", + "51810": "Virginia Beach city, Virginia", + "51820": "Waynesboro city, Virginia", + "51830": "Williamsburg city, Virginia", + "51840": "Winchester city, Virginia", + "53001": "Adams County, Washington", + "53003": "Asotin County, Washington", + "53005": "Benton County, Washington", + "53007": "Chelan County, Washington", + "53009": "Clallam County, Washington", + "53011": "Clark County, Washington", + "53013": "Columbia County, Washington", + "53015": "Cowlitz County, Washington", + "53017": "Douglas County, Washington", + "53019": "Ferry County, Washington", + "53021": "Franklin County, Washington", + "53023": "Garfield County, Washington", + "53025": "Grant County, Washington", + "53027": "Grays Harbor County, Washington", + "53029": "Island County, Washington", + "53031": "Jefferson County, Washington", + "53033": "King County, Washington", + "53035": "Kitsap County, Washington", + "53037": "Kittitas County, Washington", + "53039": "Klickitat County, Washington", + "53041": "Lewis County, Washington", + "53043": "Lincoln County, Washington", + "53045": "Mason County, Washington", + "53047": "Okanogan County, Washington", + "53049": "Pacific County, Washington", + "53051": "Pend Oreille County, Washington", + "53053": "Pierce County, Washington", + "53055": "San Juan County, Washington", + "53057": "Skagit County, Washington", + "53059": "Skamania County, Washington", + "53061": "Snohomish County, Washington", + "53063": "Spokane County, Washington", + "53065": "Stevens County, Washington", + "53067": "Thurston County, Washington", + "53069": "Wahkiakum County, Washington", + "53071": "Walla Walla County, Washington", + "53073": "Whatcom County, Washington", + "53075": "Whitman County, Washington", + "53077": "Yakima County, Washington", + "54001": "Barbour County, West Virginia", + "54003": "Berkeley County, West Virginia", + "54005": "Boone County, West Virginia", + "54007": "Braxton County, West Virginia", + "54009": "Brooke County, West Virginia", + "54011": "Cabell County, West Virginia", + "54013": "Calhoun County, West Virginia", + "54015": "Clay County, West Virginia", + "54017": "Doddridge County, West Virginia", + "54019": "Fayette County, West Virginia", + "54021": "Gilmer County, West Virginia", + "54023": "Grant County, West Virginia", + "54025": "Greenbrier County, West Virginia", + "54027": "Hampshire County, West Virginia", + "54029": "Hancock County, West Virginia", + "54031": "Hardy County, West Virginia", + "54033": "Harrison County, West Virginia", + "54035": "Jackson County, West Virginia", + "54037": "Jefferson County, West Virginia", + "54039": "Kanawha County, West Virginia", + "54041": "Lewis County, West Virginia", + "54043": "Lincoln County, West Virginia", + "54045": "Logan County, West Virginia", + "54047": "McDowell County, West Virginia", + "54049": "Marion County, West Virginia", + "54051": "Marshall County, West Virginia", + "54053": "Mason County, West Virginia", + "54055": "Mercer County, West Virginia", + "54057": "Mineral County, West Virginia", + "54059": "Mingo County, West Virginia", + "54061": "Monongalia County, West Virginia", + "54063": "Monroe County, West Virginia", + "54065": "Morgan County, West Virginia", + "54067": "Nicholas County, West Virginia", + "54069": "Ohio County, West Virginia", + "54071": "Pendleton County, West Virginia", + "54073": "Pleasants County, West Virginia", + "54075": "Pocahontas County, West Virginia", + "54077": "Preston County, West Virginia", + "54079": "Putnam County, West Virginia", + "54081": "Raleigh County, West Virginia", + "54083": "Randolph County, West Virginia", + "54085": "Ritchie County, West Virginia", + "54087": "Roane County, West Virginia", + "54089": "Summers County, West Virginia", + "54091": "Taylor County, West Virginia", + "54093": "Tucker County, West Virginia", + "54095": "Tyler County, West Virginia", + "54097": "Upshur County, West Virginia", + "54099": "Wayne County, West Virginia", + "54101": "Webster County, West Virginia", + "54103": "Wetzel County, West Virginia", + "54105": "Wirt County, West Virginia", + "54107": "Wood County, West Virginia", + "54109": "Wyoming County, West Virginia", + "55001": "Adams County, Wisconsin", + "55003": "Ashland County, Wisconsin", + "55005": "Barron County, Wisconsin", + "55007": "Bayfield County, Wisconsin", + "55009": "Brown County, Wisconsin", + "55011": "Buffalo County, Wisconsin", + "55013": "Burnett County, Wisconsin", + "55015": "Calumet County, Wisconsin", + "55017": "Chippewa County, Wisconsin", + "55019": "Clark County, Wisconsin", + "55021": "Columbia County, Wisconsin", + "55023": "Crawford County, Wisconsin", + "55025": "Dane County, Wisconsin", + "55027": "Dodge County, Wisconsin", + "55029": "Door County, Wisconsin", + "55031": "Douglas County, Wisconsin", + "55033": "Dunn County, Wisconsin", + "55035": "Eau Claire County, Wisconsin", + "55037": "Florence County, Wisconsin", + "55039": "Fond du Lac County, Wisconsin", + "55041": "Forest County, Wisconsin", + "55043": "Grant County, Wisconsin", + "55045": "Green County, Wisconsin", + "55047": "Green Lake County, Wisconsin", + "55049": "Iowa County, Wisconsin", + "55051": "Iron County, Wisconsin", + "55053": "Jackson County, Wisconsin", + "55055": "Jefferson County, Wisconsin", + "55057": "Juneau County, Wisconsin", + "55059": "Kenosha County, Wisconsin", + "55061": "Kewaunee County, Wisconsin", + "55063": "La Crosse County, Wisconsin", + "55065": "Lafayette County, Wisconsin", + "55067": "Langlade County, Wisconsin", + "55069": "Lincoln County, Wisconsin", + "55071": "Manitowoc County, Wisconsin", + "55073": "Marathon County, Wisconsin", + "55075": "Marinette County, Wisconsin", + "55077": "Marquette County, Wisconsin", + "55078": "Menominee County, Wisconsin", + "55079": "Milwaukee County, Wisconsin", + "55081": "Monroe County, Wisconsin", + "55083": "Oconto County, Wisconsin", + "55085": "Oneida County, Wisconsin", + "55087": "Outagamie County, Wisconsin", + "55089": "Ozaukee County, Wisconsin", + "55091": "Pepin County, Wisconsin", + "55093": "Pierce County, Wisconsin", + "55095": "Polk County, Wisconsin", + "55097": "Portage County, Wisconsin", + "55099": "Price County, Wisconsin", + "55101": "Racine County, Wisconsin", + "55103": "Richland County, Wisconsin", + "55105": "Rock County, Wisconsin", + "55107": "Rusk County, Wisconsin", + "55109": "St. Croix County, Wisconsin", + "55111": "Sauk County, Wisconsin", + "55113": "Sawyer County, Wisconsin", + "55115": "Shawano County, Wisconsin", + "55117": "Sheboygan County, Wisconsin", + "55119": "Taylor County, Wisconsin", + "55121": "Trempealeau County, Wisconsin", + "55123": "Vernon County, Wisconsin", + "55125": "Vilas County, Wisconsin", + "55127": "Walworth County, Wisconsin", + "55129": "Washburn County, Wisconsin", + "55131": "Washington County, Wisconsin", + "55133": "Waukesha County, Wisconsin", + "55135": "Waupaca County, Wisconsin", + "55137": "Waushara County, Wisconsin", + "55139": "Winnebago County, Wisconsin", + "55141": "Wood County, Wisconsin", + "56001": "Albany County, Wyoming", + "56003": "Big Horn County, Wyoming", + "56005": "Campbell County, Wyoming", + "56007": "Carbon County, Wyoming", + "56009": "Converse County, Wyoming", + "56011": "Crook County, Wyoming", + "56013": "Fremont County, Wyoming", + "56015": "Goshen County, Wyoming", + "56017": "Hot Springs County, Wyoming", + "56019": "Johnson County, Wyoming", + "56021": "Laramie County, Wyoming", + "56023": "Lincoln County, Wyoming", + "56025": "Natrona County, Wyoming", + "56027": "Niobrara County, Wyoming", + "56029": "Park County, Wyoming", + "56031": "Platte County, Wyoming", + "56033": "Sheridan County, Wyoming", + "56035": "Sublette County, Wyoming", + "56037": "Sweetwater County, Wyoming", + "56039": "Teton County, Wyoming", + "56041": "Uinta County, Wyoming", + "56043": "Washakie County, Wyoming", + "56045": "Weston County, Wyoming", + "72001": "Adjuntas Municipio, Puerto Rico", + "72003": "Aguada Municipio, Puerto Rico", + "72005": "Aguadilla Municipio, Puerto Rico", + "72007": "Aguas Buenas Municipio, Puerto Rico", + "72009": "Aibonito Municipio, Puerto Rico", + "72011": "Añasco Municipio, Puerto Rico", + "72013": "Arecibo Municipio, Puerto Rico", + "72015": "Arroyo Municipio, Puerto Rico", + "72017": "Barceloneta Municipio, Puerto Rico", + "72019": "Barranquitas Municipio, Puerto Rico", + "72021": "Bayamón Municipio, Puerto Rico", + "72023": "Cabo Rojo Municipio, Puerto Rico", + "72025": "Caguas Municipio, Puerto Rico", + "72027": "Camuy Municipio, Puerto Rico", + "72029": "Canóvanas Municipio, Puerto Rico", + "72031": "Carolina Municipio, Puerto Rico", + "72033": "Cataño Municipio, Puerto Rico", + "72035": "Cayey Municipio, Puerto Rico", + "72037": "Ceiba Municipio, Puerto Rico", + "72039": "Ciales Municipio, Puerto Rico", + "72041": "Cidra Municipio, Puerto Rico", + "72043": "Coamo Municipio, Puerto Rico", + "72045": "Comerío Municipio, Puerto Rico", + "72047": "Corozal Municipio, Puerto Rico", + "72049": "Culebra Municipio, Puerto Rico", + "72051": "Dorado Municipio, Puerto Rico", + "72053": "Fajardo Municipio, Puerto Rico", + "72054": "Florida Municipio, Puerto Rico", + "72055": "Guánica Municipio, Puerto Rico", + "72057": "Guayama Municipio, Puerto Rico", + "72059": "Guayanilla Municipio, Puerto Rico", + "72061": "Guaynabo Municipio, Puerto Rico", + "72063": "Gurabo Municipio, Puerto Rico", + "72065": "Hatillo Municipio, Puerto Rico", + "72067": "Hormigueros Municipio, Puerto Rico", + "72069": "Humacao Municipio, Puerto Rico", + "72071": "Isabela Municipio, Puerto Rico", + "72073": "Jayuya Municipio, Puerto Rico", + "72075": "Juana Díaz Municipio, Puerto Rico", + "72077": "Juncos Municipio, Puerto Rico", + "72079": "Lajas Municipio, Puerto Rico", + "72081": "Lares Municipio, Puerto Rico", + "72083": "Las Marías Municipio, Puerto Rico", + "72085": "Las Piedras Municipio, Puerto Rico", + "72087": "Loíza Municipio, Puerto Rico", + "72089": "Luquillo Municipio, Puerto Rico", + "72091": "Manatí Municipio, Puerto Rico", + "72093": "Maricao Municipio, Puerto Rico", + "72095": "Maunabo Municipio, Puerto Rico", + "72097": "Mayagüez Municipio, Puerto Rico", + "72099": "Moca Municipio, Puerto Rico", + "72101": "Morovis Municipio, Puerto Rico", + "72103": "Naguabo Municipio, Puerto Rico", + "72105": "Naranjito Municipio, Puerto Rico", + "72107": "Orocovis Municipio, Puerto Rico", + "72109": "Patillas Municipio, Puerto Rico", + "72111": "Peñuelas Municipio, Puerto Rico", + "72113": "Ponce Municipio, Puerto Rico", + "72115": "Quebradillas Municipio, Puerto Rico", + "72117": "Rincón Municipio, Puerto Rico", + "72119": "Río Grande Municipio, Puerto Rico", + "72121": "Sabana Grande Municipio, Puerto Rico", + "72123": "Salinas Municipio, Puerto Rico", + "72125": "San Germán Municipio, Puerto Rico", + "72127": "San Juan Municipio, Puerto Rico", + "72129": "San Lorenzo Municipio, Puerto Rico", + "72131": "San Sebastián Municipio, Puerto Rico", + "72133": "Santa Isabel Municipio, Puerto Rico", + "72135": "Toa Alta Municipio, Puerto Rico", + "72137": "Toa Baja Municipio, Puerto Rico", + "72139": "Trujillo Alto Municipio, Puerto Rico", + "72141": "Utuado Municipio, Puerto Rico", + "72143": "Vega Alta Municipio, Puerto Rico", + "72145": "Vega Baja Municipio, Puerto Rico", + "72147": "Vieques Municipio, Puerto Rico", + "72149": "Villalba Municipio, Puerto Rico", + "72151": "Yabucoa Municipio, Puerto Rico", + "72153": "Yauco Municipio, Puerto Rico" + }, "cgt_gain_band": { "gain_1000000_to_1999999": "GBP 1,000,000 to GBP 1,999,999", "gain_100000_to_249999": "GBP 100,000 to GBP 249,999", @@ -3885,6 +7128,20 @@ "early_year_award_line": { "new_returning_total_returning_31_33_35": "Total returning [31][33][35]" }, + "earnings_component": { + "ordinary_time": "Ordinary time", + "total": "Total (ordinary time and overtime)" + }, + "earnings_denominator": { + "fte": "Full-time equivalent employees (FTEs)" + }, + "earnings_sector": { + "all_sectors": "All sectors" + }, + "earnings_series_reference": { + "QEMQ.SBSZ9A": "QEMQ.SBSZ9A — all-sector ordinary-time average weekly earnings per FTE, actual (not seasonally adjusted)", + "QEMQ.SBSZ9Z": "QEMQ.SBSZ9Z — all-sector total average weekly earnings per FTE, actual (not seasonally adjusted)" + }, "eitc_child_count": { "0": "0", "1": "1", @@ -4521,6 +7778,11 @@ "publication_status": { "provisional": "Provisional" }, + "published_unit": { + "gbp_million": "£ million", + "million_sticks": "Million sticks", + "thousand_kilograms": "Thousand kilograms" + }, "quant_inc": { "d1": "First income decile", "d2": "Second income decile", @@ -4779,6 +8041,9 @@ "all_passenger_journeys": "All passenger journeys", "concessionary_passenger_journeys": "Concessionary passenger journeys" }, + "seasonal_adjustment": { + "actual": "Actual (not seasonally adjusted)" + }, "sector": { "Private": "Private sector", "Public": "Public sector", @@ -4868,6 +8133,8 @@ "unemployment": "Unemployment" }, "statistic": { + "estimate": "Estimate", + "margin_of_error": "Margin of error", "mean": "Mean", "median": "Median" }, @@ -4902,7 +8169,8 @@ "time": { "2022": "2022", "2023": "2023", - "2024": "2024" + "2024": "2024", + "2025": "2025" }, "total_income_lower_bound": { "100000": "100000", diff --git a/scripts/build_dimension_value_register.py b/scripts/build_dimension_value_register.py index 4997122b..174df6dd 100644 --- a/scripts/build_dimension_value_register.py +++ b/scripts/build_dimension_value_register.py @@ -62,18 +62,32 @@ SCRIPT_DISPLAY_PATH = "scripts/build_dimension_value_register.py" SCHEMA_VERSION = "chronicle.dimension_value_labels.v1" #: The Chronicle commit whose package builds the register was generated from. -BUILT_AT_CHRONICLE_COMMIT = "aa288c3" +BUILT_AT_CHRONICLE_COMMIT = "dece5e9" DEFAULT_BUILD_YEAR = 2023 #: Default-bundle packages that do not build at DEFAULT_BUILD_YEAR. BUILD_YEARS = { "census-acs-s0101-congressional-district-age-2024": 2024, "census-acs-s0101-national-age-2024": 2024, "census-acs-s0101-state-age-2024": 2024, + "census-acs-s1501-state-attainment-2024": 2024, "census-acs-s2201-congressional-district-snap-2024": 2024, + "census-acs5-b01003-county-2024": 2024, + "census-acs5-b11001-county-2024": 2024, + "census-acs5-b19001-county-2024": 2024, + "census-acs5-b19013-county-2024": 2024, + "census-acs5-b19025-county-2024": 2024, + "census-acs5-b19051-county-2024": 2024, + "census-acs5-b19061-county-2024": 2024, "cms-aca-effectuated-enrollment-2022": 2022, "cms-aca-oep-state-level": 2024, "cms-aca-oep-state-level-2022": 2022, "cms-aca-oep-state-level-2025": 2025, + "ed-fsa-dl-portfolio-by-repayment-plan-fy2026-q3": 2026, + "ed-fsa-portfolio-by-age-fy2026-q2": 2026, + "ed-fsa-portfolio-by-debt-size-fy2026-q2": 2026, + "ed-fsa-portfolio-by-loan-type-fy2026-q3": 2026, + "ed-fsa-portfolio-by-location-fy2026-q2": 2026, + "ed-fsa-portfolio-summary-fy2026-q3": 2026, "jct-obbba-revenue-estimates-2025": 2026, "jct-tax-expenditures-2024": 2024, } diff --git a/tests/test_dimension_value_register.py b/tests/test_dimension_value_register.py index 9b7e235e..b90ba953 100644 --- a/tests/test_dimension_value_register.py +++ b/tests/test_dimension_value_register.py @@ -139,17 +139,31 @@ def test_the_register_records_what_it_was_built_from(): assert sources["script"] == "scripts/build_dimension_value_register.py" assert sources["packages"] == "chronicle.bundle.DEFAULT_BUNDLE_SOURCES" - assert sources["built_at_chronicle_commit"].startswith("aa288c3") + assert sources["built_at_chronicle_commit"].startswith("dece5e9") assert sources["default_build_year"] == 2023 assert sources["build_years"] == { "census-acs-s0101-congressional-district-age-2024": 2024, "census-acs-s0101-national-age-2024": 2024, "census-acs-s0101-state-age-2024": 2024, + "census-acs-s1501-state-attainment-2024": 2024, "census-acs-s2201-congressional-district-snap-2024": 2024, + "census-acs5-b01003-county-2024": 2024, + "census-acs5-b11001-county-2024": 2024, + "census-acs5-b19001-county-2024": 2024, + "census-acs5-b19013-county-2024": 2024, + "census-acs5-b19025-county-2024": 2024, + "census-acs5-b19051-county-2024": 2024, + "census-acs5-b19061-county-2024": 2024, "cms-aca-effectuated-enrollment-2022": 2022, "cms-aca-oep-state-level": 2024, "cms-aca-oep-state-level-2022": 2022, "cms-aca-oep-state-level-2025": 2025, + "ed-fsa-dl-portfolio-by-repayment-plan-fy2026-q3": 2026, + "ed-fsa-portfolio-by-age-fy2026-q2": 2026, + "ed-fsa-portfolio-by-debt-size-fy2026-q2": 2026, + "ed-fsa-portfolio-by-loan-type-fy2026-q3": 2026, + "ed-fsa-portfolio-by-location-fy2026-q2": 2026, + "ed-fsa-portfolio-summary-fy2026-q3": 2026, "jct-obbba-revenue-estimates-2025": 2026, "jct-tax-expenditures-2024": 2024, } From ab386672fa645b0147ea1d40d91dc21fd2d29b6b Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:51:48 +0400 Subject: [PATCH 13/13] Give the four values main's packages word two ways the maintainer's labels (#376) The maintainer chose these on 2026-10-26. They are in CHOSEN_LABELS (now 47) and the register: - freq=A: "Annual", Eurostat's exchange-rate dataset's category label. The seven older Eurostat packages' "A" stays as their package wording. - income_range=all: "All income ranges", the IRS SOI packages' wording. Census B19001's declaration "Total" now equals the register: the ACS county generator declares the register's label for a value the register lists, and Census's text stays in the measure labels and source cells. - gambling_commission.gsgb_activity=national_lottery_draws_in_person: "Tickets for National Lottery draws bought in person", GSGB Table B.10's wording. B.9's "Tickets National Lottery draws bought in person" stays as those facts' package wording. - salary_sacrifice_threshold_position=all: "All threshold positions", Chronicle's own wording. HMRC's package words the value "Employees using salary sacrifice to make pension contributions" and "National Insurance contributions forgone through salary sacrifice for pensions", and each names the measure, not the value. The package's declaration now equals the register; the rows keep their texts. The harness doc's label rule says when the register uses Chronicle's own wording, and that a register label is display text, so it invents no source value. The register holds 8,064 values in 157 dimensions. It is the committed one with these labels applied, written by the script's register_text. The script's full --check reads every package's built facts, so it was not run here. --static-check reports no problem. Tests: the register's chosen-label count; the four labels; a test that the salary sacrifice facts carry the Chronicle-written label and keep the rows' texts; GSGB's facts report no value-label issue; the Eurostat prior-vintage digests re-pinned for freq=A's label, with the earlier pins still checked after restoring "A"; ilc_di01's package wording now includes freq. Co-Authored-By: Claude Opus 5.5 --- .../dimension_values/canonical_labels.json | 12 +++- chronicle/sources/census_acs5_county.py | 8 ++- docs/agent-source-package-harness.md | 10 +++- .../uk-gambling-participation-gsgb-2025.md | 4 +- .../source_package.yaml | 2 +- .../source_package.yaml | 7 ++- scripts/build_dimension_value_register.py | 21 ++++++- scripts/build_uk_excise_packages.py | 5 +- tests/test_chronicle_bundle.py | 5 +- tests/test_dimension_value_register.py | 2 +- tests/test_etl_eurostat.py | 21 ++++++- tests/test_issue_376_one_label_per_value.py | 59 ++++++++++++++++++- ...st_source_gambling_commission_gsgb_2025.py | 25 +++++++- 13 files changed, 163 insertions(+), 18 deletions(-) diff --git a/chronicle/dimension_values/canonical_labels.json b/chronicle/dimension_values/canonical_labels.json index fb85aa7b..913d8e90 100644 --- a/chronicle/dimension_values/canonical_labels.json +++ b/chronicle/dimension_values/canonical_labels.json @@ -43,11 +43,14 @@ "age_band=30 to 39", "age_band=40 to 49", "early_year_award_line=new_returning_total_returning_31_33_35", + "freq=A", + "gambling_commission.gsgb_activity=national_lottery_draws_in_person", "groeipakket.component=pleegzorgtoeslag", "groeipakket.component=sociale_toeslag", "groeipakket.component=wezentoeslag", "groeipakket.component=zorgtoeslag", "household_type=couple_3_plus_children_households", + "income_range=all", "measure=country_total", "na_item=belgium_household_disposable_income_gross", "na_item=belgium_household_mixed_income_gross", @@ -72,6 +75,7 @@ "quant_inc=d7", "quant_inc=d8", "quant_inc=d9", + "salary_sacrifice_threshold_position=all", "sex=female", "sex=male", "us:statutes/26/62#adjusted_gross_income=all", @@ -7178,7 +7182,7 @@ "all": "All filing statuses" }, "freq": { - "A": "A" + "A": "Annual" }, "frequency": { "annual": "Annual", @@ -7194,6 +7198,9 @@ "local_authority_bus_support": "Local authority bus support", "network_support_grant": "Network Support Grant" }, + "gambling_commission.gsgb_activity": { + "national_lottery_draws_in_person": "Tickets for National Lottery draws bought in person" + }, "gender": { "Female": "Female", "Male": "Male", @@ -8017,6 +8024,9 @@ "age_98": "Aged 98 years", "age_99": "Aged 99 years" }, + "salary_sacrifice_threshold_position": { + "all": "All threshold positions" + }, "scheme_type": { "Defined benefit": "Defined benefit", "Defined contribution": "Defined contribution", diff --git a/chronicle/sources/census_acs5_county.py b/chronicle/sources/census_acs5_county.py index 2ddb2c42..6a76d792 100644 --- a/chronicle/sources/census_acs5_county.py +++ b/chronicle/sources/census_acs5_county.py @@ -17,6 +17,7 @@ from openpyxl.utils import get_column_letter import yaml +from chronicle.dimension_labels import canonical_value_label from chronicle.epoch import schema_id from chronicle.yaml_io import safe_load @@ -391,8 +392,13 @@ def build_package(table: str, root: Path = ROOT) -> dict[str, Any]: "B19001": "Income range", "B19051": "Earnings status", }[table] + # A value Chronicle's label register lists is declared with the + # register's label (chronicle#376): B19001's "Total" is income_range + # all, which the IRS SOI packages call "All income ranges". Census's + # own text stays in the measure labels and the source cells. values[category_dimension] = { - category: label for _, category, label in _categories(table) + category: canonical_value_label(category_dimension, category) or label + for _, category, label in _categories(table) } payload = { "schema_version": schema_id("source_package"), diff --git a/docs/agent-source-package-harness.md b/docs/agent-source-package-harness.md index bc391de7..63b34632 100644 --- a/docs/agent-source-package-harness.md +++ b/docs/agent-source-package-harness.md @@ -905,7 +905,15 @@ register: the value is either listed, and takes the listed label, or needs an entry. To choose a register label for a value its packages word differently, take the wording most packages use, ties going to the national statistics office's wording, without footnote markers or words that name the measure, and -keep each package's text in its rows. The register is generated by +keep each package's text in its rows. Where every wording names the measure +rather than the value, no package's text can be the label, and the maintainer +gives the value Chronicle's own wording: HMRC's salary sacrifice package words +`salary_sacrifice_threshold_position=all` "Employees using salary sacrifice to +make pension contributions" and "National Insurance contributions forgone +through salary sacrifice for pensions", and the register calls it "All threshold +positions". A register label is display text, not a source value, so such a +label invents no fact; the packages' texts stay in their rows and in +`package_dimension_value_labels`. The register is generated by `scripts/build_dimension_value_register.py`, which holds the chosen labels; add a chosen label there, rerun it, and commit both files. Its `--static-check` mode compares the register with the package files without building them (every diff --git a/docs/data-sources/uk-gambling-participation-gsgb-2025.md b/docs/data-sources/uk-gambling-participation-gsgb-2025.md index 97c8ab7f..26707c51 100644 --- a/docs/data-sources/uk-gambling-participation-gsgb-2025.md +++ b/docs/data-sources/uk-gambling-participation-gsgb-2025.md @@ -109,7 +109,9 @@ The values: markers dropped. - B.9's row 6 reads "Tickets National Lottery draws bought in person", where B.10's reads "Tickets for National Lottery draws bought in person". - - Both are kept, so the bundle reports a groupby-label warning for that row. + - Both are kept in the rows. Chronicle's label register gives the value B.10's + wording, and B.9's facts keep their row's text in + `package_dimension_value_labels` (chronicle#376). - B.2 and B.4's rows are named by area, on a `gambling_commission.gsgb_area` axis. - **Left out:** - weighted bases; diff --git a/packages/census/acs5_b19001_county_2024/source_package.yaml b/packages/census/acs5_b19001_county_2024/source_package.yaml index a68eb7d9..1918b5b6 100644 --- a/packages/census/acs5_b19001_county_2024/source_package.yaml +++ b/packages/census/acs5_b19001_county_2024/source_package.yaml @@ -13,7 +13,7 @@ dimension_value_labels: estimate: Estimate margin_of_error: Margin of error income_range: - all: Total + all: All income ranges under_10000: Less than $10,000 10000_to_14999: $10,000 to $14,999 15000_to_19999: $15,000 to $19,999 diff --git a/packages/hmrc/salary_sacrifice_reform_2029_headcounts/source_package.yaml b/packages/hmrc/salary_sacrifice_reform_2029_headcounts/source_package.yaml index 75ad3c05..5f1b343d 100644 --- a/packages/hmrc/salary_sacrifice_reform_2029_headcounts/source_package.yaml +++ b/packages/hmrc/salary_sacrifice_reform_2029_headcounts/source_package.yaml @@ -41,7 +41,12 @@ dimension_labels: 'hmrc.salary_sacrifice_employer_arrangement': 'Employer salary sacrifice arrangement for pension contributions' dimension_value_labels: 'salary_sacrifice_threshold_position': - 'all': 'Employees using salary sacrifice to make pension contributions' + # Chronicle's register gives this label, its own wording (chronicle#376): + # the rows' texts for this value, "Employees using salary sacrifice to make + # pension contributions" and "National Insurance contributions forgone + # through salary sacrifice for pensions", name the measure, and they stay + # in the rows. + 'all': 'All threshold positions' 'within_2000': 'Employees fully protected by the GBP 2,000 threshold' 'salary_sacrifice_employer_arrangement': 'operates': 'Operates salary sacrifice arrangements for pension contributions' diff --git a/scripts/build_dimension_value_register.py b/scripts/build_dimension_value_register.py index 174df6dd..26f9735f 100644 --- a/scripts/build_dimension_value_register.py +++ b/scripts/build_dimension_value_register.py @@ -96,7 +96,10 @@ #: The label of each value its packages word more than one way. The three US #: labels are the maintainer's choice; the other 40 were proposed in #: chronicle#377 and approved on 2026-10-21, coicop_02 with ONS's overview -#: wording after review. +#: wording after review. The maintainer chose four more on 2026-10-26, for +#: values the packages merged from main word two ways: freq=A, GSGB's +#: national_lottery_draws_in_person, income_range=all and +#: salary_sacrifice_threshold_position=all. CHOSEN_LABELS: dict[tuple[str, str], str] = { (AGI, "under_1"): "No adjusted gross income", (AGI, "all"): "All returns, total", @@ -113,6 +116,15 @@ "early_year_award_line", "new_returning_total_returning_31_33_35", ): "Total returning [31][33][35]", + # Eurostat's exchange-rate dataset's JSON-stat category label; the older + # Eurostat packages carry the bare code "A". + ("freq", "A"): "Annual", + # B.10's wording; B.9 writes "Tickets National Lottery draws bought in + # person" for the same row. + ( + "gambling_commission.gsgb_activity", + "national_lottery_draws_in_person", + ): "Tickets for National Lottery draws bought in person", ("groeipakket.component", "pleegzorgtoeslag"): "Foster care supplement", ("groeipakket.component", "sociale_toeslag"): "Social supplement", ("groeipakket.component", "wezentoeslag"): "Orphan supplement", @@ -125,6 +137,8 @@ "ons.household_type", "couple_3_plus_children_households", ): "3 or more dependent children", + # The IRS SOI packages' wording; Census B19001 writes "Total". + ("income_range", "all"): "All income ranges", ("measure", "country_total"): "Country total", ( "na_item", @@ -163,6 +177,11 @@ ("quant_inc", "d7"): "Seventh income decile", ("quant_inc", "d8"): "Eighth income decile", ("quant_inc", "d9"): "Ninth income decile", + # Chronicle's own wording. HMRC's salary sacrifice package words this value + # "Employees using salary sacrifice to make pension contributions" and + # "National Insurance contributions forgone through salary sacrifice for + # pensions": each names the measure, not the threshold position. + ("salary_sacrifice_threshold_position", "all"): "All threshold positions", ("sex", "female"): "Female", ("sex", "male"): "Male", } diff --git a/scripts/build_uk_excise_packages.py b/scripts/build_uk_excise_packages.py index 7e83bb6e..6b4f8af2 100644 --- a/scripts/build_uk_excise_packages.py +++ b/scripts/build_uk_excise_packages.py @@ -3602,8 +3602,9 @@ def build_gambling_commission() -> None: ), ] # Row labels are each table's own text (whitespace collapsed, footnote marker dropped), -# so B.9 row 6's wording differs from B.10's and the bundle reports that row's two -# labels as a groupby-label warning, as it does for any label kept as published. +# so B.9 row 6's wording differs from B.10's. Chronicle's label register gives the +# value B.10's wording, and B.9's facts keep their row's text as the package's wording +# (chronicle#376). GSGB_ACTIVITY_VARIANTS = { "Tickets National Lottery draws bought in person": ( "Tickets for National Lottery draws bought in person" diff --git a/tests/test_chronicle_bundle.py b/tests/test_chronicle_bundle.py index 63bf3f4f..7590b23b 100644 --- a/tests/test_chronicle_bundle.py +++ b/tests/test_chronicle_bundle.py @@ -536,8 +536,9 @@ def test_build_bundle_writes_merged_consumer_contract(tmp_path): # chronicle#376 removes 41: every dimension value two packages emit # takes its label from chronicle/dimension_values/canonical_labels.json, # so the 17 values two packages labelled differently and the 24 values - # one package's rows labelled two ways now carry one label each, and a - # value labelled two ways is an error rather than a warning. + # one package's rows labelled two ways now carry one label each (freq=A + # 'Annual', and B.10's wording for GSGB's row), and a value labelled + # two ways is an error rather than a warning. "warning_count": 92, } row_count = 0 diff --git a/tests/test_dimension_value_register.py b/tests/test_dimension_value_register.py index b90ba953..fbe67461 100644 --- a/tests/test_dimension_value_register.py +++ b/tests/test_dimension_value_register.py @@ -128,7 +128,7 @@ def test_the_chosen_labels_are_the_registers_labels(): script = _script() register = json.loads(_REGISTER.read_text(encoding="utf-8"))["labels"] - assert len(script.CHOSEN_LABELS) == 43 + assert len(script.CHOSEN_LABELS) == 47 for (dimension_id, value_id), label in script.CHOSEN_LABELS.items(): assert register[dimension_id][value_id] == label diff --git a/tests/test_etl_eurostat.py b/tests/test_etl_eurostat.py index ffa1be21..569950a0 100644 --- a/tests/test_etl_eurostat.py +++ b/tests/test_etl_eurostat.py @@ -67,6 +67,15 @@ # the facts still hash to the digests before it (below): only the empty # field moved. PRIOR_VINTAGE_FACT_DIGESTS = { + "eurostat-gov-10a-taxag": ( + "c2e3256a6550d6864c60843591c2c5755d816aa23b97aeea8b051b6ac5b8d39a" + ), + "eurostat-spr-exp-func": ( + "f9a785e52cd2c1df0e3e97828da34a3a15348f7bc89d106e948eccff11e53d61" + ), +} +#: The digests before freq=A took the register's label "Annual" (chronicle#376). +FACT_DIGESTS_BEFORE_ANNUAL = { "eurostat-gov-10a-taxag": ( "516a28c598db59fa4aafd0abef2fd3339a22495e3f2143a0b137e7290672f9d5" ), @@ -444,10 +453,18 @@ def test_eurostat_prior_vintage_facts_remain_byte_stable(alias): fact_payloads = [asdict(fact) for fact in _package_outputs(alias)[3]] assert _fact_payload_digest(fact_payloads) == PRIOR_VINTAGE_FACT_DIGESTS[alias] - # Only the new, empty package-wording field moved the bytes (chronicle#376). + # Only labels moved the bytes (chronicle#376). Every fact's freq=A takes the + # register's "Annual", with the package's own "A" beside it. assert all( - payload["package_dimension_value_labels"] == {} for payload in fact_payloads + payload["dimension_value_labels"]["freq"] == {"A": "Annual"} + and payload["package_dimension_value_labels"] == {"freq": {"A": "A"}} + for payload in fact_payloads ) + for payload in fact_payloads: + payload["dimension_value_labels"]["freq"] = {"A": "A"} + payload["package_dimension_value_labels"] = {} + assert _fact_payload_digest(fact_payloads) == FACT_DIGESTS_BEFORE_ANNUAL[alias] + # Before that, only the new, empty package-wording field had moved them. for payload in fact_payloads: del payload["package_dimension_value_labels"] assert ( diff --git a/tests/test_issue_376_one_label_per_value.py b/tests/test_issue_376_one_label_per_value.py index 7209126f..53350149 100644 --- a/tests/test_issue_376_one_label_per_value.py +++ b/tests/test_issue_376_one_label_per_value.py @@ -125,6 +125,61 @@ def test_the_register_holds_the_three_chosen_us_labels(): assert dimension_labels.canonical_value_label("age", "85_plus") == "Age 85 plus" +def test_the_register_holds_the_four_labels_chosen_after_merging_main(): + """The maintainer's 2026-10-26 choices for values main's packages word two ways.""" + assert dimension_labels.canonical_value_label("freq", "A") == "Annual" + assert ( + dimension_labels.canonical_value_label("income_range", "all") + == "All income ranges" + ) + assert ( + dimension_labels.canonical_value_label( + "gambling_commission.gsgb_activity", "national_lottery_draws_in_person" + ) + == "Tickets for National Lottery draws bought in person" + ) + assert ( + dimension_labels.canonical_value_label( + "salary_sacrifice_threshold_position", "all" + ) + == "All threshold positions" + ) + + +def test_a_chronicle_written_label_keeps_the_rows_text_beside_it(): + """salary_sacrifice_threshold_position=all: both row texts name the measure.""" + package = load_source_package("hmrc-salary-sacrifice-reform-2029-headcounts") + facts = package.build_facts(2023, cells=package.build_source_cells(2023)) + all_positions = [ + fact + for fact in facts + if fact.filters.get("salary_sacrifice_threshold_position") == "all" + ] + + assert all_positions + assert { + fact.dimension_value_labels["salary_sacrifice_threshold_position"]["all"] + for fact in all_positions + } == {"All threshold positions"} + # Each row keeps its own text: the headcount row as its groupby label, the + # forgone National Insurance rows also as the package's wording of the value. + assert {fact.layout.groupby_value_label for fact in all_positions} == { + "Employees using salary sacrifice to make pension contributions", + "National Insurance contributions forgone through salary sacrifice for " + "pensions", + } + assert { + fact.package_dimension_value_labels.get( + "salary_sacrifice_threshold_position", {} + ).get("all") + for fact in all_positions + } == { + None, + "National Insurance contributions forgone through salary sacrifice for " + "pensions", + } + + def test_the_register_is_one_file_with_one_label_per_value(): payload = json.loads(_REGISTER.read_text(encoding="utf-8")) @@ -676,8 +731,10 @@ def test_a_groupby_value_whose_axis_is_also_a_filter_is_keyed_by_its_row_value() assert fact.filters["quant_inc"] == "D1" assert fact.layout.groupby_value_label == "First income decile" + # freq=A takes the register's "Annual"; the package's "A" is kept beside it. assert fact.package_dimension_value_labels == { - "quant_inc": {"d1": "First income decile share"} + "freq": {"A": "A"}, + "quant_inc": {"d1": "First income decile share"}, } diff --git a/tests/test_source_gambling_commission_gsgb_2025.py b/tests/test_source_gambling_commission_gsgb_2025.py index 62f48fc9..5a310c03 100644 --- a/tests/test_source_gambling_commission_gsgb_2025.py +++ b/tests/test_source_gambling_commission_gsgb_2025.py @@ -126,11 +126,30 @@ def test_facts_are_valid_labelled_and_unique(facts): assert len(facts) == 1024 assert validate_facts(facts).valid assert validate_consumer_fact_contract(facts).valid - # B.9 and B.10 word row 6 differently, and both labels are kept as published. + # B.9 and B.10 word row 6 differently. Chronicle's label register gives the + # value B.10's wording (chronicle#376), and B.9's facts keep their row's + # text as the package's wording. issues = dimension_label_issues(consumer_fact_rows(facts)) - assert [(issue.code, issue.value_id) for issue in issues] == [ - ("conflicting_groupby_value_label", "national_lottery_draws_in_person") + assert [(issue.code, issue.value_id) for issue in issues] == [] + in_person = [ + fact + for fact in facts + if fact.layout.groupby_value_id == "national_lottery_draws_in_person" ] + assert {fact.layout.groupby_value_label for fact in in_person} == { + "Tickets for National Lottery draws bought in person" + } + assert { + fact.filters["source_sheet"]: ( + fact.package_dimension_value_labels.get( + "gambling_commission.gsgb_activity", {} + ).get("national_lottery_draws_in_person") + ) + for fact in in_person + } == { + "Table B.9": "Tickets National Lottery draws bought in person", + "Table B.10": None, + } assert max(Counter(build_semantic_fact_key(f) for f in facts).values()) == 1 assert Counter(f.filters["source_sheet"] for f in facts) == { "Table B.1": 72,