diff --git a/changelog.d/remove-medicaid-cost-allocator.changed.md b/changelog.d/remove-medicaid-cost-allocator.changed.md new file mode 100644 index 000000000..41c2338f5 --- /dev/null +++ b/changelog.d/remove-medicaid-cost-allocator.changed.md @@ -0,0 +1 @@ +Removed the duplicate Medicaid conditional-cost allocator from US data so PolicyEngine US owns Medicaid cost logic. diff --git a/docs/engineering/pipeline-map.md b/docs/engineering/pipeline-map.md index 8fe63ac63..11be1a508 100644 --- a/docs/engineering/pipeline-map.md +++ b/docs/engineering/pipeline-map.md @@ -1188,22 +1188,6 @@ class USGeographyPostProcessorResult Payload after US geography fields are applied. -### `policyengine_us_data.build_outputs.us_augmentations.USMedicaidCostPostProcessor` - -```python -class USMedicaidCostPostProcessor -``` - -Preserve source Medicaid conditional costs after local H5 transforms. - -### `policyengine_us_data.build_outputs.us_augmentations.USMedicaidCostPostProcessorResult` - -```python -class USMedicaidCostPostProcessorResult -``` - -Payload after conditional Medicaid cost fields are applied. - ### `policyengine_us_data.build_outputs.us_augmentations.USTakeupPostProcessor` ```python diff --git a/docs/generated/pipeline_api.json b/docs/generated/pipeline_api.json index aca853377..aabd74418 100644 --- a/docs/generated/pipeline_api.json +++ b/docs/generated/pipeline_api.json @@ -61,7 +61,7 @@ "docstring": "\"Add auto loan balance, interest and net_worth variable.", "id": "add_auto_loan", "kind": "function", - "line": 3063, + "line": 3058, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_auto_loan_interest_and_net_worth" @@ -88,7 +88,7 @@ "docstring": "Populate household-level geography variables used by PolicyEngine US.\n\nArgs:\n cps: Output CPS H5 group receiving derived household variables.\n household: Raw CPS household table.", "id": "add_household_variables", "kind": "function", - "line": 1656, + "line": 1651, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_household_variables" @@ -115,7 +115,7 @@ "docstring": "Add basic ID and weight variables.\n\nArgs:\n cps (h5py.File): The CPS dataset file.\n person (DataFrame): The person table of the ASEC.\n tax_unit (DataFrame): The tax unit table created from the person table\n of the ASEC.\n family (DataFrame): The family table of the ASEC.\n spm_unit (DataFrame): The SPM unit table created from the person table\n of the ASEC.\n household (DataFrame): The household table of the ASEC.", "id": "add_id_variables", "kind": "function", - "line": 1049, + "line": 1044, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_id_variables" @@ -142,7 +142,7 @@ "docstring": "Impute ORG-derived labor-market inputs and derive overtime premium.", "id": "add_org_inputs", "kind": "function", - "line": 2963, + "line": 2958, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_org_labor_market_inputs" @@ -169,7 +169,7 @@ "docstring": "Add income variables.\n\nArgs:\n cps (h5py.File): The CPS dataset file.\n person (DataFrame): The CPS person table.\n year (int): The CPS year", "id": "add_personal_income_variables", "kind": "function", - "line": 1348, + "line": 1343, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_personal_income_variables" @@ -196,7 +196,7 @@ "docstring": "Add personal demographic variables.\n\nArgs:\n cps (h5py.File): The CPS dataset file.\n person (DataFrame): The CPS person table.", "id": "add_personal_variables", "kind": "function", - "line": 1111, + "line": 1106, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_personal_variables" @@ -223,7 +223,7 @@ "docstring": "", "id": "add_previous_year_income", "kind": "function", - "line": 1698, + "line": 1693, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_previous_year_income" @@ -250,7 +250,7 @@ "docstring": "", "id": "add_rent", "kind": "function", - "line": 422, + "line": 417, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_rent" @@ -277,7 +277,7 @@ "docstring": "", "id": "add_spm_variables", "kind": "function", - "line": 1617, + "line": 1612, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_spm_variables" @@ -304,7 +304,7 @@ "docstring": "Assign SSN card type using PRCITSHP, employment status, and ASEC-UA conditions.\nCodes:\n- 0: \"NONE\" - Likely undocumented immigrants\n- 1: \"CITIZEN\" - US citizens (born or naturalized)\n- 2: \"NON_CITIZEN_VALID_EAD\" - Non-citizens with work/study authorization\n- 3: \"OTHER_NON_CITIZEN\" - Non-citizens with indicators of legal status", "id": "add_ssn_card_type", "kind": "function", - "line": 1804, + "line": 1799, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_ssn_card_type" @@ -331,7 +331,7 @@ "docstring": "", "id": "add_takeup", "kind": "function", - "line": 570, + "line": 565, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_takeup" @@ -358,7 +358,7 @@ "docstring": "", "id": "add_tips", "kind": "function", - "line": 2703, + "line": 2698, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.add_tips" @@ -815,7 +815,7 @@ "docstring": "Replace clone-half person-level feature variables with donor matches.", "id": "clone_features", "kind": "function", - "line": 603, + "line": 600, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.extended_cps._splice_clone_feature_predictions" @@ -878,7 +878,7 @@ "docstring": "Assert that final exported variables are leaf inputs.", "id": "computed_export_contract", "kind": "function", - "line": 1782, + "line": 1775, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.extended_cps.ExtendedCPS._assert_no_computed_variables_exported" @@ -972,7 +972,7 @@ "docstring": "Second-stage QRF: train on CPS, predict for PUF clones.\n\nFor the PUF clone half of the extended CPS we need plausible values\nof CPS-only variables (retirement distributions, transfers, hours,\nSPM components, etc.) that are consistent with the clone's\nPUF-imputed income -- not just naively copied from the CPS donor.\n\nWe train a QRF on CPS person-level data where:\n * predictors = demographics + key income variables\n * outputs = CPS-only variables listed in\n ``CPS_ONLY_IMPUTED_VARIABLES``\n\nFor PUF clone prediction we use the PUF-imputed income values\nfrom the second half of ``data`` (the clone half, which already\nhas PUF-imputed income from stage 1).\n\nUses ``fit_predict()`` with ``max_train_samples`` instead of\nmanual sampling + separate fit/predict.\n\nArgs:\n data: Extended dataset dict after ``puf_clone_dataset()`` --\n already doubled, with PUF-imputed income in the second half.\n time_period: Tax year.\n dataset_path: Path to the CPS h5 file for Microsimulation.\n\nReturns:\n DataFrame with one column per CPS-only variable, containing\n predicted values for the PUF clone half (person-level).", "id": "cps_only", "kind": "function", - "line": 642, + "line": 639, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.extended_cps._impute_cps_only_variables" @@ -1064,7 +1064,7 @@ "docstring": "Subsample the loaded CPS dataset and preserve downsampled arrays.\n\nArgs:\n frac: Fraction of records to retain.", "id": "downsample", "kind": "function", - "line": 389, + "line": 384, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.cps.CPS.downsample" @@ -1325,7 +1325,7 @@ "docstring": "Check formula-reconstructed housing assistance before export.\n\nThe final H5 must not export formula outputs such as ``housing_assistance``.\nThis guard verifies that the remaining leaf inputs still make those\nformulas produce nonzero values before the export contract strips or\nrejects computed variables.", "id": "housing_assistance_microsim_validation", "kind": "function", - "line": 1552, + "line": 1545, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.extended_cps.ExtendedCPS._validate_housing_assistance_microsimulation" @@ -2259,7 +2259,7 @@ "docstring": "Apply US entity IDs and calibrated household weights.", "id": "local_h5_us_entity_postprocessor", "kind": "class", - "line": 181, + "line": 151, "metadata": { "api_refs": [ "policyengine_us_data.build_outputs.us_augmentations.USEntityPostProcessor" @@ -2286,7 +2286,7 @@ "docstring": "Payload after US entity ID and household-weight fields are applied.", "id": "local_h5_us_entity_postprocessor_result", "kind": "class", - "line": 75, + "line": 71, "metadata": { "api_refs": [ "policyengine_us_data.build_outputs.us_augmentations.USEntityPostProcessorResult" @@ -2313,7 +2313,7 @@ "docstring": "Apply block-derived US geography overrides.", "id": "local_h5_us_geography_postprocessor", "kind": "class", - "line": 234, + "line": 204, "metadata": { "api_refs": [ "policyengine_us_data.build_outputs.us_augmentations.USGeographyPostProcessor" @@ -2340,7 +2340,7 @@ "docstring": "Payload after US geography fields are applied.", "id": "local_h5_us_geography_postprocessor_result", "kind": "class", - "line": 101, + "line": 97, "metadata": { "api_refs": [ "policyengine_us_data.build_outputs.us_augmentations.USGeographyPostProcessorResult" @@ -2363,65 +2363,11 @@ "signature": "class USGeographyPostProcessorResult", "source_file": "policyengine_us_data/build_outputs/us_augmentations.py" }, - "local_h5_us_medicaid_cost_postprocessor": { - "docstring": "Preserve source Medicaid conditional costs after local H5 transforms.", - "id": "local_h5_us_medicaid_cost_postprocessor", - "kind": "class", - "line": 618, - "metadata": { - "api_refs": [ - "policyengine_us_data.build_outputs.us_augmentations.USMedicaidCostPostProcessor" - ], - "description": "Preserve Medicaid cost-if-enrolled inputs in local H5 payloads.", - "id": "local_h5_us_medicaid_cost_postprocessor", - "label": "USMedicaidCostPostProcessor", - "node_type": "library", - "pathways": [ - "local_h5" - ], - "source_file": "policyengine_us_data/build_outputs/us_augmentations.py", - "stability": "moving", - "status": "current", - "validation_commands": [ - "uv run pytest tests/unit/build_outputs/test_us_augmentations.py" - ] - }, - "object_path": "policyengine_us_data.build_outputs.us_augmentations.USMedicaidCostPostProcessor", - "signature": "class USMedicaidCostPostProcessor", - "source_file": "policyengine_us_data/build_outputs/us_augmentations.py" - }, - "local_h5_us_medicaid_cost_postprocessor_result": { - "docstring": "Payload after conditional Medicaid cost fields are applied.", - "id": "local_h5_us_medicaid_cost_postprocessor_result", - "kind": "class", - "line": 155, - "metadata": { - "api_refs": [ - "policyengine_us_data.build_outputs.us_augmentations.USMedicaidCostPostProcessorResult" - ], - "description": "US Medicaid conditional-cost local H5 payload data.", - "id": "local_h5_us_medicaid_cost_postprocessor_result", - "label": "USMedicaidCostPostProcessorResult", - "node_type": "library", - "pathways": [ - "local_h5" - ], - "source_file": "policyengine_us_data/build_outputs/us_augmentations.py", - "stability": "moving", - "status": "current", - "validation_commands": [ - "uv run pytest tests/unit/build_outputs/test_us_augmentations.py" - ] - }, - "object_path": "policyengine_us_data.build_outputs.us_augmentations.USMedicaidCostPostProcessorResult", - "signature": "class USMedicaidCostPostProcessorResult", - "source_file": "policyengine_us_data/build_outputs/us_augmentations.py" - }, "local_h5_us_takeup_postprocessor": { "docstring": "Apply US take-up draws after entity and geography postprocessing.", "id": "local_h5_us_takeup_postprocessor", "kind": "class", - "line": 339, + "line": 309, "metadata": { "api_refs": [ "policyengine_us_data.build_outputs.us_augmentations.USTakeupPostProcessor" @@ -2448,7 +2394,7 @@ "docstring": "Payload after US take-up fields are applied.", "id": "local_h5_us_takeup_postprocessor_result", "kind": "class", - "line": 128, + "line": 124, "metadata": { "api_refs": [ "policyengine_us_data.build_outputs.us_augmentations.USTakeupPostProcessorResult" @@ -3270,7 +3216,7 @@ "docstring": "Replace PUF clone half of CPS-only variables with QRF predictions.\n\nAfter ``puf_clone_dataset()`` the CPS-only variables in the second\nhalf are naive copies of the CPS donor values. This function\nreplaces them with the second-stage QRF predictions that are\nconsistent with the clone's PUF-imputed income.\n\nArgs:\n data: Extended dataset dict (already doubled).\n predictions: DataFrame from ``_impute_cps_only_variables()``.\n time_period: Tax year.\n dataset_path: Path to CPS h5 file for entity mapping.\n\nReturns:\n Modified data dict with CPS-only variables spliced in.", "id": "qrf_pass2", "kind": "function", - "line": 1017, + "line": 1014, "metadata": { "api_refs": [ "policyengine_us_data.datasets.cps.extended_cps._splice_cps_only_predictions" diff --git a/docs/generated/pipeline_map.json b/docs/generated/pipeline_map.json index 726dbf160..3dd12be3c 100644 --- a/docs/generated/pipeline_map.json +++ b/docs/generated/pipeline_map.json @@ -967,42 +967,6 @@ "uv run pytest tests/unit/build_outputs/test_us_augmentations.py" ] }, - { - "api_refs": [ - "policyengine_us_data.build_outputs.us_augmentations.USMedicaidCostPostProcessor" - ], - "description": "Preserve Medicaid cost-if-enrolled inputs in local H5 payloads.", - "id": "local_h5_us_medicaid_cost_postprocessor", - "label": "USMedicaidCostPostProcessor", - "node_type": "library", - "pathways": [ - "local_h5" - ], - "source_file": "policyengine_us_data/build_outputs/us_augmentations.py", - "stability": "moving", - "status": "current", - "validation_commands": [ - "uv run pytest tests/unit/build_outputs/test_us_augmentations.py" - ] - }, - { - "api_refs": [ - "policyengine_us_data.build_outputs.us_augmentations.USMedicaidCostPostProcessorResult" - ], - "description": "US Medicaid conditional-cost local H5 payload data.", - "id": "local_h5_us_medicaid_cost_postprocessor_result", - "label": "USMedicaidCostPostProcessorResult", - "node_type": "library", - "pathways": [ - "local_h5" - ], - "source_file": "policyengine_us_data/build_outputs/us_augmentations.py", - "stability": "moving", - "status": "current", - "validation_commands": [ - "uv run pytest tests/unit/build_outputs/test_us_augmentations.py" - ] - }, { "api_refs": [ "policyengine_us_data.build_outputs.us_augmentations.USTakeupPostProcessor" @@ -2082,9 +2046,9 @@ } ], "metadata": { - "api_node_count": 100, + "api_node_count": 98, "canonical_stage_count": 5, - "decorated_object_count": 160, + "decorated_object_count": 158, "mapped_decorated_node_count": 60, "stage_count": 17, "substage_count": 17 diff --git a/policyengine_us_data/build_outputs/us_augmentations.py b/policyengine_us_data/build_outputs/us_augmentations.py index ef404c13d..b418c0d19 100644 --- a/policyengine_us_data/build_outputs/us_augmentations.py +++ b/policyengine_us_data/build_outputs/us_augmentations.py @@ -31,14 +31,11 @@ "TAKEUP_VARIABLE_ENTITIES", "US_ENTITY_POSTPROCESSOR_KEY", "US_GEOGRAPHY_POSTPROCESSOR_KEY", - "US_MEDICAID_COST_POSTPROCESSOR_KEY", "US_TAKEUP_POSTPROCESSOR_KEY", "USEntityPostProcessor", "USEntityPostProcessorResult", "USGeographyPostProcessor", "USGeographyPostProcessorResult", - "USMedicaidCostPostProcessor", - "USMedicaidCostPostProcessorResult", "USTakeupPostProcessor", "USTakeupPostProcessorResult", "default_us_postprocessors", @@ -55,7 +52,6 @@ US_ENTITY_POSTPROCESSOR_KEY = "us_entity" US_GEOGRAPHY_POSTPROCESSOR_KEY = "us_geography" US_TAKEUP_POSTPROCESSOR_KEY = "us_takeup" -US_MEDICAID_COST_POSTPROCESSOR_KEY = "us_medicaid_cost" @pipeline_node( @@ -138,32 +134,6 @@ def data(self) -> PayloadData: return self.payload.data -@pipeline_node( - id="local_h5_us_medicaid_cost_postprocessor_result", - label="USMedicaidCostPostProcessorResult", - node_type="library", - description="US Medicaid conditional-cost local H5 payload data.", - source_file="policyengine_us_data/build_outputs/us_augmentations.py", - status="current", - stability="moving", - pathways=["local_h5"], - validation_commands=[ - "uv run pytest tests/unit/build_outputs/test_us_augmentations.py" - ], -) -@dataclass(frozen=True) -class USMedicaidCostPostProcessorResult: - """Payload after conditional Medicaid cost fields are applied.""" - - payload: H5Payload - - @property - def data(self) -> PayloadData: - """Augmented payload data retained for transitional callers.""" - - return self.payload.data - - @pipeline_node( id="local_h5_us_entity_postprocessor", label="USEntityPostProcessor", @@ -601,70 +571,8 @@ def _build_eligibility_masks( } -@pipeline_node( - id="local_h5_us_medicaid_cost_postprocessor", - label="USMedicaidCostPostProcessor", - node_type="library", - description="Preserve Medicaid cost-if-enrolled inputs in local H5 payloads.", - source_file="policyengine_us_data/build_outputs/us_augmentations.py", - status="current", - stability="moving", - pathways=["local_h5"], - validation_commands=[ - "uv run pytest tests/unit/build_outputs/test_us_augmentations.py" - ], -) -@dataclass(frozen=True) -class USMedicaidCostPostProcessor: - """Preserve source Medicaid conditional costs after local H5 transforms.""" - - spec = PayloadPostProcessorSpec( - key=US_MEDICAID_COST_POSTPROCESSOR_KEY, - requires=( - US_ENTITY_POSTPROCESSOR_KEY, - US_GEOGRAPHY_POSTPROCESSOR_KEY, - US_TAKEUP_POSTPROCESSOR_KEY, - ), - ) - - def apply( - self, - *, - payload: H5Payload, - context: PayloadBuildContext, - ) -> USMedicaidCostPostProcessorResult: - """Return a payload with source conditional Medicaid costs preserved.""" - - output = _copy_payload(payload.data) - cost_periods = output.get("medicaid_cost_if_enrolled", {}) - if context.time_period not in cost_periods: - source_values = _source_person_period_values( - context=context, - variable="medicaid_cost_if_enrolled", - ) - if source_values is not None: - output["medicaid_cost_if_enrolled"] = { - context.time_period: source_values.astype(np.float32) - } - - variable_entities = dict(payload.variable_entities) - if "medicaid_cost_if_enrolled" in output: - variable_entities["medicaid_cost_if_enrolled"] = "person" - return USMedicaidCostPostProcessorResult( - payload=H5Payload( - data=output, - time_period=payload.time_period, - entity_lengths=payload.entity_lengths, - variable_entities=variable_entities, - ), - ) - - def default_us_postprocessors() -> tuple[ - USEntityPostProcessor - | USGeographyPostProcessor - | USTakeupPostProcessor - | USMedicaidCostPostProcessor, + USEntityPostProcessor | USGeographyPostProcessor | USTakeupPostProcessor, ..., ]: """Return production US postprocessors in their required order.""" @@ -673,7 +581,6 @@ def default_us_postprocessors() -> tuple[ USEntityPostProcessor(), USGeographyPostProcessor(), USTakeupPostProcessor(), - USMedicaidCostPostProcessor(), ) @@ -681,26 +588,6 @@ def _copy_payload(data: Mapping[str, Mapping[Any, np.ndarray]]) -> PayloadData: return {variable: dict(periods) for variable, periods in data.items()} -def _source_person_period_values( - *, - context: PayloadBuildContext, - variable: str, -) -> np.ndarray | None: - """Return a source person input mapped to the local payload, if available.""" - - if variable not in context.source.input_variables: - return None - provider = context.source.variable_provider - get_array = getattr(provider, "get_array", None) - if not callable(get_array): - return None - try: - values = np.asarray(get_array(variable, context.time_period)) - except (KeyError, ValueError): - return None - return values[context.reindexed.person_source_indices] - - def _required_period_array( data: Mapping[str, Mapping[Any, np.ndarray]], variable: str, diff --git a/policyengine_us_data/datasets/cps/cps.py b/policyengine_us_data/datasets/cps/cps.py index 033708bb1..7e2d6b76f 100644 --- a/policyengine_us_data/datasets/cps/cps.py +++ b/policyengine_us_data/datasets/cps/cps.py @@ -49,9 +49,6 @@ derive_treasury_tipped_occupation_code, derive_is_tipped_occupation, ) -from policyengine_us_data.datasets.cps.medicaid_cost import ( - add_medicaid_cost_if_enrolled_to_dataset, -) from policyengine_us_data.utils.takeup import ( _sum_person_values_to_tax_units, _voluntary_filing_age_bin, @@ -363,8 +360,6 @@ def generate(self): add_takeup(self) logging.info("Imputing Marketplace plan benchmark ratio") add_marketplace_plan_benchmark_ratio(self) - logging.info("Adding Medicaid cost if enrolled") - add_medicaid_cost_if_enrolled_to_dataset(self) logging.info("Deriving other health insurance premiums") derive_other_health_insurance_premiums(self) logging.info("Downsampling") diff --git a/policyengine_us_data/datasets/cps/extended_cps.py b/policyengine_us_data/datasets/cps/extended_cps.py index 43fc3d5db..c43add568 100644 --- a/policyengine_us_data/datasets/cps/extended_cps.py +++ b/policyengine_us_data/datasets/cps/extended_cps.py @@ -25,9 +25,6 @@ derive_flsa_overtime_premium, load_take_up_rate, ) -from policyengine_us_data.datasets.cps.medicaid_cost import ( - add_medicaid_cost_if_enrolled_to_time_period_data, -) from policyengine_us_data.datasets.cps.takeup import prioritize_reported_recipients from policyengine_us_data.datasets.org import ( ORG_IMPUTED_VARIABLES, @@ -1173,10 +1170,6 @@ def generate(self): new_data, self.time_period, ) - new_data = add_medicaid_cost_if_enrolled_to_time_period_data( - new_data, - self.time_period, - ) new_data = self._validate_housing_assistance_microsimulation( new_data, self.time_period, diff --git a/policyengine_us_data/datasets/cps/medicaid_cost.py b/policyengine_us_data/datasets/cps/medicaid_cost.py deleted file mode 100644 index f7b1cf5f0..000000000 --- a/policyengine_us_data/datasets/cps/medicaid_cost.py +++ /dev/null @@ -1,496 +0,0 @@ -from __future__ import annotations - -from functools import lru_cache -from typing import Any, Mapping - -import numpy as np - - -@lru_cache(maxsize=1) -def _policyengine_us_parameters(): - from policyengine_us import CountryTaxBenefitSystem - - return CountryTaxBenefitSystem().parameters - - -def add_medicaid_cost_if_enrolled_to_dataset(dataset) -> None: - """Add person-level conditional Medicaid cost to an array-format dataset.""" - - data = dataset.load_dataset() - - from policyengine_us import Microsimulation - - simulation = Microsimulation(dataset=dataset) - values = calculate_medicaid_cost_if_enrolled( - simulation=simulation, - time_period=dataset.time_period, - ) - data["medicaid_cost_if_enrolled"] = values.astype(np.float32) - dataset.save_dataset(data) - - -def add_medicaid_cost_if_enrolled_to_time_period_data( - data: dict, - time_period: int, - microsimulation_cls=None, - dataset_cls=None, -) -> dict: - """Add conditional Medicaid cost to time-period-array data.""" - - if microsimulation_cls is None: - from policyengine_us import Microsimulation - - microsimulation_cls = Microsimulation - - if dataset_cls is None: - from policyengine_core.data import Dataset - from policyengine_us_data.storage import STORAGE_FOLDER - - class InMemoryTimePeriodDataset(Dataset): - name = "medicaid_cost_if_enrolled_build" - label = "Medicaid cost build" - data_format = Dataset.TIME_PERIOD_ARRAYS - file_path = STORAGE_FOLDER / "medicaid_cost_if_enrolled_build.h5" - - def __init__(self, source_data: dict, source_time_period: int): - self._data = source_data - self.time_period = source_time_period - super().__init__() - - def load(self): - return self._data - - def load_dataset(self): - return self._data - - dataset_cls = InMemoryTimePeriodDataset - - simulation = microsimulation_cls(dataset=dataset_cls(data, time_period)) - values = calculate_medicaid_cost_if_enrolled( - simulation=simulation, - time_period=time_period, - ) - data["medicaid_cost_if_enrolled"] = {time_period: values.astype(np.float32)} - return data - - -def calculate_medicaid_cost_if_enrolled( - simulation: Any, time_period: int -) -> np.ndarray: - """Return SLCSP-indexed Medicaid cost if each person enrolled. - - The allocator uses only an SLCSP-derived age/location premium index for - within-state variation. It normalizes by state against current Medicaid - enrollees so baseline weighted Medicaid costs match state spending totals, - while non-enrollees still receive a conditional cost for reform analysis. - """ - - person_slcsp = calculate_person_slcsp_cost_index(simulation, time_period) - medicaid_enrolled = _calculate( - simulation, - "medicaid_enrolled", - time_period, - map_to="person", - ).astype(bool) - person_weight = _calculate( - simulation, - "person_weight", - time_period, - map_to="person", - ).astype(float) - state_codes = _as_str_array( - _calculate( - simulation, - "state_code_str", - time_period, - map_to="person", - ) - ) - spending = medicaid_spending_by_state(time_period) - return allocate_medicaid_cost_if_enrolled_by_slcsp( - person_slcsp=person_slcsp, - medicaid_enrolled=medicaid_enrolled, - person_weight=person_weight, - state_codes=state_codes, - state_spending=spending, - ) - - -def calculate_person_slcsp_cost_index( - simulation: Any, - time_period: int, -) -> np.ndarray: - """Return an SLCSP premium index for each person.""" - - age = _calculate(simulation, "age", time_period, map_to="person").astype(float) - is_tax_unit_dependent = _calculate( - simulation, - "is_tax_unit_dependent", - time_period, - map_to="person", - ).astype(bool) - person_tax_unit_id = _calculate( - simulation, - "person_tax_unit_id", - time_period, - map_to="person", - ) - tax_unit_id = _calculate( - simulation, - "tax_unit_id", - time_period, - map_to="tax_unit", - ) - state_codes = _as_str_array( - _calculate(simulation, "state_code_str", time_period, map_to="person") - ) - rating_area = _calculate( - simulation, - "slcsp_rating_area_default", - time_period, - map_to="person", - ).astype(int) - base_cost = slcsp_age_0_by_state_rating_area( - state_codes, - rating_area, - time_period, - ) - age_rated_index = base_cost * age_curve_multiplier(age, state_codes, time_period) - return np.clip( - family_tier_slcsp_person_share( - state_codes=state_codes, - base_cost=base_cost, - age=age, - is_tax_unit_dependent=is_tax_unit_dependent, - person_tax_unit_id=person_tax_unit_id, - tax_unit_id=tax_unit_id, - fallback=age_rated_index, - time_period=time_period, - ), - 0, - None, - ) - - -def slcsp_age_0_by_state_rating_area( - state_codes: np.ndarray, - rating_areas: np.ndarray, - time_period: int, -) -> np.ndarray: - """Return the age-0 SLCSP premium by state and county-level rating area.""" - - parameters = _policyengine_us_parameters()(f"{int(time_period)}-01-01") - costs = parameters.gov.aca.state_rating_area_cost - state_codes = _as_str_array(state_codes) - rating_areas = np.asarray(rating_areas, dtype=int) - output = np.zeros(len(state_codes), dtype=float) - for state_value in np.unique(state_codes): - state = str(state_value) - state_mask = state_codes == state - if state not in STATE_CODES: - continue - state_costs = costs[state] - for rating_area in np.unique(rating_areas[state_mask]): - safe_rating_area = str(int(rating_area)) - try: - cost = float(state_costs[safe_rating_area]) - except KeyError: - cost = float(state_costs["1"]) - output[state_mask & (rating_areas == rating_area)] = cost - return output - - -def age_curve_multiplier( - age: np.ndarray, - state_codes: np.ndarray, - time_period: int, -) -> np.ndarray: - """Return ACA SLCSP age-curve multipliers by person.""" - - parameters = _policyengine_us_parameters()(f"{int(time_period)}-01-01") - curves = parameters.gov.aca.age_curves - age = np.asarray(age, dtype=float) - state_codes = _as_str_array(state_codes) - result = np.asarray(curves.default.calc(age), dtype=float) - state_specific_curves = { - "AL": curves.al, - "DC": curves.dc, - "MA": curves.ma, - "MN": curves.mn, - "MS": curves.ms, - "NY": curves.ny, - "OR": curves["or"], - "UT": curves.ut, - "VT": curves.vt, - } - for state, curve in state_specific_curves.items(): - result = np.where( - state_codes == state, _calculate_age_curve(curve, age), result - ) - return result - - -def _calculate_age_curve(curve, age: np.ndarray) -> np.ndarray: - if hasattr(curve, "calc"): - return np.asarray(curve.calc(age), dtype=float) - return np.full(len(age), float(curve), dtype=float) - - -def family_tier_slcsp_person_share( - *, - state_codes: np.ndarray, - base_cost: np.ndarray, - age: np.ndarray, - is_tax_unit_dependent: np.ndarray, - person_tax_unit_id: np.ndarray, - tax_unit_id: np.ndarray, - fallback: np.ndarray, - time_period: int, -) -> np.ndarray: - """Return tax-unit SLCSP shares for NY/VT family-tier states.""" - - state_codes = _as_str_array(state_codes) - output = np.asarray(fallback, dtype=float).copy() - family_tier_mask = np.isin(state_codes, FAMILY_TIER_STATES) - if not family_tier_mask.any(): - return output - - base_cost = np.asarray(base_cost, dtype=float) - age = np.asarray(age, dtype=float) - is_tax_unit_dependent = np.asarray(is_tax_unit_dependent, dtype=bool) - person_tax_unit_id = np.asarray(person_tax_unit_id) - tax_unit_id = np.asarray(tax_unit_id) - - parameters = _policyengine_us_parameters()(f"{int(time_period)}-01-01").gov.aca - max_child_age = float(parameters.slcsp.max_child_age) - dependent_child_age_threshold = float( - parameters.family_tier_dependent_child_age_threshold - ) - - for unit_id in tax_unit_id: - members = person_tax_unit_id == unit_id - members = members & family_tier_mask - if not members.any(): - continue - member_states = state_codes[members] - state = str(member_states[0]) - if state not in FAMILY_TIER_STATES: - continue - - dependent_child = (age[members] <= max_child_age) | ( - is_tax_unit_dependent[members] - & (age[members] < dependent_child_age_threshold) - ) - adult_count = int(np.count_nonzero(~dependent_child)) - child_count = int(np.count_nonzero(dependent_child)) - member_count = adult_count + child_count - if member_count == 0: - continue - - multiplier = _family_tier_multiplier( - state=state, - adult_count=adult_count, - child_count=child_count, - parameters=parameters, - ) - if multiplier is None: - continue - - positive_base_cost = base_cost[members][base_cost[members] > 0] - if not positive_base_cost.size: - continue - output[members] = float(np.mean(positive_base_cost)) * multiplier / member_count - - return output - - -def _family_tier_multiplier( - *, - state: str, - adult_count: int, - child_count: int, - parameters, -) -> float | None: - ratings = ( - parameters.family_tier_ratings.ny - if state == "NY" - else parameters.family_tier_ratings.vt - ) - extra_adults = max(adult_count - 2, 0) - one_adult = float(ratings.ONE_ADULT) - - if adult_count == 0: - if state == "NY" and child_count > 0: - return float(ratings.CHILD_ONLY) - # Vermont has no child-only family-tier multiplier; retain fallback. - return None - if child_count == 0: - base = one_adult if adult_count == 1 else float(ratings.TWO_ADULTS) - elif adult_count == 1: - base = float(ratings.ONE_ADULT_AND_ONE_OR_MORE_CHILDREN) - else: - base = float(ratings.TWO_ADULTS_AND_ONE_OR_MORE_CHILDREN) - return base + extra_adults * one_adult - - -def medicaid_spending_by_state(time_period: int) -> dict[str, float]: - """Return total Medicaid spending targets by state.""" - - spending = _policyengine_us_parameters()( - f"{int(time_period)}-01-01" - ).calibration.gov.hhs.medicaid.totals.spending - return {state: float(spending[state]) for state in STATE_CODES} - - -def allocate_medicaid_cost_if_enrolled_by_slcsp( - *, - person_slcsp: np.ndarray, - medicaid_enrolled: np.ndarray, - person_weight: np.ndarray, - state_codes: np.ndarray, - state_spending: Mapping[str, float], -) -> np.ndarray: - """Allocate state Medicaid spending using only SLCSP as the cost index.""" - - person_slcsp = np.nan_to_num(np.asarray(person_slcsp, dtype=float), nan=0) - person_slcsp = np.clip(person_slcsp, 0, None) - medicaid_enrolled = np.asarray(medicaid_enrolled, dtype=bool) - person_weight = np.nan_to_num(np.asarray(person_weight, dtype=float), nan=0) - state_codes = _as_str_array(state_codes) - - if not ( - len(person_slcsp) - == len(medicaid_enrolled) - == len(person_weight) - == len(state_codes) - ): - raise ValueError("Medicaid cost allocator inputs must have the same length.") - - output = np.zeros(len(person_slcsp), dtype=float) - positive_slcsp = person_slcsp > 0 - national_fallback = ( - float(np.mean(person_slcsp[positive_slcsp])) if positive_slcsp.any() else 1.0 - ) - - for state, target in state_spending.items(): - state_mask = state_codes == state - if not state_mask.any() or target <= 0: - continue - - state_index = _fill_missing_slcsp( - person_slcsp[state_mask], - fallback=national_fallback, - ) - enrolled_within_state = medicaid_enrolled[state_mask] - if not enrolled_within_state.any(): - continue - - denominator = float( - np.sum( - person_weight[state_mask][enrolled_within_state] - * state_index[enrolled_within_state] - ) - ) - if denominator <= 0: - continue - - output[state_mask] = float(target) * state_index / denominator - - return output - - -def _fill_missing_slcsp(values: np.ndarray, *, fallback: float) -> np.ndarray: - values = np.asarray(values, dtype=float) - positive = values > 0 - if positive.any(): - fallback = float(np.mean(values[positive])) - return np.where(positive, values, fallback) - - -def _calculate( - simulation: Any, - variable: str, - time_period: int, - *, - map_to: str | None = None, -) -> np.ndarray: - kwargs: dict[str, Any] = {"period": time_period} - if map_to is not None: - kwargs["map_to"] = map_to - result = simulation.calculate(variable, **kwargs) - if hasattr(result, "values"): - result = result.values - return np.asarray(result) - - -def _as_str_array(values: np.ndarray) -> np.ndarray: - values = np.asarray(values) - if values.dtype.kind == "S": - return np.char.decode(values.astype("S"), "utf-8") - if values.dtype.kind == "O": - return np.asarray( - [ - value.decode("utf-8") - if isinstance(value, (bytes, bytearray)) - else str(value) - for value in values - ] - ) - return values.astype(str) - - -FAMILY_TIER_STATES = ("NY", "VT") - -STATE_CODES = ( - "AL", - "AK", - "AZ", - "AR", - "CA", - "CO", - "CT", - "DE", - "DC", - "FL", - "GA", - "HI", - "ID", - "IL", - "IN", - "IA", - "KS", - "KY", - "LA", - "ME", - "MD", - "MA", - "MI", - "MN", - "MS", - "MO", - "MT", - "NE", - "NV", - "NH", - "NJ", - "NM", - "NY", - "NC", - "ND", - "OH", - "OK", - "OR", - "PA", - "RI", - "SC", - "SD", - "TN", - "TX", - "UT", - "VT", - "VA", - "WA", - "WV", - "WI", - "WY", -) diff --git a/pyproject.toml b/pyproject.toml index 975e75829..a8c6524f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "policyengine-us==1.711.0", + "policyengine-us==1.715.2", # policyengine-core 3.26.1 is the current 3.26.x runtime and includes the fix for # PolicyEngine/policyengine-core#482 (user-set ETERNITY inputs lost # after _invalidate_all_caches) and is required by policyengine-us 1.682.1+. diff --git a/tests/unit/build_outputs/test_us_augmentations.py b/tests/unit/build_outputs/test_us_augmentations.py index f8f0ce85b..9615ac7c5 100644 --- a/tests/unit/build_outputs/test_us_augmentations.py +++ b/tests/unit/build_outputs/test_us_augmentations.py @@ -15,11 +15,9 @@ from policyengine_us_data.build_outputs.us_augmentations import ( US_ENTITY_POSTPROCESSOR_KEY, US_GEOGRAPHY_POSTPROCESSOR_KEY, - US_MEDICAID_COST_POSTPROCESSOR_KEY, US_TAKEUP_POSTPROCESSOR_KEY, USEntityPostProcessor, USGeographyPostProcessor, - USMedicaidCostPostProcessor, USTakeupPostProcessor, _build_reported_takeup_anchors, default_us_postprocessors, @@ -142,13 +140,11 @@ def test_default_us_postprocessors_are_in_runtime_order(): USEntityPostProcessor, USGeographyPostProcessor, USTakeupPostProcessor, - USMedicaidCostPostProcessor, ) assert tuple(processor.spec.key for processor in postprocessors) == ( US_ENTITY_POSTPROCESSOR_KEY, US_GEOGRAPHY_POSTPROCESSOR_KEY, US_TAKEUP_POSTPROCESSOR_KEY, - US_MEDICAID_COST_POSTPROCESSOR_KEY, ) seen = set() for processor in postprocessors: @@ -426,65 +422,3 @@ def test_us_takeup_postprocessor_rejects_unknown_takeup_results(): payload=_geography_payload(), context=_context(), ) - - -def test_us_medicaid_cost_postprocessor_preserves_cloned_conditional_costs(): - payload = ( - USTakeupPostProcessor( - takeup_applier=lambda **kwargs: { - "takes_up_snap_if_eligible": np.array([True, False]) - }, - ) - .apply( - payload=_geography_payload(), - context=_context(), - ) - .payload - ) - payload = H5Payload( - data={ - **payload.data, - "medicaid_cost_if_enrolled": { - 2024: np.array([100.0, 200.0, 300.0], dtype=np.float32) - }, - }, - time_period=payload.time_period, - entity_lengths=payload.entity_lengths, - ) - - result = USMedicaidCostPostProcessor().apply(payload=payload, context=_context()) - - assert result.payload.variable_entities["medicaid_cost_if_enrolled"] == "person" - np.testing.assert_array_equal( - result.data["medicaid_cost_if_enrolled"][2024], - np.array([100.0, 200.0, 300.0], dtype=np.float32), - ) - - -def test_us_medicaid_cost_postprocessor_clones_source_cost_without_reallocating(): - class SourceVariableProvider: - def get_array(self, variable, period): - assert variable == "medicaid_cost_if_enrolled" - assert period == 2024 - return np.array([10.0, 20.0, 30.0], dtype=np.float32) - - context = _context() - context = replace( - context, - source=replace( - context.source, - input_variables=frozenset({"medicaid_cost_if_enrolled"}), - variable_provider=SourceVariableProvider(), - ), - ) - - result = USMedicaidCostPostProcessor().apply( - payload=_geography_payload(context), - context=context, - ) - - assert result.payload.variable_entities["medicaid_cost_if_enrolled"] == "person" - np.testing.assert_array_equal( - result.data["medicaid_cost_if_enrolled"][2024], - np.array([30.0, 10.0, 20.0], dtype=np.float32), - ) diff --git a/tests/unit/calibration/test_publish_local_area.py b/tests/unit/calibration/test_publish_local_area.py index e04f7f196..c18f13539 100644 --- a/tests/unit/calibration/test_publish_local_area.py +++ b/tests/unit/calibration/test_publish_local_area.py @@ -217,7 +217,6 @@ def write(self, **kwargs): "USEntityPostProcessor", "USGeographyPostProcessor", "USTakeupPostProcessor", - "USMedicaidCostPostProcessor", ] assert seen["build"]["source"] is source assert seen["build"]["takeup_filter"] == ("takes_up_snap",) diff --git a/tests/unit/datasets/cps/test_medicaid_cost.py b/tests/unit/datasets/cps/test_medicaid_cost.py deleted file mode 100644 index 99d5f58ba..000000000 --- a/tests/unit/datasets/cps/test_medicaid_cost.py +++ /dev/null @@ -1,121 +0,0 @@ -import numpy as np -import pytest - -from policyengine_us_data.datasets.cps.medicaid_cost import ( - allocate_medicaid_cost_if_enrolled_by_slcsp, - family_tier_slcsp_person_share, -) - - -def test_allocate_medicaid_cost_if_enrolled_matches_state_targets(): - person_slcsp = np.array([100.0, 200.0, 300.0, 400.0]) - medicaid_enrolled = np.array([True, True, False, True]) - person_weight = np.array([1.0, 2.0, 1.0, 4.0]) - state_codes = np.array(["CA", "CA", "CA", "NY"]) - state_spending = {"CA": 5_000.0, "NY": 8_000.0} - - costs = allocate_medicaid_cost_if_enrolled_by_slcsp( - person_slcsp=person_slcsp, - medicaid_enrolled=medicaid_enrolled, - person_weight=person_weight, - state_codes=state_codes, - state_spending=state_spending, - ) - - ca_baseline_cost = np.sum(costs[:2] * person_weight[:2]) - ny_baseline_cost = costs[3] * person_weight[3] - assert ca_baseline_cost == pytest.approx(5_000.0) - assert ny_baseline_cost == pytest.approx(8_000.0) - assert costs[2] / costs[0] == pytest.approx(3) - - -def test_allocate_medicaid_cost_if_enrolled_fills_missing_slcsp_with_state_mean(): - person_slcsp = np.array([0.0, 200.0, 400.0]) - medicaid_enrolled = np.array([True, True, False]) - person_weight = np.array([1.0, 1.0, 1.0]) - state_codes = np.array([b"CA", b"CA", b"CA"]) - - costs = allocate_medicaid_cost_if_enrolled_by_slcsp( - person_slcsp=person_slcsp, - medicaid_enrolled=medicaid_enrolled, - person_weight=person_weight, - state_codes=state_codes, - state_spending={"CA": 900.0}, - ) - - assert np.sum(costs[:2] * person_weight[:2]) == pytest.approx(900.0) - assert costs[0] == pytest.approx(costs[1] * 1.5) - assert costs[2] == pytest.approx(costs[1] * 2) - - -def test_allocate_medicaid_cost_if_enrolled_requires_aligned_inputs(): - with pytest.raises(ValueError, match="same length"): - allocate_medicaid_cost_if_enrolled_by_slcsp( - person_slcsp=np.array([100.0]), - medicaid_enrolled=np.array([True, False]), - person_weight=np.array([1.0]), - state_codes=np.array(["CA"]), - state_spending={"CA": 100.0}, - ) - - -def test_family_tier_slcsp_person_share_allocates_ny_tax_unit_premium(): - share = family_tier_slcsp_person_share( - state_codes=np.array(["NY", "NY", "NY", "NY"]), - base_cost=np.array([500.0, 500.0, 500.0, 500.0]), - age=np.array([40, 38, 10, 8]), - is_tax_unit_dependent=np.array([False, False, True, True]), - person_tax_unit_id=np.array([1, 1, 1, 1]), - tax_unit_id=np.array([1]), - fallback=np.array([999.0, 999.0, 999.0, 999.0]), - time_period=2026, - ) - - np.testing.assert_allclose(share, np.full(4, 500.0 * 2.85 / 4)) - - -def test_family_tier_slcsp_person_share_uses_ny_child_only_tier(): - share = family_tier_slcsp_person_share( - state_codes=np.array(["NY", "NY"]), - base_cost=np.array([500.0, 500.0]), - age=np.array([12, 10]), - is_tax_unit_dependent=np.array([True, True]), - person_tax_unit_id=np.array([1, 1]), - tax_unit_id=np.array([1]), - fallback=np.array([999.0, 999.0]), - time_period=2026, - ) - - np.testing.assert_allclose(share, np.full(2, 500.0 * 0.412 / 2)) - - -def test_family_tier_slcsp_person_share_preserves_vt_child_only_fallback(): - fallback = np.array([500.0, 500.0]) - - share = family_tier_slcsp_person_share( - state_codes=np.array(["VT", "VT"]), - base_cost=np.array([500.0, 500.0]), - age=np.array([12, 10]), - is_tax_unit_dependent=np.array([True, True]), - person_tax_unit_id=np.array([1, 1]), - tax_unit_id=np.array([1]), - fallback=fallback, - time_period=2026, - ) - - np.testing.assert_allclose(share, fallback) - - -def test_family_tier_slcsp_person_share_allocates_vt_family_tier_premium(): - share = family_tier_slcsp_person_share( - state_codes=np.array(["VT", "VT", "VT"]), - base_cost=np.array([500.0, 500.0, 500.0]), - age=np.array([40, 10, 8]), - is_tax_unit_dependent=np.array([False, True, True]), - person_tax_unit_id=np.array([1, 1, 1]), - tax_unit_id=np.array([1]), - fallback=np.array([999.0, 999.0, 999.0]), - time_period=2026, - ) - - np.testing.assert_allclose(share, np.full(3, 500.0 * 1.93 / 3)) diff --git a/tests/unit/test_policyengine_us_dependency_contract.py b/tests/unit/test_policyengine_us_dependency_contract.py index 06cd6b3ef..b03e62d24 100644 --- a/tests/unit/test_policyengine_us_dependency_contract.py +++ b/tests/unit/test_policyengine_us_dependency_contract.py @@ -39,3 +39,11 @@ def test_policyengine_us_defines_housing_income_limit_contract(): ): variable = tax_benefit_system.variables[variable_name] assert variable.entity.key == "spm_unit" + + +def test_policyengine_us_defines_formula_backed_medicaid_cost_if_enrolled(): + tax_benefit_system = CountryTaxBenefitSystem() + variable = tax_benefit_system.variables["medicaid_cost_if_enrolled"] + + assert variable.entity.key == "person" + assert getattr(variable, "formulas", None) diff --git a/uv.lock b/uv.lock index 71f4a0242..47587b9cf 100644 --- a/uv.lock +++ b/uv.lock @@ -2164,7 +2164,7 @@ wheels = [ [[package]] name = "policyengine-us" -version = "1.711.0" +version = "1.715.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microdf-python" }, @@ -2174,9 +2174,9 @@ dependencies = [ { name = "tables" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/ed/8825980a62e009610d6fa36f55f6c8a32deb0fb770d1f3513e2df9c7f7fe/policyengine_us-1.711.0.tar.gz", hash = "sha256:c52c8e68f3a01ee5935320175e841459503e67f84c41899f9768f4a5b300b4a3", size = 9956103, upload-time = "2026-05-27T21:31:17.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/ef/d87bb056084897932e083b0412976a386d29062834b0e697afa044642a75/policyengine_us-1.715.2.tar.gz", hash = "sha256:b3990ae9b7c694d2cbf497e6256850aca7be5a5a73ac98330682aba9edd61b61", size = 10014025, upload-time = "2026-05-29T02:48:39.527Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/aa/3e8471c852c75ecc7c2cbbdaedf79b70a8d207df7f689abfd2b3b570bd7a/policyengine_us-1.711.0-py3-none-any.whl", hash = "sha256:e37d7ee5926954ecf9e03d91ccd190a1609e6322426c12fd6cdd867a913ee2d9", size = 10887738, upload-time = "2026-05-27T21:31:14.859Z" }, + { url = "https://files.pythonhosted.org/packages/45/a1/1d56bdbb69d7ce06bedd3892203a75ac3350a90c0b5fcea2fb50db46670f/policyengine_us-1.715.2-py3-none-any.whl", hash = "sha256:abf079828419762f5c4b0291a70f6e424744200f237e1ae0f06e25f10130c399", size = 11035379, upload-time = "2026-05-29T02:48:35.193Z" }, ] [[package]] @@ -2246,7 +2246,7 @@ requires-dist = [ { name = "pandas", specifier = ">=2.3.1" }, { name = "pip-system-certs", specifier = ">=3.0" }, { name = "policyengine-core", specifier = ">=3.26.1,<3.27" }, - { name = "policyengine-us", specifier = "==1.711.0" }, + { name = "policyengine-us", specifier = "==1.715.2" }, { name = "requests", specifier = ">=2.25.0" }, { name = "scipy", specifier = ">=1.15.3" }, { name = "setuptools", specifier = ">=60" },