Skip to content

person_household_id and person_benunit_id are stored as float32, which rounds large identifiers #2267

Open

Description

@MaxGhenis

What happens

person_household_id and person_benunit_id are declared with value_type = float (policyengine_uk/variables/household/demographic/), so the simulation stores them as 32-bit floats. Those hold integers exactly only up to 2**24 (16,777,216). Larger identifiers read back rounded, and different households become indistinguishable.

Reproduction

On main (2.125.2):

import numpy as np
import pandas as pd
from policyengine_uk import Microsimulation
from policyengine_uk.data import UKSingleYearDataset

ids = np.array([900_000_001, 900_000_002, 900_000_003])
data = UKSingleYearDataset(
    person=pd.DataFrame(
        {"person_id": ids, "person_benunit_id": ids, "person_household_id": ids, "age": [30, 40, 50]}
    ),
    benunit=pd.DataFrame({"benunit_id": ids}),
    household=pd.DataFrame(
        {
            "household_id": ids,
            "household_weight": 1.0,
            "region": "LONDON",
            "rent": 0.0,
            "tenure_type": "OWNED_OUTRIGHT",
            "council_tax": 0.0,
        }
    ),
    fiscal_year=2025,
)
sim = Microsimulation(dataset=data)
print(sim.calculate("household_id", 2025).tolist())         # [900000001, 900000002, 900000003]
print(sim.calculate("person_household_id", 2025).tolist())  # [900000000.0, 900000000.0, 900000000.0]

In a full-size UK dataset I checked, household identifiers reach about 900 million: its 535,080 distinct household identifiers are 76,551 distinct values as float32, and its 618,980 benefit unit identifiers are 544,894.

What is and is not affected

  • Not affected: the entity structure. build_from_ids takes memberships from the dataset tables, and the populations (ids, members_entity_id) are exact. I found no formula that reads either variable.
  • Affected: anything that reads the stored values. That includes calculate("person_household_id"), exports such as to_input_dataframe(), UKSingleYearDataset.from_simulation (which writes the calculated values into the person table), and any rebuild from those values. policyengine-core's subsample rebuilds that way, which is one reason Give UK simulations a working subsample #2265 gives the UK its own subsample that reads the populations instead.

Options

  1. Declare both variables int, like household_id, benunit_id and person_id. They are then stored as 32-bit integers, which are exact up to 2**31 − 1 (about 2.1 billion). Check how datasets that store these columns as floats load, and what the metadata API reports for the type.
  2. Keep the type and document that the stored values are approximate, with a helper that returns exact memberships from the populations.

Found while fixing #2254.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions