From af64d3757c2648e820219faee96186d3d7cd9a63 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 23:44:30 -0400 Subject: [PATCH] Derive property_wealth from its uprated components instead of saving it The WAS imputation saved `property_wealth` (DVPropertyR8) as its own household column. policyengine-uk defines `property_wealth` as the sum of main_residence_value, other_residential_property_value and non_residential_property_value, and a saved column overrides that sum. The saved column has no uprating index, so on release 1.57.4 it was identical per household in 2024, 2025 and 2026 while its components were uprated, and in 2026 it fell below main_residence_value for 29,677 of 35,672 owner households. Household land, total land and total wealth inherited the freeze. Keep the WAS total in the sequential QRF, since the later outputs are predicted conditional on it, but stop writing it, and stop rescaling it in the regional HPI step. Add synthetic (Hypothesis) and built-dataset tests that property_wealth equals the sum of its components in every year, grows with them, and is at least main_residence_value. Co-Authored-By: Claude Opus 5.5 --- .../property-wealth-from-components.fixed.md | 1 + docs/imputations.md | 2 +- .../imputations/regional_property_uprating.py | 14 +- .../datasets/imputations/wealth.py | 40 ++- .../tests/test_property_wealth_components.py | 273 ++++++++++++++++++ .../tests/test_regional_property_uprating.py | 26 +- pyproject.toml | 1 + uv.lock | 73 ++++- 8 files changed, 398 insertions(+), 32 deletions(-) create mode 100644 changelog.d/property-wealth-from-components.fixed.md create mode 100644 policyengine_uk_data/tests/test_property_wealth_components.py diff --git a/changelog.d/property-wealth-from-components.fixed.md b/changelog.d/property-wealth-from-components.fixed.md new file mode 100644 index 000000000..496b9e723 --- /dev/null +++ b/changelog.d/property-wealth-from-components.fixed.md @@ -0,0 +1 @@ +Stop saving the WAS-imputed `property_wealth` column. policyengine-uk defines `property_wealth` as `main_residence_value + other_residential_property_value + non_residential_property_value`, but a saved column overrides that sum and has no uprating index, so it stayed at its 2024 value while the components were uprated: on release 1.57.4 it was identical per household in 2024, 2025 and 2026, and in 2026 it was below `main_residence_value` for 29,677 of 35,672 owner households. The saved column was not the sum of the components even in 2024 (£8,126.9bn against £8,934.1bn weighted; different for 12,476 of 52,846 households). The WAS total is still imputed, because the later wealth outputs are predicted conditional on it, but it is no longer written, so `property_wealth`, `household_land_value`, `land_value` and `total_wealth` now follow the uprated components. The household and regional land targets are unchanged, but the land the calibration fits to them now comes from the components: at 1.57.4's weights, 2025 household land is £5,752.3bn rather than £5,165.4bn against a £5,042.9bn target, so the next build's weights will move. diff --git a/docs/imputations.md b/docs/imputations.md index 9e6964ed3..f4c6e1728 100644 --- a/docs/imputations.md +++ b/docs/imputations.md @@ -42,7 +42,7 @@ Imputes household wealth components using demographic and income predictors. | Variable | Description | |----------|-------------| | `owned_land` | Value of owned land | -| `property_wealth` | Total property wealth | +| `property_wealth` | Total property wealth (conditions the later outputs; not saved, as policyengine-uk sums the three property components) | | `corporate_wealth` | Shares, pensions, investment ISAs | | `gross_financial_wealth` | Total financial assets | | `net_financial_wealth` | Financial assets minus liabilities | diff --git a/policyengine_uk_data/datasets/imputations/regional_property_uprating.py b/policyengine_uk_data/datasets/imputations/regional_property_uprating.py index 68aa7e37b..6abb9ef2e 100644 --- a/policyengine_uk_data/datasets/imputations/regional_property_uprating.py +++ b/policyengine_uk_data/datasets/imputations/regional_property_uprating.py @@ -3,10 +3,9 @@ The WAS QRF imputation compresses regional property values — London is undervalued by ~41% and the North East overvalued by ~39% relative to -UK HPI Dec 2025. This module rescales main_residence_value (and -property_wealth proportionally) so that regional means match official -average house prices, following the same pattern as -``_calibrate_energy_to_need`` in ``consumption.py``. +UK HPI Dec 2025. This module rescales main_residence_value so that +regional means match official average house prices, following the same +pattern as ``_calibrate_energy_to_need`` in ``consumption.py``. """ import numpy as np @@ -32,8 +31,9 @@ def _calibrate_property_to_hpi(household: pd.DataFrame) -> pd.DataFrame: applies it multiplicatively. This preserves within-region distributional shape while anchoring the level to admin data. - property_wealth is adjusted by the same factor so that the land-to- - property ratio stays consistent. + property_wealth is not stored: policyengine-uk sums it from + main_residence_value and the other property components, so it picks up + this rescaling without being scaled here. """ hpi_prices = _load_regional_house_prices() region = household["region"].values @@ -56,7 +56,6 @@ def _calibrate_property_to_hpi(household: pd.DataFrame) -> pd.DataFrame: factor = hpi_price / imputed_mean household.loc[owners_mask, "main_residence_value"] *= factor - household.loc[owners_mask, "property_wealth"] *= factor return household @@ -77,7 +76,6 @@ def uprate_property_by_region( household = _calibrate_property_to_hpi(household) dataset.household["main_residence_value"] = household["main_residence_value"].values - dataset.household["property_wealth"] = household["property_wealth"].values dataset.validate() return dataset diff --git a/policyengine_uk_data/datasets/imputations/wealth.py b/policyengine_uk_data/datasets/imputations/wealth.py index 36c5fd4dc..515dedddd 100644 --- a/policyengine_uk_data/datasets/imputations/wealth.py +++ b/policyengine_uk_data/datasets/imputations/wealth.py @@ -59,6 +59,14 @@ "student_loan_balance", ] +# Imputed in sequence so the variables after it are predicted conditional on +# it, but never saved. policyengine-uk defines `property_wealth` as the sum of +# `main_residence_value`, `other_residential_property_value` and +# `non_residential_property_value`; a saved column overrides that formula and, +# having no uprating index, stays frozen at the dataset year while the +# components are uprated. +CONDITIONING_ONLY_VARIABLES = ("property_wealth",) + WAS_RENAMES = { "R8xshhwgt": "household_weight", # Components for estimating land holdings. @@ -269,6 +277,27 @@ def _allocate_student_loan_balance_to_people( return balances +def store_wealth_predictions( + dataset: UKSingleYearDataset, predictions: pd.DataFrame +) -> UKSingleYearDataset: + """Write imputed wealth to the dataset, skipping conditioning-only outputs. + + `student_loan_balance` is allocated to people; every other saved output is + a household column. + """ + for column in predictions.columns: + if column in CONDITIONING_ONLY_VARIABLES: + continue + if column == "student_loan_balance": + dataset.person[column] = _allocate_student_loan_balance_to_people( + household_balances=predictions[column].clip(lower=0), + person=dataset.person, + ) + continue + dataset.household[column] = predictions[column].values + return dataset + + def save_imputation_models(): """ Train and save wealth imputation model. @@ -339,16 +368,7 @@ def impute_wealth(dataset: UKSingleYearDataset) -> UKSingleYearDataset: "NORTHERN_IRELAND", "WALES" ) # WAS doesn't sample NI -> put NI households in Wales (closest aggregate) output_df = model.predict(input_df) - - for column in output_df.columns: - if column == "student_loan_balance": - dataset.person[column] = _allocate_student_loan_balance_to_people( - household_balances=output_df[column].clip(lower=0), - person=dataset.person, - ) - continue - dataset.household[column] = output_df[column].values - + dataset = store_wealth_predictions(dataset, output_df) dataset.validate() return dataset diff --git a/policyengine_uk_data/tests/test_property_wealth_components.py b/policyengine_uk_data/tests/test_property_wealth_components.py new file mode 100644 index 000000000..e0c17bf62 --- /dev/null +++ b/policyengine_uk_data/tests/test_property_wealth_components.py @@ -0,0 +1,273 @@ +"""property_wealth must come from its uprated components, not a saved column. + +policyengine-uk defines `property_wealth` as main_residence_value + +other_residential_property_value + non_residential_property_value. A saved +`property_wealth` column overrides that sum and, having no uprating index, +stays at its dataset-year value while the components are uprated. + +The synthetic tests need no private data. The built-dataset tests run after +`make data` and are skipped when the enhanced FRS is absent. Failure messages +report counts and maxima only, never individual records. +""" + +import numpy as np +import pandas as pd +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from policyengine_uk import Microsimulation +from policyengine_uk.data import UKSingleYearDataset + +from policyengine_uk_data.datasets.frs_release import CURRENT_FRS_RELEASE +from policyengine_uk_data.datasets.imputations.regional_property_uprating import ( + _load_regional_house_prices, + uprate_property_by_region, +) +from policyengine_uk_data.datasets.imputations.wealth import ( + CONDITIONING_ONLY_VARIABLES, + IMPUTE_VARIABLES, + store_wealth_predictions, +) +from policyengine_uk_data.storage import STORAGE_FOLDER +from policyengine_uk_data.utils.uprating import uprate_dataset + +COMPONENTS = ( + "main_residence_value", + "other_residential_property_value", + "non_residential_property_value", +) +BASE_YEAR = CURRENT_FRS_RELEASE.base_year +# Two years past the base year: 2026 for the 2024-25 FRS. +PROJECTION_YEAR = BASE_YEAR + 2 +YEARS = sorted({BASE_YEAR, CURRENT_FRS_RELEASE.calibration_year, PROJECTION_YEAR}) +RTOL = 1e-5 + + +def _calculate(sim, variable, year) -> np.ndarray: + return np.asarray(sim.calculate(variable, year).values, dtype=float) + + +def _property_arrays(sim, years) -> dict: + out = {} + for year in years: + main = _calculate(sim, "main_residence_value", year) + components = sum(_calculate(sim, name, year) for name in COMPONENTS) + out[year] = { + "property_wealth": _calculate(sim, "property_wealth", year), + "main_residence_value": main, + "components": components, + } + return out + + +def _n_not_close(actual, expected) -> tuple[int, float]: + gap = np.abs(actual - expected) + bad = gap > RTOL * np.abs(expected) + 1.0 + return int(bad.sum()), float(gap[bad].sum()) + + +def _synthetic_dataset(household: pd.DataFrame) -> UKSingleYearDataset: + n = len(household) + ids = np.arange(n) + return UKSingleYearDataset( + person=pd.DataFrame( + { + "person_id": ids, + "person_benunit_id": ids, + "person_household_id": ids, + "age": np.full(n, 45), + } + ), + benunit=pd.DataFrame({"benunit_id": ids}), + household=household.assign(household_id=ids), + fiscal_year=BASE_YEAR, + ) + + +def test_property_wealth_is_imputed_before_its_components(): + """The components are predicted conditional on the WAS property total.""" + assert set(CONDITIONING_ONLY_VARIABLES) <= set(IMPUTE_VARIABLES) + position = IMPUTE_VARIABLES.index("property_wealth") + assert "property_wealth" in CONDITIONING_ONLY_VARIABLES + for component in COMPONENTS: + assert IMPUTE_VARIABLES.index(component) > position + + +def test_impute_wealth_does_not_save_property_wealth(monkeypatch): + """The imputation step drops the WAS total and keeps the components.""" + from policyengine_uk_data.datasets.imputations import wealth + + predicted = { + "property_wealth": [250_000.0, 0.0], + "main_residence_value": [200_000.0, 0.0], + "other_residential_property_value": [90_000.0, 0.0], + "non_residential_property_value": [0.0, 0.0], + } + + class DummyModel: + input_columns = ["region"] + + @staticmethod + def predict(input_df): + return pd.DataFrame(predicted, index=input_df.index) + + class DummyMicrosimulation: + def __init__(self, dataset): + self.regions = dataset.household["region"] + + def calculate_dataframe(self, predictors, map_to): + return pd.DataFrame({"region": self.regions.values}) + + monkeypatch.setattr(wealth, "create_wealth_model", lambda: DummyModel()) + monkeypatch.setattr(wealth, "Microsimulation", DummyMicrosimulation) + household = pd.DataFrame( + {"household_weight": [1.0, 1.0], "region": ["LONDON", "WALES"]} + ) + imputed = wealth.impute_wealth(_synthetic_dataset(household)) + + assert "property_wealth" not in imputed.household.columns + for component in COMPONENTS: + assert imputed.household[component].tolist() == predicted[component] + + +value = st.one_of( + st.just(0.0), + st.floats(min_value=1_000, max_value=5_000_000, allow_nan=False), +) +household_rows = st.lists( + st.tuples( + st.sampled_from(sorted(_load_regional_house_prices())), + value, # main residence + value, # other residential property + value, # non-residential property + value, # WAS property total, imputed independently of the components + ), + min_size=1, + max_size=8, +) + + +@settings( + max_examples=25, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@given(rows=household_rows) +def test_saved_property_wealth_follows_its_components(rows): + """Invariants for any non-negative imputed components and any WAS total: + + - no `property_wealth` column is saved; + - property_wealth equals the sum of its components in every year, + including after uk-data materialises the calibration year; + - property_wealth >= main_residence_value (P4); + - property_wealth grows exactly as its components do. + """ + regions, main, other, non_res, was_total = map(list, zip(*rows)) + n = len(rows) + household = pd.DataFrame( + { + "household_weight": np.ones(n), + "region": regions, + "council_tax": np.zeros(n), + "rent": np.zeros(n), + "tenure_type": ["OWNED_OUTRIGHT"] * n, + } + ) + predictions = pd.DataFrame( + { + "property_wealth": was_total, + "main_residence_value": main, + "other_residential_property_value": other, + "non_residential_property_value": non_res, + } + ) + dataset = store_wealth_predictions(_synthetic_dataset(household), predictions) + dataset = uprate_property_by_region(dataset) + assert "property_wealth" not in dataset.household.columns + + arrays = _property_arrays(Microsimulation(dataset=dataset), YEARS) + calibration_year = CURRENT_FRS_RELEASE.calibration_year + materialised = uprate_dataset(dataset, calibration_year) + # uprate_dataset stores an int year; policyengine-uk expects a string. + materialised.time_period = str(materialised.time_period) + arrays_materialised = _property_arrays( + Microsimulation(dataset=materialised), [calibration_year] + ) + + for a in [*arrays.values(), *arrays_materialised.values()]: + np.testing.assert_allclose( + a["property_wealth"], a["components"], rtol=RTOL, atol=1.0 + ) + assert np.all(a["property_wealth"] >= a["main_residence_value"]) + + base, later = arrays[BASE_YEAR], arrays[PROJECTION_YEAR] + owns = base["components"] > 0 + np.testing.assert_allclose( + later["property_wealth"][owns] / base["property_wealth"][owns], + later["components"][owns] / base["components"][owns], + rtol=RTOL, + ) + + +@pytest.fixture(scope="module") +def built_property_arrays(): + try: + dataset = UKSingleYearDataset( + STORAGE_FOLDER / CURRENT_FRS_RELEASE.enhanced_dataset_file + ) + except FileNotFoundError: + pytest.skip("Enhanced FRS dataset not available") + stored = "property_wealth" in dataset.household.columns + return stored, _property_arrays(Microsimulation(dataset=dataset), YEARS) + + +def test_built_dataset_does_not_save_property_wealth(built_property_arrays): + stored, _ = built_property_arrays + assert not stored, "the enhanced FRS saves a property_wealth column" + + +@pytest.mark.parametrize("year", YEARS, ids=map(str, YEARS)) +def test_built_property_wealth_is_the_sum_of_its_components( + built_property_arrays, year +): + _, arrays = built_property_arrays + a = arrays[year] + n_bad, total_gap = _n_not_close(a["property_wealth"], a["components"]) + assert n_bad == 0, ( + f"{year}: property_wealth differs from the sum of its components for " + f"{n_bad} of {len(a['components'])} households (unweighted absolute " + f"gap £{total_gap / 1e9:,.1f}bn)" + ) + + +@pytest.mark.parametrize("year", YEARS, ids=map(str, YEARS)) +def test_built_property_wealth_at_least_main_residence(built_property_arrays, year): + """P4: property_wealth >= main_residence_value for every household.""" + _, arrays = built_property_arrays + a = arrays[year] + below = a["property_wealth"] < a["main_residence_value"] + owners = a["main_residence_value"] > 0 + assert not below.any(), ( + f"{year}: property_wealth < main_residence_value for {int(below.sum())} " + f"households ({int((below & owners).sum())} of {int(owners.sum())} owners)" + ) + + +def test_built_property_wealth_grows_with_its_components(built_property_arrays): + """Per household, property_wealth(projection) / property_wealth(base) + equals the same ratio for the sum of its components.""" + _, arrays = built_property_arrays + base, later = arrays[BASE_YEAR], arrays[PROJECTION_YEAR] + owns = base["components"] > 0 + assert owns.any() + with np.errstate(divide="ignore", invalid="ignore"): + growth = later["property_wealth"][owns] / base["property_wealth"][owns] + component_growth = later["components"][owns] / base["components"][owns] + bad = ~(np.abs(growth - component_growth) <= RTOL * component_growth) + assert not bad.any(), ( + f"property_wealth growth {BASE_YEAR}-{PROJECTION_YEAR} differs from " + f"component growth for {int(bad.sum())} of {int(owns.sum())} households " + f"with property (mean property_wealth growth " + f"{np.nanmean(growth):.4f}, mean component growth " + f"{np.mean(component_growth):.4f})" + ) diff --git a/policyengine_uk_data/tests/test_regional_property_uprating.py b/policyengine_uk_data/tests/test_regional_property_uprating.py index dd49d9e98..622ff402b 100644 --- a/policyengine_uk_data/tests/test_regional_property_uprating.py +++ b/policyengine_uk_data/tests/test_regional_property_uprating.py @@ -42,7 +42,6 @@ def test_calibration_rescales_means(): { "region": region, "main_residence_value": v, - "property_wealth": v * 1.1, "household_weight": 1.0, } for v in values @@ -53,7 +52,6 @@ def test_calibration_rescales_means(): { "region": region, "main_residence_value": 0.0, - "property_wealth": 0.0, "household_weight": 1.0, } for _ in range(10) @@ -79,7 +77,6 @@ def test_renters_unchanged(): { "region": ["LONDON", "LONDON", "LONDON"], "main_residence_value": [500_000.0, 0.0, 0.0], - "property_wealth": [500_000.0, 0.0, 0.0], "household_weight": [1.0, 1.0, 1.0], } ) @@ -88,20 +85,25 @@ def test_renters_unchanged(): assert calibrated.iloc[2]["main_residence_value"] == 0.0 -def test_property_wealth_scales_proportionally(): - """property_wealth should scale by the same factor as main_residence_value.""" - prices = _load_regional_house_prices() +def test_only_main_residence_value_is_rescaled(): + """Other property columns are untouched and no property_wealth is created. + + policyengine-uk sums property_wealth from main_residence_value and the + other property components, so the rescaling reaches it through them. + """ df = pd.DataFrame( { "region": ["LONDON", "LONDON"], "main_residence_value": [400_000.0, 300_000.0], - "property_wealth": [500_000.0, 400_000.0], + "other_residential_property_value": [150_000.0, 0.0], + "non_residential_property_value": [0.0, 80_000.0], "household_weight": [1.0, 1.0], } ) - original_ratio = df["property_wealth"].values / df["main_residence_value"].values calibrated = _calibrate_property_to_hpi(df) - new_ratio = ( - calibrated["property_wealth"].values / calibrated["main_residence_value"].values - ) - np.testing.assert_allclose(original_ratio, new_ratio, rtol=1e-10) + assert "property_wealth" not in calibrated.columns + for column in ( + "other_residential_property_value", + "non_residential_property_value", + ): + np.testing.assert_array_equal(calibrated[column], df[column]) diff --git a/pyproject.toml b/pyproject.toml index beff7f1a4..58de5b1d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ dev = [ "ruff>=0.9.0", "pytest", + "hypothesis", "torch", "l0-python>=0.4.0", "tables", diff --git a/uv.lock b/uv.lock index a3e9f44d9..94928c832 100644 --- a/uv.lock +++ b/uv.lock @@ -577,6 +577,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/f4/124858007ddf3c61e9b144107304c9152fa80b5b6c168da07d86fe583cc1/huggingface_hub-1.1.5-py3-none-any.whl", hash = "sha256:e88ecc129011f37b868586bbcfae6c56868cae80cd56a79d61575426a3aa0d7d", size = 516000, upload-time = "2025-11-20T15:49:30.926Z" }, ] +[[package]] +name = "hypothesis" +version = "6.168.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/a8/bd70d7c2966e561228b9fdc075ee77c0ba577dcbbfbf921edf614db14f6a/hypothesis-6.168.5.tar.gz", hash = "sha256:76b9226962fe11d40858253a967eda95bb65811365286317e0118f4ec8f808c7", size = 511115, upload-time = "2026-10-05T23:26:35.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/0c/7f04c8d277dfc828ba584b7d9d10dbac5e91fce673fa5328f7bd5bf64609/hypothesis-6.168.5-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ca43a751410a9c6685f029fd5126cc5507664cafaa76017922aa8ae2e17b6620", size = 791539, upload-time = "2026-10-05T23:24:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/11/5c/660906d83db74eb86feda715d0f2df14836205b14a183332116676733e6f/hypothesis-6.168.5-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b98707cbe9f430d100a945bbe17612fd3aa44eac1b0ac5299669fe3b8e4128", size = 787208, upload-time = "2026-10-05T23:25:14.028Z" }, + { url = "https://files.pythonhosted.org/packages/01/85/36e19492bc4ff354c2be9c8fa7c6ace0c65f9d2c7116656b741680c6ca55/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dde52a0b696c642e7f988a03026c7c29f90daf21e74507b6f865c3ccc9d536e", size = 1115200, upload-time = "2026-10-05T23:25:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/d4/82/3273fb0a3567c09b767bb8fe2824d65e16ae2abb92cf1f43762df723df94/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42f02e4541fe0c17a1320617effc0ab8a8aca2a9af15e3358d4150acf3bbdc00", size = 1145113, upload-time = "2026-10-05T23:25:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/74/59/5c5904555a0bbd4b2898d73ea90c6d03f5be0d8ff0756ac1d519ace6ae66/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf6dd7e537a12763c9afa017f7a6159e5cda608e98670621fa44596a1e8e9288", size = 1139564, upload-time = "2026-10-05T23:25:56.681Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/55654ff9575587a401e304f08ad1d43b7e6318f81c66bd866fdc5ab4665b/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df2c04cd30abf42c52580184216162a75b5508b214a472b86670f6dd50659a3b", size = 1192142, upload-time = "2026-10-05T23:26:06.565Z" }, + { url = "https://files.pythonhosted.org/packages/48/91/4cc9d6e8a950473e07e3ebf00cbb8ee0d76b14d193f94c3de20f1c09e2b1/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278662eb21aaec9eaae71ea4dabd4fe390c2af11ec58a6a0606687cf6d7689b0", size = 1157262, upload-time = "2026-10-05T23:24:59.229Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/4b8aa3be788ea81b9a7bc6b673ed89edd72fd0645c6aa691d4c159ff971a/hypothesis-6.168.5-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:6bcedc4ab8ab92dd0f3af0cfe24dce184d225751d7bc870a9cddb9a557de847f", size = 1113779, upload-time = "2026-10-05T23:24:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/f9/98/2eb4c79d1851195e6a083568b065235680ab984e984bbd472f2a7d02ba33/hypothesis-6.168.5-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b58097cc3b98d8616f635ac73888fc9f859311875f2adc043f1544c40c3c466", size = 1152639, upload-time = "2026-10-05T23:25:43.635Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9c/68f7e99b43c6f37c077669a4d3bd88f48c042444ced9e7cff0eaf44bc70a/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f8a387d9ee7f804e830b31f2e2e339ab5731665e922cfda4f6f6fbdb05e191b4", size = 1290068, upload-time = "2026-10-05T23:25:28.45Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/d4f87164a0d028ab102cea345b601d9dafb3196358df5448caa88ac3c1e2/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:326f6383fdf2e37ac69773589a8238a3bf396ca8ac8efacb0fb9ed42dd08e426", size = 1418551, upload-time = "2026-10-05T23:24:51.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/6b518a25514f0e643f95610c77e279bfbf0e0b3bd423aac0187d6f039b9a/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d33fc74e43bbd7c3a8f6f7161a8b93b676924286e97e70e828c6e0dcee5c01f", size = 1372130, upload-time = "2026-10-05T23:25:32.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/c2/32538e14e63193ca894ba584696805d1eb45cfc27e15fccd47acfb87531c/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:1994923cf5e5220ae6bf19645302504b27c0289d83e5d8690df71dcae63d8416", size = 1268950, upload-time = "2026-10-05T23:25:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/e50e7e98af9489aa05203c2ab38c95d891dd8d1ed08fad972dcdb6955332/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:501038fd24d3bc95239cfd093a23cf1151f29dd82382a3554dac5dfdab9729ae", size = 1283907, upload-time = "2026-10-05T23:24:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/41c460a7d2148a04b212b2d594d39992fb52e0b844e13bf6784573fc8dea/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2292ddc24fe6d04b7d30fa6a7e2c9e280ad5078fe671d0bf4aa6df6e143b5ac", size = 1323762, upload-time = "2026-10-05T23:24:18.984Z" }, + { url = "https://files.pythonhosted.org/packages/68/4f/37a7fc1fe445e3589e0f56ff4573c28de1d6e6a03009cba2f99f04e46ffa/hypothesis-6.168.5-cp310-abi3-win32.whl", hash = "sha256:925d67c69b719d416334aa961c0cdfc4a58a471af1ebd2d7101bd515a70f4e5f", size = 677903, upload-time = "2026-10-05T23:25:07.129Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/7b25ca7845a60522ebc5f8054f6bba68d47126fb5d940c784fc528a4be4a/hypothesis-6.168.5-cp310-abi3-win_amd64.whl", hash = "sha256:2311590eccba452de863dfe3466daa86a05c25f072ab31ed8bb4d3313ee68439", size = 684616, upload-time = "2026-10-05T23:25:04.028Z" }, + { url = "https://files.pythonhosted.org/packages/c3/00/40e7c36b46c8788eddc7a322ad324e6db53c8ab9a8b9a95d6535ee7bdaaf/hypothesis-6.168.5-cp310-abi3-win_arm64.whl", hash = "sha256:222a6d23a2a824b0f9f73761c2fb9cd2aca96cf3e5b441617625bce4f7eb4fd4", size = 683061, upload-time = "2026-10-05T23:25:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b3/1f7f72cd28d02a5ca99c432fbffe4b750a375df2284af9d916943dd3aa4f/hypothesis-6.168.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:714337b25ca9137bc359c570b868269462307e120999412ca1946f997f4b9db5", size = 793075, upload-time = "2026-10-05T23:25:15.905Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ba/5b0874828695c4d49e3858d0967254f783e563cd0e211a6db27d11d48a1f/hypothesis-6.168.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f1c3617155fcf5b5259a1f2e4c775d3eec7bfa80b162b2f6f145b08f871ab08", size = 784693, upload-time = "2026-10-05T23:24:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5f/ca777becba5251b0d778bb9d83d15524c559a07e4b5d4e6211473855bae2/hypothesis-6.168.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebee70b7a026210bb47c86c89e5bfb42effd5bd630080e76bc084f29c01c7f7a", size = 1113693, upload-time = "2026-10-05T23:24:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/5a74bf329e405db3edc5639a2595eccf33ad6f5aaa191019e9f824d630f4/hypothesis-6.168.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cfb06b31cca005345b8ad63f88986d21fd359a7dc3dba2965dd3515b720e5c9", size = 1156530, upload-time = "2026-10-05T23:24:47.153Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/e79cf67f03f212a1394abac21053bd6887aa70f557be1da3f9c9c73e58ae/hypothesis-6.168.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a4c244d7ab64963fb575f0ec2d813630e1d14cefc39e7c460d5d778e5af4118", size = 1288713, upload-time = "2026-10-05T23:25:22.763Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/df452159ac8d7b278071a3e81fafc69da833ec4302b8c85f5b6e530aea21/hypothesis-6.168.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e59d519f6fb38b3fa4fcde046767b03a24740fe827d261ee7ff9a721c06169b", size = 1323134, upload-time = "2026-10-05T23:26:02.485Z" }, + { url = "https://files.pythonhosted.org/packages/af/fb/f07d8d09fb57eb14555cad64dfbe29bfdcecff3806f1e01268258088e741/hypothesis-6.168.5-cp313-cp313-win_amd64.whl", hash = "sha256:c103f655644afa4ef6bf7efbf86e44b78ee475fd0691da2db86e2cfe72c07234", size = 682019, upload-time = "2026-10-05T23:24:22.888Z" }, + { url = "https://files.pythonhosted.org/packages/de/e9/7c3c2262b8cfa825c4c1764d62aa15e628bae257ccfd2ee4f3ffa4f81eaa/hypothesis-6.168.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c4dc037d8001bc6eccb8636f4a38d16ea6b250d6bf0a89075aaa5e5069f751cc", size = 793253, upload-time = "2026-10-05T23:26:14.331Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/7909ed7d29302491e9b7bc0e7ac3287c20736c05a0cc35bae65024aeec3b/hypothesis-6.168.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c90743321f29b65491d146adfc2ece85869bacb71ce18b47674795e896c81ee3", size = 784837, upload-time = "2026-10-05T23:25:24.728Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/57742c459349052e6a3e0c011855840f8cbbbadca91079d5b591f08b25ae/hypothesis-6.168.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09debb7f7f0f229da5f7e2ad515a5be7a8dc607ec204074775f8ab6731a447f0", size = 1114268, upload-time = "2026-10-05T23:24:27.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/07bd2449f91f9426f705fb689429bab6e26d1365f8ac4ef7d7c1cec9055e/hypothesis-6.168.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d227f8ac497eca0bde4e8562d32dd4e82fc9566526020bbd567f76b833b923b0", size = 1156838, upload-time = "2026-10-05T23:24:35.211Z" }, + { url = "https://files.pythonhosted.org/packages/fe/75/7f3dda517e5134f73e2ae41821bf40b3fd3ac6551a9a43ea9287471738a1/hypothesis-6.168.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc6ebd35601c72c842e5899c3f760f9ed26c69e786ee40a9a64fb5a4a3058315", size = 1288864, upload-time = "2026-10-05T23:24:42.645Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cd/4b1364140642cf3f1431ca59b5841fc322872dfa7197b2facb97692da234/hypothesis-6.168.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:503e103ad49e702bad200157d82778eebbc14d3045e9700a8e8fe5db40912953", size = 1323300, upload-time = "2026-10-05T23:24:14.826Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/47d7cffcaf15318a4308516b6b3d2fd0db599f18eacc0f2dc553be2206a7/hypothesis-6.168.5-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:bc5cc310f9f86ec62f0d0dd7eea5a4788f18ec793b70ee2c7163b916768e1057", size = 624239, upload-time = "2026-10-05T23:24:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/97/6e/2ca0f68150be175b7cfa7bfb6692260638d86aeb9313478ba82e198186e6/hypothesis-6.168.5-cp314-cp314-win_amd64.whl", hash = "sha256:71ce0599e806ce3a68f9f118edf450bf091e11b134f6bcc5f8dd706b42c91ebc", size = 681884, upload-time = "2026-10-05T23:25:00.757Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/4fc2b5970df0c27668ec08abc505f1d01314a69953f89dc0edc6528ff5a0/hypothesis-6.168.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f66b02c9e95e916a2c58f725a92377ec988146ed7b5aeccd5e78ceecac1eae6f", size = 791636, upload-time = "2026-10-05T23:26:22.365Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/03cdf5f052dcb441e045be1fd0aa531e85cde1a1cbabab60968625c570a3/hypothesis-6.168.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bab27926e1d1575fb43b70d4aeece05b74a5e477af0509b56cb6fd778070dd93", size = 783280, upload-time = "2026-10-05T23:26:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/615557af244e2f3ce4763029c03a62ed82dcbbd646b72a8c479ef0408b33/hypothesis-6.168.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edeb42c3009b5652dc1c44907ec91bfe9284100ad5e57993dfebabb76f2961a1", size = 1112149, upload-time = "2026-10-05T23:24:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/67/a6707fcd51dc5f2531bf88ac072e99f31ab9d8020488b01349a6d2981081/hypothesis-6.168.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8977456328147c521a16a089325017b2c728fddc23351693a4fd924cc7fc7001", size = 1155637, upload-time = "2026-10-05T23:24:24.047Z" }, + { url = "https://files.pythonhosted.org/packages/85/d4/ac2e852d2f163afd398854662bcbb0b849a767abbf2f95a75de6f685f821/hypothesis-6.168.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:36ecf7ac351f9c0b5489ba800884b607da754e88ef40713fbfcc170d2151e6eb", size = 1287040, upload-time = "2026-10-05T23:25:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/f77b1602704bda6ff3d9d0817120bd7fb94fd792bd25508b36ce4b8bd2a2/hypothesis-6.168.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0333aa5129ba3019a83fb81a7f0fc238180e415a9edddd9a15101f8deaaa517e", size = 1322166, upload-time = "2026-10-05T23:25:45.39Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2e/94138a73e0906b31cb5968d20be58688f582a09e5958f2c75d45a7049545/hypothesis-6.168.5-cp314-cp314t-win_amd64.whl", hash = "sha256:2fcb87341d76ae0183e8219c9a14d55957c50d14973879db5fea3e81da45ba1a", size = 681599, upload-time = "2026-10-05T23:24:37.827Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/92cb8092b680be2b6ec5ffe83b9f1a98dbf414117566f9e3ba4e8b569214/hypothesis-6.168.5-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:453ab7d0a1fadbaa54ae8722d22463cc2046fa8ef25b9b88715d28279bf79fc1", size = 790950, upload-time = "2026-10-05T23:24:45.852Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/78aea12694e3d1177e2980d44798b6d93e191faf59155b18bf5ae315f6a2/hypothesis-6.168.5-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:bbdbc43d1f9dad595b249b7bbe8ee5102bc94a4fcb0a79ff76d20e41fcfe342a", size = 782939, upload-time = "2026-10-05T23:25:39.961Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/5ef9947b2d149f773428e555bdf66688405aa5167510bdbe97c8ec5c6090/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bc36194d7b6083591060836c7872711a6820217b325bf432dd7e10b3d4af5cb", size = 1111380, upload-time = "2026-10-05T23:24:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/e1/65/7e668e203fb2659c6214dc0c24cc09b7dea8a02c7c8d0ad338f644a054c4/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22425e2b1543a43c157a81472c713ba8f291cbaf054c70ffe128e2cacc294f65", size = 1142644, upload-time = "2026-10-05T23:26:30.697Z" }, + { url = "https://files.pythonhosted.org/packages/c0/77/b112978676e795658d58c4294bf90cdbb8cb56cb8292c8c4874650468cf9/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eea0bc513d0e38d1d5ddfb581132928871cd02dc54dfe4511a5396727c48e9d0", size = 1135966, upload-time = "2026-10-05T23:24:49.806Z" }, + { url = "https://files.pythonhosted.org/packages/e6/27/cd3bf01e8246c4318ec3df15f5eeeee3214f444df0129a6c7f9a62859ee8/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb142bc70bbf6645e15c7ca72de3f7c8dae198aa2743a609f4f3e3bb4f9c3a52", size = 1189646, upload-time = "2026-10-05T23:26:04.471Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a6/4d3e882f31c289e432dfec34dbb9029296038a8c69e8b28cebb0a5fb7ea8/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a27b758707bd37f5a1759cca6eef83fe1a212c38dc4ca0a203434004c5647d15", size = 1154770, upload-time = "2026-10-05T23:25:10.814Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/96f5455e1b3d0409cbbb1434c98e792bcceefd614a4b11e600072520b487/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:77a111cb50c330fa7098f65852fa17a01ecd781a85be3cf5e5871bdeeeb0ecbc", size = 1109925, upload-time = "2026-10-05T23:25:26.369Z" }, + { url = "https://files.pythonhosted.org/packages/3d/64/0758985d9d36f0c5ec981a1457ea1c8173f62d46a94531417aec117df4d7/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdd0afc13e86ec76cae3d3659569c1f601f4e9ca52b5cf91c1685979eae64d7b", size = 1148476, upload-time = "2026-10-05T23:24:54.552Z" }, + { url = "https://files.pythonhosted.org/packages/43/5c/a9b8953e1d8aefcd3c22cf8d10dd8acf93e602b903278e2e51cf8544ccea/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:5fefb02035864c3d322e3b0969b296250923fdcfb574ea1ad4374f1a6333f663", size = 1285856, upload-time = "2026-10-05T23:25:38.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/66098444dc832523ddc4f2e05723662834e5f99bba3c759615d059f6420e/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:9db8aa1f5529e1b577ec18b775c2fb4225821712e946f7762b90c966604faf83", size = 1415500, upload-time = "2026-10-05T23:24:21.682Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/e971b6fe20ef8d7c2019cbf24b4f6149468efc88c42a744f5bc99e6ca0ed/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:59e07d2f62b5ff573b0059959ae9cef9edfb0f5393fdb35ea81fce1ee77b27ac", size = 1368623, upload-time = "2026-10-05T23:26:11.292Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ac/b279dfbd2c06cdb3030ba7eea042cb2cf0171d0013563d103d5207dde63b/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:8a03ca128bea29d6826fc545f1f6289fb1ea2e83a5bb811321761b2d515ca575", size = 1264457, upload-time = "2026-10-05T23:25:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/55/57/16ac9f8ddfada1cd278bd2185234d0d36ebd304926b69ad0497c210c6fed/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:5c03f2d3f84f626f3fd07f54573ab40455e1a1996e98a4f4971caf8b7e796afe", size = 1281744, upload-time = "2026-10-05T23:24:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d1/99a44430b82998fdef0ffd7d353f64ee5f078c2805ff70f8677ee102cb6c/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2bdf8ce9b72a620cd5ec4dd6b1c1837ff6971489a863851d11d9b0f58dd4062a", size = 1321504, upload-time = "2026-10-05T23:26:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6a/58ef2564d1985a5c1a1dc57906b8363a767094abca180e80a0aca4cb635f/hypothesis-6.168.5-cp315-abi3.abi3t-win32.whl", hash = "sha256:5c3abbef7b17571fd713b0922407d9cd8cbc652254c0f462875f15199fcb29f7", size = 674959, upload-time = "2026-10-05T23:24:36.482Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/153414f55eb0c85bd9d891bd7811d746978c7ad3de81ea79eeb4e62e088b/hypothesis-6.168.5-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:38172199abab94a04bc017613e055faa796d7175fbc6221aac504d406c960b60", size = 681329, upload-time = "2026-10-05T23:26:08.897Z" }, + { url = "https://files.pythonhosted.org/packages/6d/63/117c82f08ab3ba1dcfbf6562ac43b8deb8efa8106646494fadd15122cc1b/hypothesis-6.168.5-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:0600ddc24c32dab5ca8e780630ab6e2561df6d7f594f781d0608b38e04c4da91", size = 679632, upload-time = "2026-10-05T23:24:33.753Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1364,7 +1433,7 @@ wheels = [ [[package]] name = "policyengine-uk-data" -version = "1.56.16" +version = "1.57.4" source = { editable = "." } dependencies = [ { name = "google-auth" }, @@ -1392,6 +1461,7 @@ dependencies = [ dev = [ { name = "build" }, { name = "furo" }, + { name = "hypothesis" }, { name = "itables" }, { name = "l0-python" }, { name = "pytest" }, @@ -1410,6 +1480,7 @@ requires-dist = [ { name = "google-auth" }, { name = "google-cloud-storage" }, { name = "huggingface-hub" }, + { name = "hypothesis", marker = "extra == 'dev'" }, { name = "itables", marker = "extra == 'dev'" }, { name = "l0-python", marker = "extra == 'dev'", specifier = ">=0.4.0" }, { name = "microcalibrate", specifier = ">=0.18.0" },