From 590191ec442ff2b63e4ed699117318e8d8ead3ed Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:22:08 -0400 Subject: [PATCH 1/2] Add CHIP enrollment calibration targets --- .../build_outputs/us_augmentations.py | 7 + .../calibration/entity_clone.py | 7 + .../calibration/target_config.yaml | 6 + .../calibration/unified_matrix_builder.py | 8 ++ policyengine_us_data/datasets/cps/cps.py | 9 ++ policyengine_us_data/db/etl_medicaid.py | 123 +++++++++++++++++- policyengine_us_data/db/validate_hierarchy.py | 2 + .../parameters/take_up/chip.yaml | 12 ++ policyengine_us_data/storage/README.md | 6 + .../chip_enrollment_2024.csv | 52 ++++++++ .../chip_enrollment_2025.csv | 52 ++++++++ .../chip_enrollment_2026.csv | 52 ++++++++ policyengine_us_data/utils/loss.py | 53 ++++++++ policyengine_us_data/utils/takeup.py | 6 + tests/integration/test_cps_generation.py | 4 + .../build_outputs/test_us_augmentations.py | 7 + tests/unit/calibration/test_loss_targets.py | 22 ++++ tests/unit/calibration/test_target_config.py | 5 + .../calibration/test_unified_calibration.py | 2 +- tests/unit/test_etl_medicaid.py | 21 +++ 20 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 policyengine_us_data/parameters/take_up/chip.yaml create mode 100644 policyengine_us_data/storage/calibration_targets/chip_enrollment_2024.csv create mode 100644 policyengine_us_data/storage/calibration_targets/chip_enrollment_2025.csv create mode 100644 policyengine_us_data/storage/calibration_targets/chip_enrollment_2026.csv create mode 100644 tests/unit/test_etl_medicaid.py diff --git a/policyengine_us_data/build_outputs/us_augmentations.py b/policyengine_us_data/build_outputs/us_augmentations.py index b418c0d19..25407ffbe 100644 --- a/policyengine_us_data/build_outputs/us_augmentations.py +++ b/policyengine_us_data/build_outputs/us_augmentations.py @@ -633,6 +633,13 @@ def _build_reported_takeup_anchors( reported_anchors["takes_up_medicaid_if_eligible"] = data[ "has_medicaid_health_coverage_at_interview" ][time_period].astype(bool) + if ( + "reported_has_chip_health_coverage_at_interview" in data + and time_period in data["reported_has_chip_health_coverage_at_interview"] + ): + reported_anchors["takes_up_chip_if_eligible"] = data[ + "reported_has_chip_health_coverage_at_interview" + ][time_period].astype(bool) if ( "receives_housing_assistance" in data and time_period in data["receives_housing_assistance"] diff --git a/policyengine_us_data/calibration/entity_clone.py b/policyengine_us_data/calibration/entity_clone.py index a432b208a..72da20853 100644 --- a/policyengine_us_data/calibration/entity_clone.py +++ b/policyengine_us_data/calibration/entity_clone.py @@ -142,6 +142,13 @@ def _build_reported_takeup_anchors(data: dict, time_period: int) -> dict: reported_anchors["takes_up_medicaid_if_eligible"] = data[ "has_medicaid_health_coverage_at_interview" ][time_period].astype(bool) + if ( + "reported_has_chip_health_coverage_at_interview" in data + and time_period in data["reported_has_chip_health_coverage_at_interview"] + ): + reported_anchors["takes_up_chip_if_eligible"] = data[ + "reported_has_chip_health_coverage_at_interview" + ][time_period].astype(bool) if ( "receives_housing_assistance" in data and time_period in data["receives_housing_assistance"] diff --git a/policyengine_us_data/calibration/target_config.yaml b/policyengine_us_data/calibration/target_config.yaml index 8e2835213..be3e6e1c2 100644 --- a/policyengine_us_data/calibration/target_config.yaml +++ b/policyengine_us_data/calibration/target_config.yaml @@ -45,6 +45,12 @@ include: - variable: person_count geo_level: national domain_variable: medicaid_enrolled + - variable: person_count + geo_level: state + domain_variable: chip_enrolled + - variable: person_count + geo_level: national + domain_variable: chip_enrolled # REMOVED: is_pregnant — 100% unachievable across all 51 state geos - variable: snap geo_level: state diff --git a/policyengine_us_data/calibration/unified_matrix_builder.py b/policyengine_us_data/calibration/unified_matrix_builder.py index 6e59925b5..dccce222e 100644 --- a/policyengine_us_data/calibration/unified_matrix_builder.py +++ b/policyengine_us_data/calibration/unified_matrix_builder.py @@ -2800,6 +2800,14 @@ def build_matrix( reported_takeup_anchors["takes_up_medicaid_if_eligible"] = f[ "has_medicaid_health_coverage_at_interview" ][period_key][...].astype(bool) + if ( + "reported_has_chip_health_coverage_at_interview" in f + and period_key + in f["reported_has_chip_health_coverage_at_interview"] + ): + reported_takeup_anchors["takes_up_chip_if_eligible"] = f[ + "reported_has_chip_health_coverage_at_interview" + ][period_key][...].astype(bool) if ( "receives_housing_assistance" in f and period_key in f["receives_housing_assistance"] diff --git a/policyengine_us_data/datasets/cps/cps.py b/policyengine_us_data/datasets/cps/cps.py index 7e2d6b76f..e9545e576 100644 --- a/policyengine_us_data/datasets/cps/cps.py +++ b/policyengine_us_data/datasets/cps/cps.py @@ -579,6 +579,7 @@ def add_takeup(self): snap_rate = load_take_up_rate("snap", self.time_period) aca_rate = load_take_up_rate("aca", self.time_period) medicaid_rates_by_state = load_take_up_rate("medicaid", self.time_period) + chip_rate = load_take_up_rate("chip", self.time_period) head_start_rate = load_take_up_rate("head_start", self.time_period) early_head_start_rate = load_take_up_rate("early_head_start", self.time_period) ssi_rate = load_take_up_rate("ssi", self.time_period) @@ -637,6 +638,14 @@ def add_takeup(self): group_keys=person_states, ) + # CHIP: preserve current full-takeup default while anchoring CPS reporters. + rng = seeded_rng("takes_up_chip_if_eligible") + data["takes_up_chip_if_eligible"] = assign_takeup_with_reported_anchors( + rng.random(n_persons), + chip_rate, + reported_mask=data["reported_has_chip_health_coverage_at_interview"], + ) + # Head Start rng = seeded_rng("takes_up_head_start_if_eligible") data["takes_up_head_start_if_eligible"] = rng.random(n_persons) < head_start_rate diff --git a/policyengine_us_data/db/etl_medicaid.py b/policyengine_us_data/db/etl_medicaid.py index 05eff43e4..030817031 100644 --- a/policyengine_us_data/db/etl_medicaid.py +++ b/policyengine_us_data/db/etl_medicaid.py @@ -129,6 +129,61 @@ def transform_administrative_medicaid_data(state_admin_df, year): return state_df[["ucgid_str", "medicaid_enrollment"]] +def transform_administrative_chip_data(state_admin_df, year): + reporting_period = year * 100 + 12 + print(f"Reporting period is {reporting_period}") + state_df = state_admin_df.loc[ + (state_admin_df["Reporting Period"] == reporting_period) + & (state_admin_df["Final Report"] == "Y"), + [ + "State Abbreviation", + "Reporting Period", + "Total CHIP Enrollment", + ], + ].copy() + + state_df["FIPS"] = state_df["State Abbreviation"].map(STATE_ABBREV_TO_FIPS) + + state_df = state_df.rename(columns={"Total CHIP Enrollment": "chip_enrollment"}) + + problem_states = state_df[state_df["chip_enrollment"].isna()][ + "State Abbreviation" + ].tolist() + + if problem_states: + print( + f"Warning: States with missing CHIP enrollment in {reporting_period}: " + f"{problem_states}" + ) + print("Attempting to use most recent non-zero values...") + + for state_abbrev in problem_states: + state_history = state_admin_df[ + (state_admin_df["State Abbreviation"] == state_abbrev) + & (state_admin_df["Final Report"] == "Y") + & (state_admin_df["Total CHIP Enrollment"] > 0) + & (state_admin_df["Reporting Period"] < reporting_period) + ].sort_values("Reporting Period", ascending=False) + + if not state_history.empty: + fallback_value = state_history.iloc[0]["Total CHIP Enrollment"] + fallback_period = state_history.iloc[0]["Reporting Period"] + print( + f" {state_abbrev}: Using {fallback_value:,.0f} " + f"from period {fallback_period}" + ) + state_df.loc[ + state_df["State Abbreviation"] == state_abbrev, + "chip_enrollment", + ] = fallback_value + else: + print(f" {state_abbrev}: No historical data found, keeping 0") + + state_df["ucgid_str"] = "0400000US" + state_df["FIPS"].astype(str) + + return state_df[["ucgid_str", "chip_enrollment"]] + + def transform_survey_medicaid_data(cd_survey_df): cd_df = cd_survey_df[ ["GEO_ID", "state", "congressional district", "S2704_C02_006E"] @@ -145,7 +200,7 @@ def transform_survey_medicaid_data(cd_survey_df): return cd_df[["ucgid_str", "medicaid_enrollment"]] -def load_medicaid_data(long_state, long_cd, year): +def load_medicaid_data(long_state, long_cd, year, long_chip_state=None): DATABASE_URL = f"sqlite:///{STORAGE_FOLDER / 'calibration' / 'policy_data.db'}" engine = create_engine(DATABASE_URL) @@ -175,6 +230,30 @@ def load_medicaid_data(long_state, long_cd, year): "state": {}, } + if long_chip_state is not None: + nat_chip_stratum = Stratum( + parent_stratum_id=geo_strata["national"], + notes="National CHIP Enrolled", + ) + nat_chip_stratum.constraints_rel = [ + StratumConstraint( + constraint_variable="chip_enrolled", + operation="==", + value="True", + ), + ] + nat_chip_stratum.targets_rel.append( + Target( + variable="person_count", + period=year, + value=long_chip_state["chip_enrollment"].sum(), + active=True, + source="CMS CHIP", + ) + ) + session.add(nat_chip_stratum) + session.flush() + # State ------------------- for _, row in long_state.iterrows(): # Parse the UCGID to get state_fips @@ -215,6 +294,40 @@ def load_medicaid_data(long_state, long_cd, year): session.flush() medicaid_stratum_lookup["state"][state_fips] = new_stratum.stratum_id + if long_chip_state is not None: + for _, row in long_chip_state.iterrows(): + geo_info = parse_ucgid(row["ucgid_str"]) + state_fips = geo_info["state_fips"] + parent_stratum_id = geo_strata["state"][state_fips] + + new_stratum = Stratum( + parent_stratum_id=parent_stratum_id, + notes=f"State FIPS {state_fips} CHIP Enrolled", + ) + new_stratum.constraints_rel = [ + StratumConstraint( + constraint_variable="state_fips", + operation="==", + value=str(state_fips), + ), + StratumConstraint( + constraint_variable="chip_enrolled", + operation="==", + value="True", + ), + ] + new_stratum.targets_rel.append( + Target( + variable="person_count", + period=year, + value=row["chip_enrollment"], + active=True, + source="CMS CHIP", + ) + ) + session.add(new_stratum) + session.flush() + # District ------------------- if long_cd is None: session.commit() @@ -291,9 +404,15 @@ def main(): # Transform ------------------- long_state = transform_administrative_medicaid_data(state_admin_df, year) + long_chip_state = transform_administrative_chip_data(state_admin_df, year) # Load (state admin only, no CD survey) --- - load_medicaid_data(long_state, long_cd=None, year=year) + load_medicaid_data( + long_state, + long_cd=None, + year=year, + long_chip_state=long_chip_state, + ) if __name__ == "__main__": diff --git a/policyengine_us_data/db/validate_hierarchy.py b/policyengine_us_data/db/validate_hierarchy.py index 1c555703f..c6eb90e2f 100644 --- a/policyengine_us_data/db/validate_hierarchy.py +++ b/policyengine_us_data/db/validate_hierarchy.py @@ -197,11 +197,13 @@ def validate_demographic_strata(session): # because CD-level survey data is disabled pending 119th Congress # district code remapping (see etl_medicaid.py TODO). # the national medicaid target actually uses the `medicaid` (expense) variable + # chip_enrolled has national and state targets but no district targets. expected_counts = { "age": 18 * 488, "adjusted_gross_income": 9 * 488, "snap": 1 * 488, "medicaid_enrolled": 1 * 51, + "chip_enrolled": 1 * 52, "eitc_child_count": 4 * 488, } diff --git a/policyengine_us_data/parameters/take_up/chip.yaml b/policyengine_us_data/parameters/take_up/chip.yaml new file mode 100644 index 000000000..7978d34fb --- /dev/null +++ b/policyengine_us_data/parameters/take_up/chip.yaml @@ -0,0 +1,12 @@ +description: Percentage of people who enroll in CHIP, if eligible. +metadata: + label: CHIP takeup rate + unit: /1 + period: year + reference: + - title: CMS Medicaid and CHIP enrollment data + href: https://data.medicaid.gov/dataset/6165f45b-ca93-5bb5-9d06-db29c692a360 +# Keep the current PolicyEngine-US default behavior until state-specific CHIP +# rates are derived from the new CMS enrollment targets and modeled eligibility. +values: + 2024-01-01: 1.0 diff --git a/policyengine_us_data/storage/README.md b/policyengine_us_data/storage/README.md index 03ea8023a..1f563c32d 100644 --- a/policyengine_us_data/storage/README.md +++ b/policyengine_us_data/storage/README.md @@ -42,6 +42,12 @@ • Location: https://data.medicaid.gov/dataset/6165f45b-ca93-5bb5-9d06-db29c692a360?conditions%5B0%5D%5Boperator%5D=%3D&conditions%5B0%5D%5Bproperty%5D=reporting_period&conditions%5B0%5D%5Bvalue%5D=202512&conditions%5B1%5D%5Boperator%5D=%3D&conditions%5B1%5D%5Bproperty%5D=preliminary_or_updated&conditions%5B1%5D%5Bvalue%5D=U • Notes: Uses `total_medicaid_enrollment`, not combined Medicaid and CHIP enrollment. +- **chip_enrollment_2024.csv, chip_enrollment_2025.csv, chip_enrollment_2026.csv** + • Source: Medicaid.gov performance indicator dataset, Applications, Eligibility, and Enrollment Data + • Date: December 2024 final reports, December 2025 final reports, and January 2026 preliminary reports + • Location: https://data.medicaid.gov/dataset/6165f45b-ca93-5bb5-9d06-db29c692a360 + • Notes: Uses `total_chip_enrollment`, not combined Medicaid and CHIP enrollment. The 2026 file mirrors the reporting period and preliminary status used by `medicaid_enrollment_2026.csv`. + - **district_mapping.csv** • Source: created by the script `policyengine_us/storage/calibration_targets/make_district_mapping.py` • Notes: this script is not part of `make data` because of the length of time it takes to run and the diff --git a/policyengine_us_data/storage/calibration_targets/chip_enrollment_2024.csv b/policyengine_us_data/storage/calibration_targets/chip_enrollment_2024.csv new file mode 100644 index 000000000..61cc3f0ba --- /dev/null +++ b/policyengine_us_data/storage/calibration_targets/chip_enrollment_2024.csv @@ -0,0 +1,52 @@ +state,enrollment +AK,14814 +AL,183595 +AR,82091 +AZ,120881 +CA,1232909 +CO,135402 +CT,23444 +DC,17485 +DE,12528 +FL,172021 +GA,234732 +HI,22067 +IA,85253 +ID,20533 +IL,291267 +IN,143162 +KS,77160 +KY,133062 +LA,147957 +MA,197970 +MD,197870 +ME,28061 +MI,173347 +MN,5012 +MO,131476 +MS,84484 +MT,19812 +NC,347586 +ND,5221 +NE,37691 +NH,19337 +NJ,253484 +NM,47202 +NV,52007 +NY,675709 +OH,241124 +OK,83925 +OR,188738 +PA,289619 +RI,0 +SC,104711 +SD,16387 +TN,171595 +TX,356109 +UT,36723 +VA,200323 +VT,5196 +WA,60356 +WI,80976 +WV,37499 +WY,5961 diff --git a/policyengine_us_data/storage/calibration_targets/chip_enrollment_2025.csv b/policyengine_us_data/storage/calibration_targets/chip_enrollment_2025.csv new file mode 100644 index 000000000..39448ae32 --- /dev/null +++ b/policyengine_us_data/storage/calibration_targets/chip_enrollment_2025.csv @@ -0,0 +1,52 @@ +state,enrollment +AK,12976 +AL,194447 +AR,83783 +AZ,112634 +CA,1233169 +CO,140354 +CT,25353 +DC,16392 +DE,13989 +FL,160955 +GA,190017 +HI,24762 +IA,85504 +ID,19848 +IL,322451 +IN,126144 +KS,72305 +KY,133266 +LA,137824 +MA,191701 +MD,198218 +ME,27038 +MI,169340 +MN,4361 +MO,133259 +MS,83921 +MT,20272 +NC,346463 +ND,5542 +NE,37597 +NH,19186 +NJ,259759 +NM,43713 +NV,47076 +NY,662825 +OH,242337 +OK,79005 +OR,184481 +PA,281068 +RI,33661 +SC,114661 +SD,14418 +TN,183297 +TX,346787 +UT,34982 +VA,195480 +VT,5432 +WA,65056 +WI,100494 +WV,39681 +WY,5945 diff --git a/policyengine_us_data/storage/calibration_targets/chip_enrollment_2026.csv b/policyengine_us_data/storage/calibration_targets/chip_enrollment_2026.csv new file mode 100644 index 000000000..2691a4d36 --- /dev/null +++ b/policyengine_us_data/storage/calibration_targets/chip_enrollment_2026.csv @@ -0,0 +1,52 @@ +state,enrollment +AK,12869 +AL,194604 +AR,84160 +AZ,112545 +CA,1237976 +CO,140483 +CT,25561 +DC,16165 +DE,13946 +FL,163022 +GA,183456 +HI,25342 +IA,84760 +ID,19587 +IL,320864 +IN,122313 +KS,72803 +KY,134031 +LA,137043 +MA,188257 +MD,196171 +ME,21431 +MI,167306 +MN,4104 +MO,134247 +MS,83703 +MT,19792 +NC,344352 +ND,5414 +NE,37249 +NH,18907 +NJ,256572 +NM,44018 +NV,47335 +NY,659617 +OH,242200 +OK,74717 +OR,184066 +PA,278730 +RI,34013 +SC,113356 +SD,14404 +TN,181049 +TX,344813 +UT,34679 +VA,192709 +VT,5468 +WA,65228 +WI,99940 +WV,39648 +WY,6033 diff --git a/policyengine_us_data/utils/loss.py b/policyengine_us_data/utils/loss.py index b143824c0..6fedfc2dd 100644 --- a/policyengine_us_data/utils/loss.py +++ b/policyengine_us_data/utils/loss.py @@ -568,6 +568,12 @@ def _load_medicaid_enrollment_targets( return _load_yeared_target_csv("medicaid_enrollment", requested_year) +def _load_chip_enrollment_targets( + requested_year: int, +) -> tuple[pd.DataFrame, int]: + return _load_yeared_target_csv("chip_enrollment", requested_year) + + def _get_aca_national_targets(requested_year: int) -> tuple[float, float, int]: targets, data_year = _load_aca_spending_and_enrollment_targets(requested_year) aca_ptc_state = _load_aca_ptc_state_targets(requested_year) @@ -607,6 +613,11 @@ def _get_medicaid_national_targets(requested_year: int) -> tuple[float, float, i ) +def _get_chip_national_enrollment_target(requested_year: int) -> tuple[float, int]: + targets, data_year = _load_chip_enrollment_targets(requested_year) + return float(targets["enrollment"].sum()), data_year + + def _skip_unverified_target(value) -> bool: """Return True when a CSV value is a placeholder instead of a real target. @@ -1557,6 +1568,20 @@ def build_loss_matrix(dataset: type, time_period): loss_matrix[label] = sim.map_result(on_medicaid, "person", "household") targets_array.append(medicaid_enrollment_target) + # 3. CHIP Enrollment + chip_enrollment_target, _ = _get_chip_national_enrollment_target(time_period) + label = "nation/hhs/chip_enrollment" + on_chip = ( + sim.calculate( + "chip_enrolled", + map_to="person", + period=time_period, + ).values + > 0 + ).astype(int) + loss_matrix[label] = sim.map_result(on_chip, "person", "household") + targets_array.append(chip_enrollment_target) + # National ACA Spending aca_spending_target, aca_enrollment_target, _ = _get_aca_national_targets( time_period @@ -1917,6 +1942,34 @@ def build_loss_matrix(dataset: type, time_period): f"with target {row['enrollment']:.0f}k" ) + # CHIP enrollment by state + + chip_enrollment_by_state, _ = _load_chip_enrollment_targets(time_period) + + has_chip = sim.calculate( + "chip_enrolled", map_to="person", period=time_period + ).values + is_chip_eligible = sim.calculate( + "is_chip_eligible", map_to="person", period=time_period + ).values + is_enrolled = has_chip & is_chip_eligible + + for _, row in chip_enrollment_by_state.iterrows(): + in_state = state_person == row["state"] + in_state_enrolled = in_state & is_enrolled + + label = f"state/hhs/chip_enrollment/{row['state'].lower()}" + loss_matrix[label] = sim.map_result(in_state_enrolled, "person", "household") + if any(loss_matrix[label].isna()): + raise ValueError(f"Missing values for {label}") + + targets_array.append(row["enrollment"]) + + logging.info( + f"Targeting CHIP enrollment for {row['state']} " + f"with target {row['enrollment']:.0f}" + ) + # State 10-year age targets age_targets = pd.read_csv(CALIBRATION_FOLDER / "age_state.csv") diff --git a/policyengine_us_data/utils/takeup.py b/policyengine_us_data/utils/takeup.py index 38377514d..f521dc988 100644 --- a/policyengine_us_data/utils/takeup.py +++ b/policyengine_us_data/utils/takeup.py @@ -66,6 +66,12 @@ "rate_key": "medicaid", "target": "medicaid", }, + { + "variable": "takes_up_chip_if_eligible", + "entity": "person", + "rate_key": "chip", + "target": "chip", + }, { "variable": "takes_up_tanf_if_eligible", "entity": "spm_unit", diff --git a/tests/integration/test_cps_generation.py b/tests/integration/test_cps_generation.py index 32c985bcc..94de8a347 100644 --- a/tests/integration/test_cps_generation.py +++ b/tests/integration/test_cps_generation.py @@ -74,6 +74,9 @@ def __init__(self, file_path): "reported_has_subsidized_marketplace_health_coverage_at_interview": np.array( [False, False] ), + "reported_has_chip_health_coverage_at_interview": np.array( + [False, False] + ), "has_medicaid_health_coverage_at_interview": np.array([False, False]), "employment_income": np.array([20_000.0, 0.0], dtype=np.float32), "age": np.array([40, 66], dtype=np.int32), @@ -104,6 +107,7 @@ def save_dataset(self, data): "snap": 1.0, "aca": 0.0, "medicaid": {"CA": 0.0}, + "chip": 1.0, "head_start": 0.0, "early_head_start": 0.0, "ssi": 1.0, diff --git a/tests/unit/build_outputs/test_us_augmentations.py b/tests/unit/build_outputs/test_us_augmentations.py index 9615ac7c5..66c88473f 100644 --- a/tests/unit/build_outputs/test_us_augmentations.py +++ b/tests/unit/build_outputs/test_us_augmentations.py @@ -175,6 +175,9 @@ def test_build_reported_takeup_anchors_uses_present_period(): "has_medicaid_health_coverage_at_interview": { 2024: np.array([False, True, False]) }, + "reported_has_chip_health_coverage_at_interview": { + 2024: np.array([False, False, True]) + }, "receives_housing_assistance": { 2024: np.array([True, False]), }, @@ -190,6 +193,10 @@ def test_build_reported_takeup_anchors_uses_present_period(): anchors["takes_up_medicaid_if_eligible"], np.array([False, True, False]), ) + np.testing.assert_array_equal( + anchors["takes_up_chip_if_eligible"], + np.array([False, False, True]), + ) np.testing.assert_array_equal( anchors["takes_up_housing_assistance_if_eligible"], np.array([True, False]), diff --git a/tests/unit/calibration/test_loss_targets.py b/tests/unit/calibration/test_loss_targets.py index 9ec8032b9..e26901bf8 100644 --- a/tests/unit/calibration/test_loss_targets.py +++ b/tests/unit/calibration/test_loss_targets.py @@ -38,9 +38,11 @@ _add_ssi_recipient_targets, _add_transfer_balance_targets, _cbo_program_target_value, + _get_chip_national_enrollment_target, _get_medicaid_national_targets, _get_aca_national_targets, _load_aca_spending_and_enrollment_targets, + _load_chip_enrollment_targets, _load_medicaid_enrollment_targets, _should_skip_soi_agi_row, _should_skip_soi_taxability_row, @@ -264,6 +266,26 @@ def test_medicaid_national_targets_use_2026_enrollment(): assert spending == pytest.approx(1_000_645_800_000.0001) +def test_chip_targets_roll_forward_to_2026(): + targets, data_year = _load_chip_enrollment_targets(2026) + + assert data_year == 2026 + assert len(targets) == 51 + assert int(targets["enrollment"].sum()) == 7_241_058 + + +def test_chip_targets_fall_back_to_earliest_available_year(): + _, data_year = _load_chip_enrollment_targets(2023) + assert data_year == 2024 + + +def test_chip_national_target_uses_2026_enrollment(): + enrollment, data_year = _get_chip_national_enrollment_target(2026) + + assert data_year == 2026 + assert enrollment == 7_241_058 + + class _FakeArrayResult: def __init__(self, values): self.values = np.asarray(values) diff --git a/tests/unit/calibration/test_target_config.py b/tests/unit/calibration/test_target_config.py index 09203f745..6120469ee 100644 --- a/tests/unit/calibration/test_target_config.py +++ b/tests/unit/calibration/test_target_config.py @@ -480,6 +480,11 @@ def test_training_config_uses_enrollment_flag_for_medicaid_count_target(self): "geo_level": "national", "domain_variable": "medicaid_enrolled", } in include_rules + assert { + "variable": "person_count", + "geo_level": "national", + "domain_variable": "chip_enrolled", + } in include_rules assert { "variable": "person_count", "geo_level": "national", diff --git a/tests/unit/calibration/test_unified_calibration.py b/tests/unit/calibration/test_unified_calibration.py index 1342b0511..ccc04740d 100644 --- a/tests/unit/calibration/test_unified_calibration.py +++ b/tests/unit/calibration/test_unified_calibration.py @@ -716,7 +716,7 @@ def test_all_entries_have_required_keys(self): ) def test_expected_count(self): - assert len(SIMPLE_TAKEUP_VARS) == 10 + assert len(SIMPLE_TAKEUP_VARS) == 11 class TestTakeupAffectedTargets: diff --git a/tests/unit/test_etl_medicaid.py b/tests/unit/test_etl_medicaid.py new file mode 100644 index 000000000..eb95a452a --- /dev/null +++ b/tests/unit/test_etl_medicaid.py @@ -0,0 +1,21 @@ +import pandas as pd + +from policyengine_us_data.db.etl_medicaid import transform_administrative_chip_data + + +def test_transform_administrative_chip_data_preserves_reported_zero_enrollment(): + source = pd.DataFrame( + { + "State Abbreviation": ["RI", "RI", "CA"], + "Reporting Period": [202411, 202412, 202412], + "Final Report": ["Y", "Y", "Y"], + "Total CHIP Enrollment": [12_345, 0, 1_232_909], + } + ) + + transformed = transform_administrative_chip_data(source, 2024) + + assert transformed.to_dict("records") == [ + {"ucgid_str": "0400000US44", "chip_enrollment": 0}, + {"ucgid_str": "0400000US06", "chip_enrollment": 1_232_909}, + ] From 0485cb4994e3867dc7f5e348ade17de935a6aa50 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:19:10 -0400 Subject: [PATCH 2/2] Fix CHIP calibration CI failures --- changelog.d/1162.added.md | 1 + policyengine_us_data/parameters/take_up/chip.yaml | 2 +- pyproject.toml | 2 +- tests/unit/calibration/test_unified_calibration.py | 11 +++++++++++ tests/unit/test_stochastic_variables.py | 4 ++++ uv.lock | 8 ++++---- 6 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 changelog.d/1162.added.md diff --git a/changelog.d/1162.added.md b/changelog.d/1162.added.md new file mode 100644 index 000000000..16e57391b --- /dev/null +++ b/changelog.d/1162.added.md @@ -0,0 +1 @@ +Add CHIP enrollment calibration targets and CPS CHIP take-up anchoring. diff --git a/policyengine_us_data/parameters/take_up/chip.yaml b/policyengine_us_data/parameters/take_up/chip.yaml index 7978d34fb..d3afdecb1 100644 --- a/policyengine_us_data/parameters/take_up/chip.yaml +++ b/policyengine_us_data/parameters/take_up/chip.yaml @@ -9,4 +9,4 @@ metadata: # Keep the current PolicyEngine-US default behavior until state-specific CHIP # rates are derived from the new CMS enrollment targets and modeled eligibility. values: - 2024-01-01: 1.0 + 2018-01-01: 1.0 diff --git a/pyproject.toml b/pyproject.toml index f9dff0c7b..b345a1a3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "policyengine-us==1.715.3", + "policyengine-us==1.719.0", # 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/calibration/test_unified_calibration.py b/tests/unit/calibration/test_unified_calibration.py index ccc04740d..8491ae44a 100644 --- a/tests/unit/calibration/test_unified_calibration.py +++ b/tests/unit/calibration/test_unified_calibration.py @@ -416,6 +416,17 @@ def test_returns_all_takeup_vars(self): assert spec["variable"] in result assert result[spec["variable"]].dtype == bool + def test_chip_takeup_loads_for_2023_pipeline_year(self): + args = self._make_arrays(4, 2, 1, 1) + result = apply_block_takeup_to_arrays( + *args, + time_period=2023, + takeup_filter=["takes_up_chip_if_eligible"], + ) + + assert len(result["takes_up_chip_if_eligible"]) == 8 + assert result["takes_up_chip_if_eligible"].all() + def test_correct_entity_counts(self): args = self._make_arrays(20, 10, 4, 3) result = apply_block_takeup_to_arrays( diff --git a/tests/unit/test_stochastic_variables.py b/tests/unit/test_stochastic_variables.py index a168f3ee3..f8412f595 100644 --- a/tests/unit/test_stochastic_variables.py +++ b/tests/unit/test_stochastic_variables.py @@ -37,6 +37,10 @@ def test_aca_rate_loads(self): rate = load_take_up_rate("aca", 2022) assert 0 < rate <= 1 + def test_chip_rate_preserves_current_default_for_2023_pipeline(self): + rate = load_take_up_rate("chip", 2023) + assert rate == 1.0 + def test_head_start_rate_loads(self): rate = load_take_up_rate("head_start", 2022) assert 0 < rate <= 1 diff --git a/uv.lock b/uv.lock index 66f1ed9d7..a83f50df0 100644 --- a/uv.lock +++ b/uv.lock @@ -2164,7 +2164,7 @@ wheels = [ [[package]] name = "policyengine-us" -version = "1.715.3" +version = "1.719.0" 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/60/bc/ea8cf84d7653d4d76d1f7b05feb74722ff903637c616357610de1fd3b431/policyengine_us-1.715.3.tar.gz", hash = "sha256:5b41b22be90ef155a9440bcae7dd26115c887cad92ae8a51d9080a9692053b66", size = 10014788, upload-time = "2026-05-29T21:33:02.993Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/1c/de4b426cb63d7d1a1e4d1d47341e0e2a6200dc70cdf63e29a5ddfe192c47/policyengine_us-1.719.0.tar.gz", hash = "sha256:d0577cf40bc894364a10c5872343645747233c48baf8e521e43be8c2a1b3f828", size = 10060337, upload-time = "2026-06-01T15:39:38.511Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/0f/e6b594d46fffeb6e40db3a51441cec6a6e76ade2b178eab3836528dbc15c/policyengine_us-1.715.3-py3-none-any.whl", hash = "sha256:a34f305871f702d94f7a4d220bfd5312f11d83a417e793566892541871dfded3", size = 11037631, upload-time = "2026-05-29T21:32:59.464Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4b/c6eef8a64e38c6d591fe70847cf714cbbb5d0df64644c0928f56281fe7c9/policyengine_us-1.719.0-py3-none-any.whl", hash = "sha256:67af72ebb32191b061f063f5823c3d5bab597ae39825d0aeb8f08faa49125a62", size = 11163411, upload-time = "2026-06-01T15:39:33.111Z" }, ] [[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.715.3" }, + { name = "policyengine-us", specifier = "==1.719.0" }, { name = "requests", specifier = ">=2.25.0" }, { name = "scipy", specifier = ">=1.15.3" }, { name = "setuptools", specifier = ">=60" },