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 00000000..496b9e72 --- /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 9e6964ed..f4c6e172 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 68aa7e37..6abb9ef2 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 36c5fd4d..515deddd 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 00000000..e0c17bf6 --- /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 dd49d9e9..622ff402 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 beff7f1a..58de5b1d 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 a3e9f44d..94928c83 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" },