From 6ad29111c7d15ba59e8e10609618099ee56ff3b8 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:21:15 -0400 Subject: [PATCH 1/6] Rebase ACA marketplace ETL onto main --- .gitignore | 1 + Makefile | 2 +- changelog.d/618.added.md | 2 + .../calibration/target_config.yaml | 8 + .../db/create_field_valid_values.py | 2 +- .../db/etl_aca_marketplace.py | 223 +++++++++++++ ...marketplace_state_metal_selection_2024.csv | 307 ++++++++++++++++++ tests/unit/test_aca_marketplace_targets.py | 64 ++++ 8 files changed, 607 insertions(+), 2 deletions(-) create mode 100644 changelog.d/618.added.md create mode 100644 policyengine_us_data/db/etl_aca_marketplace.py create mode 100644 policyengine_us_data/storage/calibration_targets/aca_marketplace_state_metal_selection_2024.csv create mode 100644 tests/unit/test_aca_marketplace_targets.py diff --git a/.gitignore b/.gitignore index 9e85d7069..822da6e79 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ node_modules !age_state.csv !agi_state.csv !soi_targets.csv +!policyengine_us_data/storage/calibration_targets/aca_marketplace_state_metal_selection_2024.csv !policyengine_us_data/storage/social_security_aux.csv !policyengine_us_data/storage/SSPopJul_TR2024.csv !policyengine_us_data/storage/national_and_district_rents_2023.csv diff --git a/Makefile b/Makefile index f5234df02..e5b005acb 100644 --- a/Makefile +++ b/Makefile @@ -87,7 +87,7 @@ database: python policyengine_us_data/db/etl_tanf.py --year $(YEAR) python policyengine_us_data/db/etl_state_income_tax.py --year $(YEAR) python policyengine_us_data/db/etl_irs_soi.py --year $(YEAR) - python policyengine_us_data/db/etl_aca_agi_state_targets.py --year $(YEAR) + python policyengine_us_data/db/etl_aca_marketplace.py --year $(YEAR) python policyengine_us_data/db/etl_pregnancy.py --year $(YEAR) python policyengine_us_data/db/validate_database.py diff --git a/changelog.d/618.added.md b/changelog.d/618.added.md new file mode 100644 index 000000000..37b60f5d5 --- /dev/null +++ b/changelog.d/618.added.md @@ -0,0 +1,2 @@ +Add an ACA marketplace ETL that loads state-level HC.gov bronze-plan +selection targets for APTC recipients into the calibration database. diff --git a/policyengine_us_data/calibration/target_config.yaml b/policyengine_us_data/calibration/target_config.yaml index d4d627945..cdea0a649 100644 --- a/policyengine_us_data/calibration/target_config.yaml +++ b/policyengine_us_data/calibration/target_config.yaml @@ -59,6 +59,14 @@ include: geo_level: state domain_variable: tanf + # === STATE — ACA marketplace APTC and bronze-plan enrollment counts === + - variable: tax_unit_count + geo_level: state + domain_variable: used_aca_ptc + - variable: tax_unit_count + geo_level: state + domain_variable: selected_marketplace_plan_benchmark_ratio,used_aca_ptc + # === STATE — fine AGI bracket targets (stubs 9/10 from in55cmcsv) === - variable: person_count geo_level: state diff --git a/policyengine_us_data/db/create_field_valid_values.py b/policyengine_us_data/db/create_field_valid_values.py index 4dd1b54c6..5c71db58f 100644 --- a/policyengine_us_data/db/create_field_valid_values.py +++ b/policyengine_us_data/db/create_field_valid_values.py @@ -69,7 +69,7 @@ def populate_field_valid_values(session: Session) -> None: source_values = [ ("source", "Census ACS S0101", "survey"), ("source", "IRS SOI", "administrative"), - ("source", "CMS Marketplace", "administrative"), + ("source", "CMS 2024 OEP state metal status PUF", "administrative"), ("source", "CMS Medicaid", "administrative"), ("source", "Census ACS S2704", "survey"), ("source", "USDA FNS SNAP", "administrative"), diff --git a/policyengine_us_data/db/etl_aca_marketplace.py b/policyengine_us_data/db/etl_aca_marketplace.py new file mode 100644 index 000000000..bdf988874 --- /dev/null +++ b/policyengine_us_data/db/etl_aca_marketplace.py @@ -0,0 +1,223 @@ +from __future__ import annotations + +import logging +from pathlib import Path + +import pandas as pd +from sqlmodel import Session, create_engine + +from policyengine_us_data.calibration.calibration_utils import STATE_CODES +from policyengine_us_data.db.create_database_tables import ( + Stratum, + StratumConstraint, + Target, +) +from policyengine_us_data.storage import CALIBRATION_FOLDER, STORAGE_FOLDER +from policyengine_us_data.utils.db import etl_argparser, get_geographic_strata + +logger = logging.getLogger(__name__) + +# `selected_marketplace_plan_benchmark_ratio == 1.0` represents benchmark +# silver coverage, so bronze plan selections are the subset below this ratio. +BENCHMARK_SILVER_RATIO = 1.0 + +STATE_METAL_SELECTION_PATH = ( + CALIBRATION_FOLDER / "aca_marketplace_state_metal_selection_2024.csv" +) + +STATE_ABBR_TO_FIPS = {abbr: fips for fips, abbr in STATE_CODES.items()} + + +def _extra_args(parser) -> None: + parser.add_argument( + "--state-metal-csv", + type=Path, + default=STATE_METAL_SELECTION_PATH, + help=("State-metal CMS OEP proxy CSV. Default: %(default)s"), + ) + + +def extract_aca_marketplace_state_metal_data( + state_metal_csv_path: Path, +) -> pd.DataFrame: + """Extract CMS marketplace state metal-status inputs from the checked-in CSV. + + This ETL keeps an explicit extract step even though the source file already + lives in the repository. The original CMS 2024 OEP state metal status PUF + is not currently pulled from a stable direct-download endpoint in CI, so we + store the normalized input CSV at + `policyengine_us_data/storage/calibration_targets/aca_marketplace_state_metal_selection_2024.csv`. + + To reproduce or update that file: + 1. Download the CMS 2024 OEP state metal status public use file. + 2. Preserve one row per state/platform/metal/enrollment-status combination. + 3. Keep the `state_code`, `platform`, `metal_level`, + `enrollment_status`, `consumers`, and `aptc_consumers` columns. + 4. Save the normalized output back to `state_metal_csv_path`. + """ + return pd.read_csv(state_metal_csv_path) + + +def build_state_marketplace_bronze_aptc_targets( + state_metal_df: pd.DataFrame, +) -> pd.DataFrame: + """ + Build HC.gov state bronze-selection targets among APTC consumers. + + The 2024 CMS state-metal-status PUF exposes: + - metal rows (`B`, `G`, `S`) with enrollment_status=`All` + - aggregate rows (`All`) broken out by enrollment status (`01-atv`, etc.) + + We use: + - total APTC consumers = sum of `aptc_consumers` for `metal_level == All` + across enrollment statuses + - bronze APTC consumers = `aptc_consumers` on the bronze row + """ + df = state_metal_df.copy() + df = df[df["platform"] == "HC.gov"].copy() + + total_rows = df[ + (df["metal_level"] == "All") & (df["aptc_consumers"].notna()) + ].copy() + bronze_rows = df[ + (df["metal_level"] == "B") + & (df["enrollment_status"] == "All") + & (df["aptc_consumers"].notna()) + ].copy() + + total_aptc = total_rows.groupby("state_code", as_index=False).agg( + marketplace_aptc_consumers=("aptc_consumers", "sum"), + marketplace_consumers=("consumers", "sum"), + ) + bronze_aptc = bronze_rows[["state_code", "aptc_consumers", "consumers"]].rename( + columns={ + "aptc_consumers": "bronze_aptc_consumers", + "consumers": "bronze_consumers", + } + ) + + result = total_aptc.merge(bronze_aptc, on="state_code", how="inner") + result["state_fips"] = result["state_code"].map(STATE_ABBR_TO_FIPS) + result = result[result["state_fips"].notna()].copy() + result["state_fips"] = result["state_fips"].astype(int) + result["bronze_aptc_share"] = ( + result["bronze_aptc_consumers"] / result["marketplace_aptc_consumers"] + ) + result.insert(0, "year", 2024) + result.insert(1, "source", "cms_2024_oep_state_metal_status_puf") + return result.sort_values("state_code").reset_index(drop=True) + + +def load_state_marketplace_bronze_aptc_targets( + targets_df: pd.DataFrame, + year: int, +) -> None: + db_url = f"sqlite:///{STORAGE_FOLDER / 'calibration' / 'policy_data.db'}" + engine = create_engine(db_url) + + with Session(engine) as session: + geo_strata = get_geographic_strata(session) + + for row in targets_df.itertuples(index=False): + state_fips = int(row.state_fips) + parent_id = geo_strata["state"].get(state_fips) + if parent_id is None: + logger.warning( + "No state geographic stratum for FIPS %s, skipping", state_fips + ) + continue + + # We intentionally do not subset to `tax_unit_is_filer == 1`. + # These CMS targets describe marketplace coverage groups rather + # than the IRS filer universe, so the closest calibration entity is + # a tax unit with positive modeled APTC use. + aptc_stratum = Stratum( + parent_stratum_id=parent_id, + notes=f"State FIPS {state_fips} Marketplace APTC recipients", + ) + aptc_stratum.constraints_rel = [ + StratumConstraint( + constraint_variable="state_fips", + operation="==", + value=str(state_fips), + ), + StratumConstraint( + constraint_variable="used_aca_ptc", + operation=">", + value="0", + ), + ] + aptc_stratum.targets_rel.append( + Target( + # We use `tax_unit_count` rather than household/person + # counts because insurance groups map most closely to + # PolicyEngine tax units in the current calibration schema. + variable="tax_unit_count", + period=year, + value=float(row.marketplace_aptc_consumers), + active=True, + source="CMS 2024 OEP state metal status PUF", + notes="HC.gov APTC consumers across all enrollment statuses", + ) + ) + session.add(aptc_stratum) + session.flush() + + bronze_stratum = Stratum( + parent_stratum_id=aptc_stratum.stratum_id, + notes=f"State FIPS {state_fips} Marketplace bronze APTC recipients", + ) + bronze_stratum.constraints_rel = [ + StratumConstraint( + constraint_variable="state_fips", + operation="==", + value=str(state_fips), + ), + StratumConstraint( + constraint_variable="used_aca_ptc", + operation=">", + value="0", + ), + StratumConstraint( + constraint_variable="selected_marketplace_plan_benchmark_ratio", + operation="<", + value=str(BENCHMARK_SILVER_RATIO), + ), + ] + bronze_stratum.targets_rel.append( + Target( + variable="tax_unit_count", + period=year, + value=float(row.bronze_aptc_consumers), + active=True, + source="CMS 2024 OEP state metal status PUF", + notes="HC.gov bronze plan selections among APTC consumers", + ) + ) + session.add(bronze_stratum) + session.flush() + + session.commit() + + +def main() -> None: + args, year = etl_argparser( + "ETL for ACA marketplace bronze-selection calibration targets", + extra_args_fn=_extra_args, + ) + + state_metal = extract_aca_marketplace_state_metal_data(args.state_metal_csv) + targets_df = build_state_marketplace_bronze_aptc_targets(state_metal) + if targets_df.empty: + raise RuntimeError("No HC.gov marketplace bronze/APTC targets were generated.") + + print( + "Loading ACA marketplace bronze/APTC state targets for " + f"{len(targets_df)} states from {args.state_metal_csv}" + ) + load_state_marketplace_bronze_aptc_targets(targets_df, year) + print("ACA marketplace bronze/APTC targets loaded.") + + +if __name__ == "__main__": + main() diff --git a/policyengine_us_data/storage/calibration_targets/aca_marketplace_state_metal_selection_2024.csv b/policyengine_us_data/storage/calibration_targets/aca_marketplace_state_metal_selection_2024.csv new file mode 100644 index 000000000..a00a3296c --- /dev/null +++ b/policyengine_us_data/storage/calibration_targets/aca_marketplace_state_metal_selection_2024.csv @@ -0,0 +1,307 @@ +year,source,state_code,platform,metal_level,enrollment_status,consumers,avg_selected_premium,avg_selected_net_premium,selected_lte10_share,selected_lte10_consumers,aptc_consumers,aptc_share,avg_aptc +2024,cms_2024_oep_state_metal_status_puf,AK,HC.gov,All,01-atv,15495.0,987.0,209.0,0.25,3873.75,13790.550000000001,0.89,879.0 +2024,cms_2024_oep_state_metal_status_puf,AK,HC.gov,All,02-aut,5886.0,988.0,297.0,0.25,1471.5,4591.08,0.78,891.0 +2024,cms_2024_oep_state_metal_status_puf,AK,HC.gov,All,03-new,6083.0,897.0,216.0,0.29,1764.07,5170.55,0.85,804.0 +2024,cms_2024_oep_state_metal_status_puf,AK,HC.gov,B,All,11197.0,777.0,249.0,0.32,3583.04,8733.66,0.78,680.0 +2024,cms_2024_oep_state_metal_status_puf,AK,HC.gov,G,All,12546.0,1074.0,246.0,0.17,2132.82,11165.94,0.89,932.0 +2024,cms_2024_oep_state_metal_status_puf,AK,HC.gov,S,All,3721.0,1181.0,113.0,0.33,1227.93,3609.37,0.97,1102.0 +2024,cms_2024_oep_state_metal_status_puf,AL,HC.gov,All,01-atv,170472.0,762.0,81.0,0.47,80121.84,167062.56,0.98,698.0 +2024,cms_2024_oep_state_metal_status_puf,AL,HC.gov,All,02-aut,105451.0,691.0,94.0,0.49,51670.99,99123.93999999999,0.94,639.0 +2024,cms_2024_oep_state_metal_status_puf,AL,HC.gov,All,03-new,110272.0,632.0,49.0,0.69,76087.68,105861.12,0.96,607.0 +2024,cms_2024_oep_state_metal_status_puf,AL,HC.gov,B,All,90440.0,579.0,63.0,0.72,65116.799999999996,85013.59999999999,0.94,551.0 +2024,cms_2024_oep_state_metal_status_puf,AL,HC.gov,G,All,10672.0,911.0,476.0,0.0,0.0,8857.76,0.83,522.0 +2024,cms_2024_oep_state_metal_status_puf,AL,HC.gov,S,All,283062.0,741.0,62.0,0.5,141531.0,277400.76,0.98,692.0 +2024,cms_2024_oep_state_metal_status_puf,AR,HC.gov,All,01-atv,56525.0,605.0,130.0,0.26,14696.5,53133.5,0.94,507.0 +2024,cms_2024_oep_state_metal_status_puf,AR,HC.gov,All,02-aut,46955.0,557.0,136.0,0.23,10799.65,42259.5,0.9,467.0 +2024,cms_2024_oep_state_metal_status_puf,AR,HC.gov,All,03-new,53127.0,502.0,84.0,0.53,28157.31,49408.11,0.93,451.0 +2024,cms_2024_oep_state_metal_status_puf,AR,HC.gov,B,All,37722.0,525.0,164.0,0.24,9053.279999999999,32063.7,0.85,423.0 +2024,cms_2024_oep_state_metal_status_puf,AR,HC.gov,G,All,5950.0,656.0,410.0,0.0,0.0,4165.0,0.7,350.0 +2024,cms_2024_oep_state_metal_status_puf,AR,HC.gov,S,All,112935.0,561.0,84.0,0.39,44044.65,108417.59999999999,0.96,497.0 +2024,cms_2024_oep_state_metal_status_puf,AZ,HC.gov,All,01-atv,148318.0,561.0,133.0,0.37,54877.659999999996,133486.2,0.9,474.0 +2024,cms_2024_oep_state_metal_status_puf,AZ,HC.gov,All,02-aut,97021.0,517.0,138.0,0.36,34927.56,82467.84999999999,0.85,446.0 +2024,cms_2024_oep_state_metal_status_puf,AZ,HC.gov,All,03-new,102716.0,473.0,86.0,0.58,59575.28,93471.56,0.91,426.0 +2024,cms_2024_oep_state_metal_status_puf,AZ,HC.gov,B,All,128534.0,480.0,121.0,0.47,60410.979999999996,109253.9,0.85,421.0 +2024,cms_2024_oep_state_metal_status_puf,AZ,HC.gov,G,All,17461.0,628.0,435.0,0.0,0.0,10825.82,0.62,312.0 +2024,cms_2024_oep_state_metal_status_puf,AZ,HC.gov,S,All,201802.0,541.0,93.0,0.44,88792.88,189693.87999999998,0.94,478.0 +2024,cms_2024_oep_state_metal_status_puf,CA,SBM,All,01-atv,455635.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CA,SBM,All,02-aut,1022636.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CA,SBM,All,03-new,306382.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CA,SBM,B,All,460034.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CA,SBM,G,All,159969.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CA,SBM,S,All,1079327.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CO,SBM,All,01-atv,110114.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CO,SBM,All,02-aut,78857.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CO,SBM,All,03-new,48135.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CO,SBM,B,All,96052.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CO,SBM,G,All,65197.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CO,SBM,S,All,73920.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CT,SBM,All,01-atv,31375.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CT,SBM,All,02-aut,69032.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CT,SBM,All,03-new,28593.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CT,SBM,B,All,35648.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CT,SBM,G,All,22489.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,CT,SBM,S,All,69287.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,DC,SBM,All,01-atv,1460.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,DC,SBM,All,02-aut,10634.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,DC,SBM,All,03-new,2705.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,DC,SBM,B,All,4565.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,DC,SBM,G,All,4168.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,DC,SBM,S,All,3207.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,DE,HC.gov,All,01-atv,20364.0,763.0,205.0,0.16,3258.2400000000002,18734.88,0.92,605.0 +2024,cms_2024_oep_state_metal_status_puf,DE,HC.gov,All,02-aut,13760.0,718.0,225.0,0.07,963.2,11833.6,0.86,574.0 +2024,cms_2024_oep_state_metal_status_puf,DE,HC.gov,All,03-new,10718.0,654.0,141.0,0.35,3751.2999999999997,9860.560000000001,0.92,561.0 +2024,cms_2024_oep_state_metal_status_puf,DE,HC.gov,B,All,11587.0,623.0,187.0,0.28,3244.36,9848.949999999999,0.85,510.0 +2024,cms_2024_oep_state_metal_status_puf,DE,HC.gov,G,All,19508.0,769.0,266.0,0.11,2145.88,17362.12,0.89,562.0 +2024,cms_2024_oep_state_metal_status_puf,DE,HC.gov,S,All,13061.0,738.0,77.0,0.2,2612.2000000000003,12669.17,0.97,679.0 +2024,cms_2024_oep_state_metal_status_puf,FL,HC.gov,All,01-atv,2390303.0,650.0,63.0,0.45,1075636.35,2342496.94,0.98,597.0 +2024,cms_2024_oep_state_metal_status_puf,FL,HC.gov,All,02-aut,812170.0,612.0,92.0,0.44,357354.8,763439.7999999999,0.94,552.0 +2024,cms_2024_oep_state_metal_status_puf,FL,HC.gov,All,03-new,1009429.0,546.0,49.0,0.63,635940.27,979146.13,0.97,511.0 +2024,cms_2024_oep_state_metal_status_puf,FL,HC.gov,B,All,1292350.0,584.0,94.0,0.56,723716.0000000001,1227732.5,0.95,518.0 +2024,cms_2024_oep_state_metal_status_puf,FL,HC.gov,G,All,226291.0,659.0,183.0,0.38,85990.58,205924.81,0.91,523.0 +2024,cms_2024_oep_state_metal_status_puf,FL,HC.gov,S,All,2674179.0,625.0,34.0,0.47,1256864.13,2647437.21,0.99,595.0 +2024,cms_2024_oep_state_metal_status_puf,GA,HC.gov,All,01-atv,675563.0,607.0,67.0,0.56,378315.28,655296.11,0.97,559.0 +2024,cms_2024_oep_state_metal_status_puf,GA,HC.gov,All,02-aut,278876.0,596.0,120.0,0.28,78085.28000000001,256565.92,0.92,516.0 +2024,cms_2024_oep_state_metal_status_puf,GA,HC.gov,All,03-new,350675.0,525.0,55.0,0.66,231445.5,336648.0,0.96,490.0 +2024,cms_2024_oep_state_metal_status_puf,GA,HC.gov,B,All,351039.0,538.0,83.0,0.6,210623.4,326466.27,0.93,492.0 +2024,cms_2024_oep_state_metal_status_puf,GA,HC.gov,G,All,79691.0,673.0,344.0,0.0,0.0,61362.07,0.77,426.0 +2024,cms_2024_oep_state_metal_status_puf,GA,HC.gov,S,All,870231.0,593.0,45.0,0.54,469924.74000000005,861528.69,0.99,554.0 +2024,cms_2024_oep_state_metal_status_puf,HI,HC.gov,All,01-atv,11238.0,738.0,238.0,0.11,1236.18,9889.44,0.88,568.0 +2024,cms_2024_oep_state_metal_status_puf,HI,HC.gov,All,02-aut,6823.0,679.0,273.0,0.09,614.0699999999999,5117.25,0.75,539.0 +2024,cms_2024_oep_state_metal_status_puf,HI,HC.gov,All,03-new,4109.0,612.0,235.0,0.14,575.2600000000001,3246.11,0.79,479.0 +2024,cms_2024_oep_state_metal_status_puf,HI,HC.gov,B,All,5691.0,557.0,229.0,0.15,853.65,4268.25,0.75,440.0 +2024,cms_2024_oep_state_metal_status_puf,HI,HC.gov,G,All,6620.0,735.0,285.0,0.0,0.0,5494.599999999999,0.83,541.0 +2024,cms_2024_oep_state_metal_status_puf,HI,HC.gov,S,All,6075.0,726.0,104.0,0.26,1579.5,5892.75,0.97,642.0 +2024,cms_2024_oep_state_metal_status_puf,IA,HC.gov,All,01-atv,50477.0,621.0,136.0,0.31,15647.869999999999,46438.840000000004,0.92,529.0 +2024,cms_2024_oep_state_metal_status_puf,IA,HC.gov,All,02-aut,33404.0,582.0,152.0,0.3,10021.199999999999,28727.44,0.86,500.0 +2024,cms_2024_oep_state_metal_status_puf,IA,HC.gov,All,03-new,27542.0,525.0,100.0,0.49,13495.58,24787.8,0.9,474.0 +2024,cms_2024_oep_state_metal_status_puf,IA,HC.gov,B,All,41603.0,510.0,131.0,0.38,15809.14,35778.58,0.86,441.0 +2024,cms_2024_oep_state_metal_status_puf,IA,HC.gov,G,All,35246.0,645.0,217.0,0.09,3172.14,29959.1,0.85,504.0 +2024,cms_2024_oep_state_metal_status_puf,IA,HC.gov,S,All,34482.0,616.0,45.0,0.58,19999.559999999998,34137.18,0.99,579.0 +2024,cms_2024_oep_state_metal_status_puf,ID,SBM,All,01-atv,23266.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ID,SBM,All,02-aut,62148.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ID,SBM,All,03-new,18369.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ID,SBM,B,All,53715.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ID,SBM,G,All,11350.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ID,SBM,S,All,38244.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,IL,HC.gov,All,01-atv,186428.0,733.0,197.0,0.18,33557.04,171513.76,0.92,583.0 +2024,cms_2024_oep_state_metal_status_puf,IL,HC.gov,All,02-aut,95949.0,692.0,243.0,0.16,15351.84,78678.18,0.82,549.0 +2024,cms_2024_oep_state_metal_status_puf,IL,HC.gov,All,03-new,116437.0,570.0,133.0,0.41,47739.17,105957.67,0.91,482.0 +2024,cms_2024_oep_state_metal_status_puf,IL,HC.gov,B,All,150128.0,654.0,214.0,0.23,34529.44,126107.51999999999,0.84,526.0 +2024,cms_2024_oep_state_metal_status_puf,IL,HC.gov,G,All,29117.0,858.0,509.0,0.0,0.0,20673.07,0.71,493.0 +2024,cms_2024_oep_state_metal_status_puf,IL,HC.gov,S,All,218068.0,665.0,127.0,0.29,63239.719999999994,209345.28,0.96,562.0 +2024,cms_2024_oep_state_metal_status_puf,IN,HC.gov,All,01-atv,134626.0,560.0,139.0,0.38,51157.88,121163.40000000001,0.9,466.0 +2024,cms_2024_oep_state_metal_status_puf,IN,HC.gov,All,02-aut,71094.0,539.0,168.0,0.25,17773.5,59008.02,0.83,447.0 +2024,cms_2024_oep_state_metal_status_puf,IN,HC.gov,All,03-new,90052.0,484.0,85.0,0.58,52230.159999999996,82847.84,0.92,434.0 +2024,cms_2024_oep_state_metal_status_puf,IN,HC.gov,B,All,116178.0,507.0,151.0,0.4,46471.200000000004,97589.51999999999,0.84,422.0 +2024,cms_2024_oep_state_metal_status_puf,IN,HC.gov,G,All,8491.0,687.0,510.0,0.0,0.0,4839.87,0.57,313.0 +2024,cms_2024_oep_state_metal_status_puf,IN,HC.gov,S,All,171103.0,541.0,96.0,0.43,73574.29,160836.81999999998,0.94,474.0 +2024,cms_2024_oep_state_metal_status_puf,KS,HC.gov,All,01-atv,85913.0,672.0,115.0,0.42,36083.46,81617.34999999999,0.95,588.0 +2024,cms_2024_oep_state_metal_status_puf,KS,HC.gov,All,02-aut,40115.0,631.0,136.0,0.33,13237.95,36103.5,0.9,552.0 +2024,cms_2024_oep_state_metal_status_puf,KS,HC.gov,All,03-new,45348.0,563.0,80.0,0.57,25848.359999999997,42627.119999999995,0.94,517.0 +2024,cms_2024_oep_state_metal_status_puf,KS,HC.gov,B,All,59577.0,574.0,121.0,0.46,27405.420000000002,53619.3,0.9,506.0 +2024,cms_2024_oep_state_metal_status_puf,KS,HC.gov,G,All,17909.0,775.0,348.0,0.02,358.18,15043.56,0.84,511.0 +2024,cms_2024_oep_state_metal_status_puf,KS,HC.gov,S,All,93773.0,645.0,59.0,0.5,46886.5,91897.54,0.98,601.0 +2024,cms_2024_oep_state_metal_status_puf,KY,SBM,All,01-atv,10560.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,KY,SBM,All,02-aut,45997.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,KY,SBM,All,03-new,18760.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,KY,SBM,B,All,30668.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,KY,SBM,G,All,6703.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,KY,SBM,S,All,37430.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,LA,HC.gov,All,01-atv,80677.0,759.0,96.0,0.51,41145.270000000004,78256.69,0.97,681.0 +2024,cms_2024_oep_state_metal_status_puf,LA,HC.gov,All,02-aut,62841.0,708.0,118.0,0.51,32048.91,58442.130000000005,0.93,636.0 +2024,cms_2024_oep_state_metal_status_puf,LA,HC.gov,All,03-new,68975.0,655.0,57.0,0.71,48972.25,66905.75,0.97,616.0 +2024,cms_2024_oep_state_metal_status_puf,LA,HC.gov,B,All,94148.0,646.0,72.0,0.66,62137.68,89440.59999999999,0.95,603.0 +2024,cms_2024_oep_state_metal_status_puf,LA,HC.gov,G,All,12301.0,859.0,389.0,0.0,0.0,10824.88,0.88,533.0 +2024,cms_2024_oep_state_metal_status_puf,LA,HC.gov,S,All,106044.0,749.0,71.0,0.57,60445.079999999994,102862.68,0.97,697.0 +2024,cms_2024_oep_state_metal_status_puf,MA,SBM,All,01-atv,49276.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MA,SBM,All,02-aut,196010.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MA,SBM,All,03-new,65913.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MA,SBM,B,All,27780.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MA,SBM,G,All,8512.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MA,SBM,S,All,271998.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MD,SBM,All,01-atv,33919.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MD,SBM,All,02-aut,123756.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MD,SBM,All,03-new,56220.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MD,SBM,B,All,50695.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MD,SBM,G,All,102286.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MD,SBM,S,All,55420.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ME,SBM,All,01-atv,19528.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ME,SBM,All,02-aut,32813.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ME,SBM,All,03-new,10245.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ME,SBM,B,All,28427.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ME,SBM,G,All,5716.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,ME,SBM,S,All,27456.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MI,HC.gov,All,01-atv,195516.0,552.0,143.0,0.27,52789.32000000001,179874.72,0.92,445.0 +2024,cms_2024_oep_state_metal_status_puf,MI,HC.gov,All,02-aut,112285.0,517.0,163.0,0.31,34808.35,94319.4,0.84,423.0 +2024,cms_2024_oep_state_metal_status_puf,MI,HC.gov,All,03-new,110299.0,459.0,99.0,0.47,51840.53,100372.09,0.91,394.0 +2024,cms_2024_oep_state_metal_status_puf,MI,HC.gov,B,All,191315.0,481.0,147.0,0.35,66960.25,162617.75,0.85,391.0 +2024,cms_2024_oep_state_metal_status_puf,MI,HC.gov,G,All,22440.0,578.0,328.0,0.01,224.4,16605.6,0.74,337.0 +2024,cms_2024_oep_state_metal_status_puf,MI,HC.gov,S,All,202535.0,548.0,105.0,0.36,72912.59999999999,194433.6,0.96,463.0 +2024,cms_2024_oep_state_metal_status_puf,MN,SBM,All,01-atv,15663.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MN,SBM,All,02-aut,83132.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MN,SBM,All,03-new,36206.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MN,SBM,B,All,63323.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MN,SBM,G,All,25693.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MN,SBM,S,All,42578.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,MO,HC.gov,All,01-atv,180002.0,686.0,96.0,0.48,86400.95999999999,171001.9,0.95,621.0 +2024,cms_2024_oep_state_metal_status_puf,MO,HC.gov,All,02-aut,85764.0,637.0,104.0,0.45,38593.8,78045.24,0.91,587.0 +2024,cms_2024_oep_state_metal_status_puf,MO,HC.gov,All,03-new,93603.0,592.0,72.0,0.62,58033.86,88922.84999999999,0.95,548.0 +2024,cms_2024_oep_state_metal_status_puf,MO,HC.gov,B,All,147847.0,603.0,93.0,0.59,87229.73,136019.24000000002,0.92,551.0 +2024,cms_2024_oep_state_metal_status_puf,MO,HC.gov,G,All,14267.0,746.0,410.0,0.0,0.0,10414.91,0.73,462.0 +2024,cms_2024_oep_state_metal_status_puf,MO,HC.gov,S,All,196476.0,680.0,67.0,0.49,96273.24,190581.72,0.97,632.0 +2024,cms_2024_oep_state_metal_status_puf,MS,HC.gov,All,01-atv,142275.0,645.0,30.0,0.67,95324.25,140852.25,0.99,621.0 +2024,cms_2024_oep_state_metal_status_puf,MS,HC.gov,All,02-aut,61379.0,612.0,58.0,0.52,31917.08,58923.84,0.96,578.0 +2024,cms_2024_oep_state_metal_status_puf,MS,HC.gov,All,03-new,82756.0,570.0,29.0,0.73,60411.88,81100.88,0.98,551.0 +2024,cms_2024_oep_state_metal_status_puf,MS,HC.gov,B,All,72106.0,605.0,31.0,0.81,58405.86,69942.81999999999,0.97,589.0 +2024,cms_2024_oep_state_metal_status_puf,MS,HC.gov,G,All,3157.0,747.0,444.0,0.0,0.0,2209.8999999999996,0.7,434.0 +2024,cms_2024_oep_state_metal_status_puf,MS,HC.gov,S,All,211147.0,618.0,31.0,0.61,128799.67,209035.53,0.99,595.0 +2024,cms_2024_oep_state_metal_status_puf,MT,HC.gov,All,01-atv,32914.0,643.0,165.0,0.25,8228.5,29622.600000000002,0.9,530.0 +2024,cms_2024_oep_state_metal_status_puf,MT,HC.gov,All,02-aut,17769.0,596.0,194.0,0.3,5330.7,14392.890000000001,0.81,498.0 +2024,cms_2024_oep_state_metal_status_puf,MT,HC.gov,All,03-new,15653.0,530.0,121.0,0.39,6104.67,14087.7,0.9,456.0 +2024,cms_2024_oep_state_metal_status_puf,MT,HC.gov,B,All,39115.0,555.0,161.0,0.32,12516.800000000001,33247.75,0.85,462.0 +2024,cms_2024_oep_state_metal_status_puf,MT,HC.gov,G,All,6540.0,716.0,373.0,0.0,0.0,5166.6,0.79,432.0 +2024,cms_2024_oep_state_metal_status_puf,MT,HC.gov,S,All,20377.0,668.0,96.0,0.34,6928.18,19561.92,0.96,594.0 +2024,cms_2024_oep_state_metal_status_puf,NC,HC.gov,All,01-atv,520788.0,628.0,73.0,0.56,291641.28,505164.36,0.97,574.0 +2024,cms_2024_oep_state_metal_status_puf,NC,HC.gov,All,02-aut,288807.0,614.0,95.0,0.4,115522.8,268590.51,0.93,560.0 +2024,cms_2024_oep_state_metal_status_puf,NC,HC.gov,All,03-new,218335.0,555.0,61.0,0.65,141917.75,209601.6,0.96,517.0 +2024,cms_2024_oep_state_metal_status_puf,NC,HC.gov,B,All,421708.0,551.0,80.0,0.59,248807.72,396405.51999999996,0.94,504.0 +2024,cms_2024_oep_state_metal_status_puf,NC,HC.gov,G,All,75825.0,695.0,287.0,0.0,0.0,65209.5,0.86,473.0 +2024,cms_2024_oep_state_metal_status_puf,NC,HC.gov,S,All,527815.0,644.0,43.0,0.57,300854.55,522536.85,0.99,610.0 +2024,cms_2024_oep_state_metal_status_puf,ND,HC.gov,All,01-atv,21019.0,538.0,127.0,0.29,6095.509999999999,19337.48,0.92,446.0 +2024,cms_2024_oep_state_metal_status_puf,ND,HC.gov,All,02-aut,10502.0,513.0,149.0,0.3,3150.6,8926.699999999999,0.85,430.0 +2024,cms_2024_oep_state_metal_status_puf,ND,HC.gov,All,03-new,7014.0,476.0,124.0,0.33,2314.62,6172.32,0.88,398.0 +2024,cms_2024_oep_state_metal_status_puf,ND,HC.gov,B,All,14616.0,440.0,133.0,0.39,5700.24,12277.439999999999,0.84,365.0 +2024,cms_2024_oep_state_metal_status_puf,ND,HC.gov,G,All,14056.0,588.0,186.0,0.1,1405.6000000000001,12931.52,0.92,440.0 +2024,cms_2024_oep_state_metal_status_puf,ND,HC.gov,S,All,9520.0,555.0,51.0,0.47,4474.4,9329.6,0.98,515.0 +2024,cms_2024_oep_state_metal_status_puf,NE,HC.gov,All,01-atv,71040.0,690.0,123.0,0.33,23443.2,68198.4,0.96,591.0 +2024,cms_2024_oep_state_metal_status_puf,NE,HC.gov,All,02-aut,20710.0,670.0,136.0,0.42,8698.199999999999,18846.100000000002,0.91,586.0 +2024,cms_2024_oep_state_metal_status_puf,NE,HC.gov,All,03-new,26132.0,616.0,101.0,0.44,11498.08,24564.079999999998,0.94,546.0 +2024,cms_2024_oep_state_metal_status_puf,NE,HC.gov,B,All,69146.0,627.0,130.0,0.36,24892.559999999998,64305.780000000006,0.93,533.0 +2024,cms_2024_oep_state_metal_status_puf,NE,HC.gov,G,All,12064.0,729.0,258.0,0.06,723.8399999999999,10857.6,0.9,522.0 +2024,cms_2024_oep_state_metal_status_puf,NE,HC.gov,S,All,36639.0,731.0,57.0,0.5,18319.5,36272.61,0.99,683.0 +2024,cms_2024_oep_state_metal_status_puf,NH,HC.gov,All,01-atv,29901.0,483.0,207.0,0.16,4784.16,22425.75,0.75,366.0 +2024,cms_2024_oep_state_metal_status_puf,NH,HC.gov,All,02-aut,21507.0,440.0,214.0,0.19,4086.33,13979.550000000001,0.65,346.0 +2024,cms_2024_oep_state_metal_status_puf,NH,HC.gov,All,03-new,13709.0,408.0,169.0,0.23,3153.07,10144.66,0.74,322.0 +2024,cms_2024_oep_state_metal_status_puf,NH,HC.gov,B,All,22137.0,404.0,218.0,0.15,3320.5499999999997,13946.31,0.63,294.0 +2024,cms_2024_oep_state_metal_status_puf,NH,HC.gov,G,All,13377.0,486.0,331.0,0.0,0.0,7491.120000000001,0.56,277.0 +2024,cms_2024_oep_state_metal_status_puf,NH,HC.gov,S,All,29149.0,480.0,130.0,0.3,8744.699999999999,25359.63,0.87,404.0 +2024,cms_2024_oep_state_metal_status_puf,NJ,SBM,All,01-atv,60592.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NJ,SBM,All,02-aut,236431.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NJ,SBM,All,03-new,100919.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NJ,SBM,B,All,67074.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NJ,SBM,G,All,5607.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NJ,SBM,S,All,324273.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NM,SBM,All,01-atv,20654.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NM,SBM,All,02-aut,24470.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NM,SBM,All,03-new,11348.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NM,SBM,B,All,2418.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NM,SBM,G,All,40540.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NM,SBM,S,All,13514.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NV,SBM,All,01-atv,22965.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NV,SBM,All,02-aut,50794.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NV,SBM,All,03-new,25553.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NV,SBM,B,All,36499.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NV,SBM,G,All,4475.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NV,SBM,S,All,58066.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NY,SBM,All,01-atv,88241.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NY,SBM,All,02-aut,162218.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NY,SBM,All,03-new,38222.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NY,SBM,B,All,135986.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NY,SBM,G,All,28060.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,NY,SBM,S,All,105329.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,OH,HC.gov,All,01-atv,219367.0,611.0,142.0,0.42,92134.14,199623.97,0.91,516.0 +2024,cms_2024_oep_state_metal_status_puf,OH,HC.gov,All,02-aut,116533.0,592.0,184.0,0.26,30298.58,96722.39,0.83,492.0 +2024,cms_2024_oep_state_metal_status_puf,OH,HC.gov,All,03-new,141893.0,527.0,89.0,0.59,83716.87,130541.56000000001,0.92,476.0 +2024,cms_2024_oep_state_metal_status_puf,OH,HC.gov,B,All,241564.0,548.0,142.0,0.49,118366.36,210160.68,0.87,468.0 +2024,cms_2024_oep_state_metal_status_puf,OH,HC.gov,G,All,22545.0,707.0,427.0,0.0,0.0,15781.499999999998,0.7,402.0 +2024,cms_2024_oep_state_metal_status_puf,OH,HC.gov,S,All,211342.0,610.0,98.0,0.42,88763.64,200774.9,0.95,537.0 +2024,cms_2024_oep_state_metal_status_puf,OK,HC.gov,All,01-atv,133624.0,652.0,73.0,0.58,77501.92,130951.52,0.98,592.0 +2024,cms_2024_oep_state_metal_status_puf,OK,HC.gov,All,02-aut,75025.0,637.0,112.0,0.42,31510.5,69773.25,0.93,566.0 +2024,cms_2024_oep_state_metal_status_puf,OK,HC.gov,All,03-new,68787.0,587.0,55.0,0.68,46775.16,66723.39,0.97,550.0 +2024,cms_2024_oep_state_metal_status_puf,OK,HC.gov,B,All,128536.0,564.0,64.0,0.65,83548.40000000001,123394.56,0.96,523.0 +2024,cms_2024_oep_state_metal_status_puf,OK,HC.gov,G,All,39039.0,717.0,194.0,0.26,10150.140000000001,36306.270000000004,0.93,564.0 +2024,cms_2024_oep_state_metal_status_puf,OK,HC.gov,S,All,109543.0,682.0,55.0,0.57,62439.509999999995,107352.14,0.98,637.0 +2024,cms_2024_oep_state_metal_status_puf,OR,HC.gov,All,01-atv,83751.0,693.0,233.0,0.14,11725.140000000001,70350.84,0.84,547.0 +2024,cms_2024_oep_state_metal_status_puf,OR,HC.gov,All,02-aut,31798.0,632.0,258.0,0.15,4769.7,22576.579999999998,0.71,524.0 +2024,cms_2024_oep_state_metal_status_puf,OR,HC.gov,All,03-new,29960.0,575.0,198.0,0.18,5392.8,24567.199999999997,0.82,458.0 +2024,cms_2024_oep_state_metal_status_puf,OR,HC.gov,B,All,65644.0,564.0,215.0,0.2,13128.800000000001,49889.44,0.76,458.0 +2024,cms_2024_oep_state_metal_status_puf,OR,HC.gov,G,All,23991.0,709.0,411.0,0.0,0.0,16793.7,0.7,428.0 +2024,cms_2024_oep_state_metal_status_puf,OR,HC.gov,S,All,55874.0,740.0,174.0,0.16,8939.84,50845.340000000004,0.91,620.0 +2024,cms_2024_oep_state_metal_status_puf,PA,SBM,All,01-atv,77309.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,PA,SBM,All,02-aut,267517.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,PA,SBM,All,03-new,89745.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,PA,SBM,B,All,100226.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,PA,SBM,G,All,184773.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,PA,SBM,S,All,148314.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,RI,SBM,All,01-atv,2928.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,RI,SBM,All,02-aut,23029.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,RI,SBM,All,03-new,10164.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,RI,SBM,B,All,7886.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,RI,SBM,G,All,13021.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,RI,SBM,S,All,14350.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,SC,HC.gov,All,01-atv,307184.0,627.0,70.0,0.59,181238.56,297968.48,0.97,575.0 +2024,cms_2024_oep_state_metal_status_puf,SC,HC.gov,All,02-aut,126543.0,586.0,83.0,0.51,64536.93,117684.99,0.93,539.0 +2024,cms_2024_oep_state_metal_status_puf,SC,HC.gov,All,03-new,137448.0,550.0,56.0,0.67,92090.16,130575.59999999999,0.95,517.0 +2024,cms_2024_oep_state_metal_status_puf,SC,HC.gov,B,All,293049.0,545.0,62.0,0.67,196342.83000000002,278396.55,0.95,507.0 +2024,cms_2024_oep_state_metal_status_puf,SC,HC.gov,G,All,25527.0,753.0,341.0,0.0,0.0,21697.95,0.85,486.0 +2024,cms_2024_oep_state_metal_status_puf,SC,HC.gov,S,All,250303.0,650.0,48.0,0.57,142672.71,245296.94,0.98,612.0 +2024,cms_2024_oep_state_metal_status_puf,SD,HC.gov,All,01-atv,30112.0,721.0,108.0,0.36,10840.32,29208.64,0.97,632.0 +2024,cms_2024_oep_state_metal_status_puf,SD,HC.gov,All,02-aut,13325.0,686.0,122.0,0.38,5063.5,12259.0,0.92,612.0 +2024,cms_2024_oep_state_metal_status_puf,SD,HC.gov,All,03-new,9537.0,626.0,122.0,0.35,3337.95,8964.779999999999,0.94,539.0 +2024,cms_2024_oep_state_metal_status_puf,SD,HC.gov,B,All,25929.0,625.0,120.0,0.4,10371.6,24373.26,0.94,539.0 +2024,cms_2024_oep_state_metal_status_puf,SD,HC.gov,G,All,6305.0,788.0,226.0,0.02,126.10000000000001,5989.75,0.95,593.0 +2024,cms_2024_oep_state_metal_status_puf,SD,HC.gov,S,All,20464.0,762.0,70.0,0.42,8594.88,20259.36,0.99,702.0 +2024,cms_2024_oep_state_metal_status_puf,TN,HC.gov,All,01-atv,273806.0,653.0,71.0,0.59,161545.53999999998,262853.76,0.96,606.0 +2024,cms_2024_oep_state_metal_status_puf,TN,HC.gov,All,02-aut,127243.0,617.0,97.0,0.5,63621.5,115791.13,0.91,572.0 +2024,cms_2024_oep_state_metal_status_puf,TN,HC.gov,All,03-new,154054.0,573.0,57.0,0.68,104756.72,146351.3,0.95,542.0 +2024,cms_2024_oep_state_metal_status_puf,TN,HC.gov,B,All,223916.0,578.0,78.0,0.66,147784.56,208241.88,0.93,540.0 +2024,cms_2024_oep_state_metal_status_puf,TN,HC.gov,G,All,14399.0,749.0,348.0,0.07,1007.9300000000001,11375.210000000001,0.79,509.0 +2024,cms_2024_oep_state_metal_status_puf,TN,HC.gov,S,All,316386.0,648.0,57.0,0.58,183503.87999999998,306894.42,0.97,610.0 +2024,cms_2024_oep_state_metal_status_puf,TX,HC.gov,All,01-atv,1821679.0,596.0,44.0,0.65,1184091.35,1785245.42,0.98,565.0 +2024,cms_2024_oep_state_metal_status_puf,TX,HC.gov,All,02-aut,705873.0,563.0,80.0,0.4,282349.2,656461.89,0.93,520.0 +2024,cms_2024_oep_state_metal_status_puf,TX,HC.gov,All,03-new,957080.0,513.0,41.0,0.72,689097.6,918796.7999999999,0.96,491.0 +2024,cms_2024_oep_state_metal_status_puf,TX,HC.gov,B,All,570001.0,483.0,75.0,0.65,370500.65,518700.91000000003,0.91,449.0 +2024,cms_2024_oep_state_metal_status_puf,TX,HC.gov,G,All,1189779.0,569.0,65.0,0.62,737662.98,1130290.05,0.95,528.0 +2024,cms_2024_oep_state_metal_status_puf,TX,HC.gov,S,All,1722670.0,593.0,32.0,0.61,1050828.7,1705443.3,0.99,567.0 +2024,cms_2024_oep_state_metal_status_puf,UT,HC.gov,All,01-atv,229494.0,469.0,63.0,0.5,114747.0,220314.24,0.96,422.0 +2024,cms_2024_oep_state_metal_status_puf,UT,HC.gov,All,02-aut,59764.0,487.0,88.0,0.41,24503.239999999998,54982.880000000005,0.92,435.0 +2024,cms_2024_oep_state_metal_status_puf,UT,HC.gov,All,03-new,77681.0,450.0,62.0,0.57,44278.17,73796.95,0.95,408.0 +2024,cms_2024_oep_state_metal_status_puf,UT,HC.gov,B,All,154242.0,443.0,101.0,0.4,61696.8,140360.22,0.91,375.0 +2024,cms_2024_oep_state_metal_status_puf,UT,HC.gov,G,All,8440.0,600.0,288.0,0.03,253.2,6920.799999999999,0.82,380.0 +2024,cms_2024_oep_state_metal_status_puf,UT,HC.gov,S,All,203338.0,480.0,30.0,0.6,122002.79999999999,201304.62,0.99,455.0 +2024,cms_2024_oep_state_metal_status_puf,VA,SBM,All,01-atv,73105.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VA,SBM,All,02-aut,269924.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VA,SBM,All,03-new,57029.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VA,SBM,B,All,160289.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VA,SBM,G,All,94941.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VA,SBM,S,All,138917.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VT,SBM,All,01-atv,2080.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VT,SBM,All,02-aut,22965.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VT,SBM,All,03-new,4982.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VT,SBM,B,All,9486.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VT,SBM,G,All,6251.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,VT,SBM,S,All,12726.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,WA,SBM,All,01-atv,48973.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,WA,SBM,All,02-aut,163708.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,WA,SBM,All,03-new,59813.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,WA,SBM,B,All,96856.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,WA,SBM,G,All,56418.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,WA,SBM,S,All,118852.0,,,,,,, +2024,cms_2024_oep_state_metal_status_puf,WI,HC.gov,All,01-atv,148437.0,717.0,170.0,0.26,38593.62,135077.67,0.91,603.0 +2024,cms_2024_oep_state_metal_status_puf,WI,HC.gov,All,02-aut,57285.0,646.0,184.0,0.29,16612.649999999998,48119.4,0.84,554.0 +2024,cms_2024_oep_state_metal_status_puf,WI,HC.gov,All,03-new,60605.0,583.0,139.0,0.41,24848.05,53332.4,0.88,507.0 +2024,cms_2024_oep_state_metal_status_puf,WI,HC.gov,B,All,129867.0,633.0,185.0,0.29,37661.43,110386.95,0.85,526.0 +2024,cms_2024_oep_state_metal_status_puf,WI,HC.gov,G,All,32528.0,787.0,361.0,0.02,650.5600000000001,25371.84,0.78,545.0 +2024,cms_2024_oep_state_metal_status_puf,WI,HC.gov,S,All,101471.0,690.0,74.0,0.42,42617.82,98426.87,0.97,631.0 +2024,cms_2024_oep_state_metal_status_puf,WV,HC.gov,All,01-atv,20100.0,1189.0,126.0,0.43,8643.0,19698.0,0.98,1087.0 +2024,cms_2024_oep_state_metal_status_puf,WV,HC.gov,All,02-aut,13375.0,1139.0,154.0,0.43,5751.25,12572.5,0.94,1048.0 +2024,cms_2024_oep_state_metal_status_puf,WV,HC.gov,All,03-new,17571.0,1024.0,82.0,0.62,10894.02,17043.87,0.97,966.0 +2024,cms_2024_oep_state_metal_status_puf,WV,HC.gov,B,All,21498.0,1004.0,87.0,0.63,13543.74,20638.079999999998,0.96,951.0 +2024,cms_2024_oep_state_metal_status_puf,WV,HC.gov,G,All,9256.0,1286.0,334.0,0.03,277.68,8700.64,0.94,1017.0 +2024,cms_2024_oep_state_metal_status_puf,WV,HC.gov,S,All,20226.0,1167.0,52.0,0.57,11528.82,20023.74,0.99,1130.0 +2024,cms_2024_oep_state_metal_status_puf,WY,HC.gov,All,01-atv,24314.0,973.0,109.0,0.46,11184.44,23584.579999999998,0.97,896.0 +2024,cms_2024_oep_state_metal_status_puf,WY,HC.gov,All,02-aut,9118.0,921.0,150.0,0.41,3738.3799999999997,8297.380000000001,0.91,850.0 +2024,cms_2024_oep_state_metal_status_puf,WY,HC.gov,All,03-new,8861.0,854.0,111.0,0.45,3987.4500000000003,8417.949999999999,0.95,783.0 +2024,cms_2024_oep_state_metal_status_puf,WY,HC.gov,B,All,8096.0,794.0,164.0,0.41,3319.3599999999997,7367.360000000001,0.91,694.0 +2024,cms_2024_oep_state_metal_status_puf,WY,HC.gov,G,All,24807.0,943.0,131.0,0.46,11411.220000000001,23566.649999999998,0.95,855.0 +2024,cms_2024_oep_state_metal_status_puf,WY,HC.gov,S,All,9390.0,1045.0,46.0,0.44,4131.6,9202.2,0.98,1016.0 diff --git a/tests/unit/test_aca_marketplace_targets.py b/tests/unit/test_aca_marketplace_targets.py new file mode 100644 index 000000000..ca979e69c --- /dev/null +++ b/tests/unit/test_aca_marketplace_targets.py @@ -0,0 +1,64 @@ +import numpy as np +import pandas as pd +import pytest + +from policyengine_us_data.db.etl_aca_marketplace import ( + build_state_marketplace_bronze_aptc_targets, +) + + +def test_build_state_marketplace_bronze_aptc_targets_uses_hcgov_aptc_counts(): + df = pd.DataFrame( + [ + { + "state_code": "AL", + "platform": "HC.gov", + "metal_level": "All", + "enrollment_status": "01-atv", + "consumers": 100.0, + "aptc_consumers": 90.0, + }, + { + "state_code": "AL", + "platform": "HC.gov", + "metal_level": "All", + "enrollment_status": "02-aut", + "consumers": 50.0, + "aptc_consumers": 40.0, + }, + { + "state_code": "AL", + "platform": "HC.gov", + "metal_level": "B", + "enrollment_status": "All", + "consumers": 80.0, + "aptc_consumers": 60.0, + }, + { + "state_code": "CA", + "platform": "SBM", + "metal_level": "All", + "enrollment_status": "01-atv", + "consumers": 200.0, + "aptc_consumers": np.nan, + }, + { + "state_code": "CA", + "platform": "SBM", + "metal_level": "B", + "enrollment_status": "All", + "consumers": 90.0, + "aptc_consumers": np.nan, + }, + ] + ) + + result = build_state_marketplace_bronze_aptc_targets(df) + + assert list(result["state_code"]) == ["AL"] + row = result.iloc[0] + assert row["marketplace_aptc_consumers"] == pytest.approx(130.0) + assert row["marketplace_consumers"] == pytest.approx(150.0) + assert row["bronze_aptc_consumers"] == pytest.approx(60.0) + assert row["bronze_consumers"] == pytest.approx(80.0) + assert row["bronze_aptc_share"] == pytest.approx(60.0 / 130.0) From 8df074d8f712d77daa7fed163b905434f7f44105 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:10:24 -0400 Subject: [PATCH 2/6] Format CPS marketplace benchmark helper --- policyengine_us_data/datasets/cps/cps.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/policyengine_us_data/datasets/cps/cps.py b/policyengine_us_data/datasets/cps/cps.py index a9f1a2491..751581267 100644 --- a/policyengine_us_data/datasets/cps/cps.py +++ b/policyengine_us_data/datasets/cps/cps.py @@ -491,14 +491,11 @@ def add_marketplace_plan_benchmark_ratio(self): map_to="tax_unit", period=period, ).values - takes_up_aca = ( - baseline.calculate( - "takes_up_aca_if_eligible", - map_to="tax_unit", - period=period, - ) - .values.astype(bool) - ) + takes_up_aca = baseline.calculate( + "takes_up_aca_if_eligible", + map_to="tax_unit", + period=period, + ).values.astype(bool) data["selected_marketplace_plan_benchmark_ratio"] = ( compute_marketplace_plan_benchmark_ratio( From 8fd89908f99a5e938bd6775138102db2e0f38629 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 20 Apr 2026 18:13:02 -0400 Subject: [PATCH 3/6] Fix bronze-stratum domain_variable ordering and drop dead ETL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fixes from the standing review of PR 618: 1. P0 bug: bronze stratum constraints were inserted in the order ``state_fips, used_aca_ptc, selected_marketplace_plan_benchmark_ratio``, which SQLite's ``GROUP_CONCAT(DISTINCT ...)`` preserves insertion order for. That produced ``domain_variable = "used_aca_ptc,...``, but ``target_config.yaml:68`` expects the alphabetical form ``selected_marketplace_plan_benchmark_ratio,used_aca_ptc``. The rule didn't match, so the bronze target silently dropped out of the loss. Reorder the inserts and add a comment explaining why order matters. 2. Delete the now-dead ``etl_aca_agi_state_targets.py`` — it still used ``source="CMS Marketplace"`` (rejected by ``create_field_valid_values``) and the Makefile no longer invokes it. Redirect ``tests/integration/test_database_build.py`` to the new ``etl_aca_marketplace.py``. 3. Add a ValueError guard for corrupt source data (bronze APTC consumers exceeding total APTC consumers for any state). 4. Add the CMS Marketplace PUF URL to the ETL extract docstring so the input CSV is actually refetchable. 5. Expand the unit test file: add a real-CSV regression test (expects 27+ HC.gov states with bronze ≤ total and no SBM states leaking in) and a negative test for the new ValueError. --- .../db/etl_aca_agi_state_targets.py | 293 ------------------ .../db/etl_aca_marketplace.py | 31 +- tests/integration/test_database_build.py | 2 +- tests/unit/test_aca_marketplace_targets.py | 51 +++ 4 files changed, 77 insertions(+), 300 deletions(-) delete mode 100644 policyengine_us_data/db/etl_aca_agi_state_targets.py diff --git a/policyengine_us_data/db/etl_aca_agi_state_targets.py b/policyengine_us_data/db/etl_aca_agi_state_targets.py deleted file mode 100644 index 6dffab7de..000000000 --- a/policyengine_us_data/db/etl_aca_agi_state_targets.py +++ /dev/null @@ -1,293 +0,0 @@ -"""ETL for ACA spending/enrollment and AGI state targets into policy_data.db.""" - -from __future__ import annotations - -import logging -import hashlib - -import pandas as pd -from sqlmodel import Session, create_engine, select - -from policyengine_us_data.db.create_database_tables import ( - Stratum, - StratumConstraint, - Target, -) -from policyengine_us_data.storage import STORAGE_FOLDER -from policyengine_us_data.utils.census import STATE_ABBREV_TO_FIPS -from policyengine_us_data.utils.db import etl_argparser, get_geographic_strata - -logger = logging.getLogger(__name__) - -ACA_SPENDING_2024 = 9.8e10 - - -def _definition_hash( - parent_stratum_id: int, constraints: list[StratumConstraint] -) -> str: - constraint_strings = [ - f"{c.constraint_variable}|{c.operation}|{c.value}" for c in constraints - ] - constraint_strings.sort() - fingerprint_text = f"{parent_stratum_id}\n" + "\n".join(constraint_strings) - return hashlib.sha256(fingerprint_text.encode("utf-8")).hexdigest() - - -def _get_or_create_stratum( - session: Session, - parent_stratum_id: int, - note: str, - constraints: list[StratumConstraint], -) -> Stratum: - definition_hash = _definition_hash(parent_stratum_id, constraints) - existing = session.exec( - select(Stratum).where(Stratum.definition_hash == definition_hash) - ).first() - if existing is not None: - return existing - - stratum = Stratum( - parent_stratum_id=parent_stratum_id, - notes=note, - ) - stratum.constraints_rel = constraints - session.add(stratum) - return stratum - - -def _upsert_target( - session: Session, - stratum: Stratum, - *, - variable: str, - period: int, - value: float, - source: str, - notes: str | None = None, -) -> None: - if stratum.stratum_id is None: - stratum.targets_rel.append( - Target( - variable=variable, - period=period, - value=value, - active=True, - source=source, - notes=notes, - ) - ) - return - - existing = session.exec( - select(Target).where( - Target.stratum_id == stratum.stratum_id, - Target.variable == variable, - Target.period == period, - Target.reform_id == 0, - ) - ).first() - if existing is None: - session.add( - Target( - variable=variable, - period=period, - value=value, - active=True, - source=source, - notes=notes, - stratum_id=stratum.stratum_id, - ) - ) - return - - existing.value = value - existing.active = True - existing.source = source - if notes is not None: - existing.notes = notes - - -def _load_aca_targets(session: Session, year: int, geo_strata: dict) -> None: - data = pd.read_csv( - STORAGE_FOLDER / "calibration_targets" / "aca_spending_and_enrollment_2024.csv" - ) - - # Monthly to yearly and normalize to national target to match loss.py. - data["spending"] = data["spending"] * 12 - data["spending"] = data["spending"] * (ACA_SPENDING_2024 / data["spending"].sum()) - - for _, row in data.iterrows(): - state = str(row["state"]).strip() - state_fips = STATE_ABBREV_TO_FIPS.get(state) - if state_fips is None: - logger.warning("Skipping ACA target for unknown state %s", state) - continue - state_fips = int(state_fips) - - parent_stratum_id = geo_strata["state"].get(state_fips) - if parent_stratum_id is None: - logger.warning("No geo stratum for state %s (%s)", state, state_fips) - continue - - spending_note = f"State FIPS {state_fips} ACA PTC spending" - enrollment_note = f"State FIPS {state_fips} ACA PTC enrollment" - - spending_constraints = [ - StratumConstraint( - constraint_variable="state_fips", - operation="==", - value=str(state_fips), - ), - ] - spending_stratum = _get_or_create_stratum( - session, - parent_stratum_id, - spending_note, - spending_constraints, - ) - _upsert_target( - session, - spending_stratum, - variable="aca_ptc", - period=year, - value=float(row["spending"]), - source="CMS Marketplace", - notes="Annualized state ACA PTC spending scaled to national total", - ) - - enrollment_constraints = [ - StratumConstraint( - constraint_variable="state_fips", - operation="==", - value=str(state_fips), - ), - StratumConstraint( - constraint_variable="aca_ptc", - operation=">", - value="0", - ), - StratumConstraint( - constraint_variable="is_aca_ptc_eligible", - operation="==", - value="True", - ), - ] - enrollment_stratum = _get_or_create_stratum( - session, - parent_stratum_id, - enrollment_note, - enrollment_constraints, - ) - _upsert_target( - session, - enrollment_stratum, - variable="person_count", - period=year, - value=float(row["enrollment"]), - source="CMS Marketplace", - notes="State ACA enrollment (eligible with positive PTC)", - ) - - -def _load_agi_state_targets(session: Session, year: int, geo_strata: dict) -> None: - soi_targets = pd.read_csv(STORAGE_FOLDER / "calibration_targets" / "agi_state.csv") - - for _, row in soi_targets.iterrows(): - state = str(row["GEO_NAME"]).strip() - state_fips = STATE_ABBREV_TO_FIPS.get(state) - if state_fips is None: - logger.warning("Skipping AGI target for unknown state %s", state) - continue - state_fips = int(state_fips) - - parent_stratum_id = geo_strata["state"].get(state_fips) - if parent_stratum_id is None: - logger.warning("No geo stratum for state %s (%s)", state, state_fips) - continue - - lower = float(row["AGI_LOWER_BOUND"]) - upper = float(row["AGI_UPPER_BOUND"]) - is_count = bool(row["IS_COUNT"]) - if is_count: - target_variable = "tax_unit_count" - note = ( - f"State FIPS {state_fips} AGI tax-unit count ({lower} <= AGI < {upper})" - ) - else: - target_variable = "adjusted_gross_income" - note = f"State FIPS {state_fips} AGI total ({lower} <= AGI < {upper})" - - constraints = [ - StratumConstraint( - constraint_variable="state_fips", - operation="==", - value=str(state_fips), - ), - StratumConstraint( - constraint_variable="adjusted_gross_income", - operation="<=", - value=str(upper), - ), - ] - if is_count: - if lower > 0: - constraints.append( - StratumConstraint( - constraint_variable="adjusted_gross_income", - operation=">=", - value=str(lower), - ) - ) - else: - constraints.append( - StratumConstraint( - constraint_variable="adjusted_gross_income", - operation=">", - value="0", - ) - ) - else: - constraints.append( - StratumConstraint( - constraint_variable="adjusted_gross_income", - operation=">=", - value=str(lower), - ) - ) - stratum = _get_or_create_stratum( - session, - parent_stratum_id, - note, - constraints, - ) - _upsert_target( - session, - stratum, - variable=target_variable, - period=year, - value=float(row["VALUE"]), - source="IRS SOI", - ) - - -def main() -> int: - _, year = etl_argparser( - "ETL for ACA spending/enrollment and AGI state targets", - allow_year=True, - ) - - database_url = f"sqlite:///{STORAGE_FOLDER / 'calibration' / 'policy_data.db'}" - engine = create_engine(database_url) - - with Session(engine) as session: - geo_strata = get_geographic_strata(session) - _load_aca_targets(session, year, geo_strata) - _load_agi_state_targets(session, year, geo_strata) - session.commit() - - logger.info("Loaded ACA and AGI state targets for %s", year) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/policyengine_us_data/db/etl_aca_marketplace.py b/policyengine_us_data/db/etl_aca_marketplace.py index bdf988874..9605cc5e8 100644 --- a/policyengine_us_data/db/etl_aca_marketplace.py +++ b/policyengine_us_data/db/etl_aca_marketplace.py @@ -48,8 +48,12 @@ def extract_aca_marketplace_state_metal_data( store the normalized input CSV at `policyengine_us_data/storage/calibration_targets/aca_marketplace_state_metal_selection_2024.csv`. + Source (CMS Marketplace Open Enrollment Period Public Use Files): + https://www.cms.gov/marketplace/resources/data/public-use-files + To reproduce or update that file: - 1. Download the CMS 2024 OEP state metal status public use file. + 1. Download the CMS 2024 OEP State, Metal Level, and Enrollment Status PUF + from the URL above. 2. Preserve one row per state/platform/metal/enrollment-status combination. 3. Keep the `state_code`, `platform`, `metal_level`, `enrollment_status`, `consumers`, and `aptc_consumers` columns. @@ -100,6 +104,15 @@ def build_state_marketplace_bronze_aptc_targets( result["state_fips"] = result["state_code"].map(STATE_ABBR_TO_FIPS) result = result[result["state_fips"].notna()].copy() result["state_fips"] = result["state_fips"].astype(int) + invalid_bronze = ( + result["bronze_aptc_consumers"] > result["marketplace_aptc_consumers"] + ) + if invalid_bronze.any(): + bad_states = result.loc[invalid_bronze, "state_code"].tolist() + raise ValueError( + "Bronze APTC consumers exceed total APTC consumers for states: " + f"{bad_states}. Source CSV likely corrupted." + ) result["bronze_aptc_share"] = ( result["bronze_aptc_consumers"] / result["marketplace_aptc_consumers"] ) @@ -167,22 +180,28 @@ def load_state_marketplace_bronze_aptc_targets( parent_stratum_id=aptc_stratum.stratum_id, notes=f"State FIPS {state_fips} Marketplace bronze APTC recipients", ) + # Constraint order matters: `stratum_domain.domain_variable` is + # built via SQLite `GROUP_CONCAT(DISTINCT ...)`, which preserves + # insertion order. Non-geo constraints must be inserted in the + # same order as the matching rule in `target_config.yaml` + # (alphabetical: + # `selected_marketplace_plan_benchmark_ratio,used_aca_ptc`). bronze_stratum.constraints_rel = [ StratumConstraint( constraint_variable="state_fips", operation="==", value=str(state_fips), ), - StratumConstraint( - constraint_variable="used_aca_ptc", - operation=">", - value="0", - ), StratumConstraint( constraint_variable="selected_marketplace_plan_benchmark_ratio", operation="<", value=str(BENCHMARK_SILVER_RATIO), ), + StratumConstraint( + constraint_variable="used_aca_ptc", + operation=">", + value="0", + ), ] bronze_stratum.targets_rel.append( Target( diff --git a/tests/integration/test_database_build.py b/tests/integration/test_database_build.py index 5ea0cb24d..7d86d58c4 100644 --- a/tests/integration/test_database_build.py +++ b/tests/integration/test_database_build.py @@ -31,7 +31,7 @@ ("db/etl_tanf.py", ["--year", "2024"]), ("db/etl_state_income_tax.py", ["--year", "2024"]), ("db/etl_irs_soi.py", ["--year", "2024"]), - ("db/etl_aca_agi_state_targets.py", ["--year", "2024"]), + ("db/etl_aca_marketplace.py", ["--year", "2024"]), ("db/etl_pregnancy.py", ["--year", "2024"]), ("db/validate_database.py", []), ] diff --git a/tests/unit/test_aca_marketplace_targets.py b/tests/unit/test_aca_marketplace_targets.py index ca979e69c..fbf3b74aa 100644 --- a/tests/unit/test_aca_marketplace_targets.py +++ b/tests/unit/test_aca_marketplace_targets.py @@ -3,7 +3,9 @@ import pytest from policyengine_us_data.db.etl_aca_marketplace import ( + STATE_METAL_SELECTION_PATH, build_state_marketplace_bronze_aptc_targets, + extract_aca_marketplace_state_metal_data, ) @@ -62,3 +64,52 @@ def test_build_state_marketplace_bronze_aptc_targets_uses_hcgov_aptc_counts(): assert row["bronze_aptc_consumers"] == pytest.approx(60.0) assert row["bronze_consumers"] == pytest.approx(80.0) assert row["bronze_aptc_share"] == pytest.approx(60.0 / 130.0) + + +def test_checked_in_csv_produces_hcgov_targets_with_consistent_bronze_counts(): + """Regression: the real checked-in CMS CSV must produce HC.gov targets + with bronze APTC consumers ≤ total APTC consumers for every state, and + must not silently include SBM states.""" + df = extract_aca_marketplace_state_metal_data(STATE_METAL_SELECTION_PATH) + result = build_state_marketplace_bronze_aptc_targets(df) + + # Non-trivial number of HC.gov states (32 in 2024; 27+ leaves a cushion). + assert len(result) >= 27, f"Expected 27+ HC.gov states, got {len(result)}" + + # Bronze is always a subset of total APTC. + assert ( + result["bronze_aptc_consumers"] <= result["marketplace_aptc_consumers"] + ).all() + + # State-based marketplaces must be excluded (CA, NY, WA, etc.). + sbm_states = {"CA", "NY", "WA", "CO", "CT", "MA", "MD", "MN", "NJ", "RI", "VT"} + assert sbm_states.isdisjoint(result["state_code"]) + + # state_fips is always a valid positive integer. + assert result["state_fips"].between(1, 56).all() + + +def test_build_targets_raises_when_bronze_exceeds_total(): + """ETL-level sanity check: bronze APTC > total APTC signals bad data.""" + df = pd.DataFrame( + [ + { + "state_code": "AL", + "platform": "HC.gov", + "metal_level": "All", + "enrollment_status": "01-atv", + "consumers": 100.0, + "aptc_consumers": 40.0, + }, + { + "state_code": "AL", + "platform": "HC.gov", + "metal_level": "B", + "enrollment_status": "All", + "consumers": 80.0, + "aptc_consumers": 60.0, # exceeds total + }, + ] + ) + with pytest.raises(ValueError, match="Bronze APTC consumers exceed"): + build_state_marketplace_bronze_aptc_targets(df) From 3e2292dacf84b6b9e2e17ed1e16728cf8ebd8a52 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 20 Apr 2026 22:16:57 -0400 Subject: [PATCH 4/6] Make domain_variable ordering deterministic and fix stale integration test Codex review on 8fd8990 found two issues: 1. ``tests/integration/test_database_build.py::test_state_aca_and_agi_targets_loaded`` still asserted legacy ``aca_ptc`` / ``person_count`` / ``adjusted_gross_income`` state targets that the deleted ``etl_aca_agi_state_targets.py`` used to load, so it would fail against the rebuilt DB. Rename and rewrite it as ``test_state_marketplace_targets_loaded`` that asserts the new APTC and bronze-selection targets land with the canonical alphabetical ``domain_variable`` strings. 2. The previous constraint-insertion-order workaround relied on SQLite's ``GROUP_CONCAT(DISTINCT ...)`` preserving insertion order, which is undocumented. Add ``ORDER BY`` to the ``domain_variable`` aggregation in the ``stratum_domain`` view so the canonical form is enforced at the view level, regardless of how callers insert constraints. Drop the now-obsolete ordering comment in ``etl_aca_marketplace.py``. --- .../db/create_database_tables.py | 9 ++++ .../db/etl_aca_marketplace.py | 6 --- tests/integration/test_database_build.py | 49 ++++++++----------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/policyengine_us_data/db/create_database_tables.py b/policyengine_us_data/db/create_database_tables.py index 1e279979f..98612d982 100644 --- a/policyengine_us_data/db/create_database_tables.py +++ b/policyengine_us_data/db/create_database_tables.py @@ -341,11 +341,20 @@ def validate_parent_child_constraints(mapper, connection, target: Stratum): THEN sc.value END), 'US' ) AS geographic_id, + -- Alphabetical ORDER BY makes the concatenation deterministic across + -- insertion order so downstream rule matching in target_config.yaml + -- can rely on a canonical form (e.g. + -- "selected_marketplace_plan_benchmark_ratio,used_aca_ptc"). GROUP_CONCAT(DISTINCT CASE WHEN sc.constraint_variable NOT IN ( 'state_fips', 'congressional_district_geoid', 'tax_unit_is_filer', 'ucgid_str' ) THEN sc.constraint_variable + END ORDER BY CASE + WHEN sc.constraint_variable NOT IN ( + 'state_fips', 'congressional_district_geoid', + 'tax_unit_is_filer', 'ucgid_str' + ) THEN sc.constraint_variable END) AS domain_variable FROM targets t LEFT JOIN stratum_constraints sc ON t.stratum_id = sc.stratum_id diff --git a/policyengine_us_data/db/etl_aca_marketplace.py b/policyengine_us_data/db/etl_aca_marketplace.py index 9605cc5e8..42fcaa8c4 100644 --- a/policyengine_us_data/db/etl_aca_marketplace.py +++ b/policyengine_us_data/db/etl_aca_marketplace.py @@ -180,12 +180,6 @@ def load_state_marketplace_bronze_aptc_targets( parent_stratum_id=aptc_stratum.stratum_id, notes=f"State FIPS {state_fips} Marketplace bronze APTC recipients", ) - # Constraint order matters: `stratum_domain.domain_variable` is - # built via SQLite `GROUP_CONCAT(DISTINCT ...)`, which preserves - # insertion order. Non-geo constraints must be inserted in the - # same order as the matching rule in `target_config.yaml` - # (alphabetical: - # `selected_marketplace_plan_benchmark_ratio,used_aca_ptc`). bronze_stratum.constraints_rel = [ StratumConstraint( constraint_variable="state_fips", diff --git a/tests/integration/test_database_build.py b/tests/integration/test_database_build.py index 7d86d58c4..eb56f3357 100644 --- a/tests/integration/test_database_build.py +++ b/tests/integration/test_database_build.py @@ -197,50 +197,43 @@ def test_state_income_tax_targets(built_db): assert tn_val == 2_926_000 -def test_state_aca_and_agi_targets_loaded(built_db): - """ACA spending/enrollment and AGI state targets should be present.""" +def test_state_marketplace_targets_loaded(built_db): + """ACA marketplace APTC and bronze state targets should be present, with + canonical alphabetical domain_variable strings that ``target_config.yaml`` + rules can match.""" conn = sqlite3.connect(str(built_db)) - aca_spending = conn.execute( + aptc_targets = conn.execute( """ SELECT COUNT(*) FROM target_overview - WHERE variable = 'aca_ptc' - AND geo_level = 'state' - """ - ).fetchone()[0] - aca_enrollment = conn.execute( - """ - SELECT COUNT(*) - FROM target_overview - WHERE variable = 'person_count' - AND geo_level = 'state' - AND domain_variable LIKE '%aca_ptc%' - """ - ).fetchone()[0] - agi_amount = conn.execute( - """ - SELECT COUNT(*) - FROM target_overview - WHERE variable = 'adjusted_gross_income' + WHERE variable = 'tax_unit_count' AND geo_level = 'state' - AND domain_variable LIKE '%adjusted_gross_income%' + AND domain_variable = 'used_aca_ptc' """ ).fetchone()[0] - agi_count = conn.execute( + # Regression for the bronze domain_variable ordering bug: must match the + # alphabetical form in target_config.yaml:68, not an insertion-ordered + # alternative. + bronze_targets = conn.execute( """ SELECT COUNT(*) FROM target_overview WHERE variable = 'tax_unit_count' AND geo_level = 'state' - AND domain_variable LIKE '%adjusted_gross_income%' + AND domain_variable + = 'selected_marketplace_plan_benchmark_ratio,used_aca_ptc' """ ).fetchone()[0] conn.close() - assert aca_spending > 0, "Missing ACA spending targets by state" - assert aca_enrollment > 0, "Missing ACA enrollment targets by state" - assert agi_amount > 0, "Missing state AGI amount targets" - assert agi_count > 0, "Missing state AGI count targets" + # HC.gov had 32 states in 2024; allow a cushion for data updates. + assert aptc_targets >= 27, ( + f"Missing state marketplace APTC targets (got {aptc_targets})" + ) + assert bronze_targets >= 27, ( + "Missing state marketplace bronze-selection targets with canonical " + f"domain_variable (got {bronze_targets})" + ) def test_tanf_targets(built_db): From bd15fd7b89d1df94bed5d6693f1df9dd08c7e15a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 20 Apr 2026 22:24:19 -0400 Subject: [PATCH 5/6] Use correlated subquery for domain_variable ordering (SQLite portability) The prior ``GROUP_CONCAT(DISTINCT ... ORDER BY ...)`` form requires SQLite >= 3.44 and failed on the Modal integration runner with ``sqlite3.OperationalError: near "ORDER": syntax error``. Replace with a correlated subquery that selects distinct constraint names ordered alphabetically and then concatenates them without an inner ORDER BY. Works on all supported SQLite versions and still produces the canonical form (e.g. ``selected_marketplace_plan_benchmark_ratio,used_aca_ptc``) regardless of constraint insertion order. Verified by running the real view against in-memory SQLite with non-alphabetical insert order; result matches the expected canonical string. --- .../db/create_database_tables.py | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/policyengine_us_data/db/create_database_tables.py b/policyengine_us_data/db/create_database_tables.py index 98612d982..a49fe9382 100644 --- a/policyengine_us_data/db/create_database_tables.py +++ b/policyengine_us_data/db/create_database_tables.py @@ -341,21 +341,24 @@ def validate_parent_child_constraints(mapper, connection, target: Stratum): THEN sc.value END), 'US' ) AS geographic_id, - -- Alphabetical ORDER BY makes the concatenation deterministic across - -- insertion order so downstream rule matching in target_config.yaml - -- can rely on a canonical form (e.g. - -- "selected_marketplace_plan_benchmark_ratio,used_aca_ptc"). - GROUP_CONCAT(DISTINCT CASE - WHEN sc.constraint_variable NOT IN ( - 'state_fips', 'congressional_district_geoid', - 'tax_unit_is_filer', 'ucgid_str' - ) THEN sc.constraint_variable - END ORDER BY CASE - WHEN sc.constraint_variable NOT IN ( - 'state_fips', 'congressional_district_geoid', - 'tax_unit_is_filer', 'ucgid_str' - ) THEN sc.constraint_variable - END) AS domain_variable + -- Compute domain_variable via a correlated subquery so we can sort + -- the distinct constraint names alphabetically before concatenation. + -- We can't use `GROUP_CONCAT(DISTINCT ... ORDER BY ...)` because the + -- `ORDER BY` form inside aggregates requires SQLite >= 3.44, and the + -- Modal runner ships an older libsqlite. + ( + SELECT GROUP_CONCAT(cv, ',') + FROM ( + SELECT DISTINCT sc2.constraint_variable AS cv + FROM stratum_constraints sc2 + WHERE sc2.stratum_id = t.stratum_id + AND sc2.constraint_variable NOT IN ( + 'state_fips', 'congressional_district_geoid', + 'tax_unit_is_filer', 'ucgid_str' + ) + ORDER BY sc2.constraint_variable + ) + ) AS domain_variable FROM targets t LEFT JOIN stratum_constraints sc ON t.stratum_id = sc.stratum_id GROUP BY t.target_id, t.stratum_id, t.variable, From 59a04918e5907357b5332a07dce5e27f6077fb2f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 21 Apr 2026 02:22:54 -0400 Subject: [PATCH 6/6] Restore etl_aca_agi_state_targets.py alongside new marketplace ETL The deletion in 8fd89908 was too aggressive. That file loaded three distinct target families into the calibration DB: 1. state-level ``aca_ptc`` spending targets (sourced from ``aca_spending_and_enrollment_2024.csv``) 2. state-level ``person_count`` enrollment targets (same source) 3. state-level AGI bracket targets (sourced from ``agi_state.csv``) This PR adds *new* marketplace APTC-count and bronze-count targets but does not replace the ACA spending/enrollment or AGI targets. Without them the calibrator has nothing to pin state-level ACA PTC spending, and ``test_aca_calibration`` / ``test_sparse_aca_calibration`` fail with >500% state deviations. Restore the file verbatim from the pre-deletion state, keep the ``CMS Marketplace`` source string it uses (re-added to the ``create_field_valid_values`` allowlist alongside the newer ``CMS 2024 OEP state metal status PUF`` source the marketplace ETL uses), re-add the Makefile invocation, and put its entry back in the integration-build script list ahead of the marketplace ETL. Keep the new ``test_state_marketplace_targets_loaded`` as a peer to the restored ``test_state_aca_and_agi_targets_loaded``. The long-term cleanup (migrating the spending/enrollment targets into the marketplace ETL or deprecating them) is a follow-up. --- Makefile | 1 + .../db/create_field_valid_values.py | 1 + .../db/etl_aca_agi_state_targets.py | 293 ++++++++++++++++++ tests/integration/test_database_build.py | 48 +++ 4 files changed, 343 insertions(+) create mode 100644 policyengine_us_data/db/etl_aca_agi_state_targets.py diff --git a/Makefile b/Makefile index e5b005acb..8f02d252a 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,7 @@ database: python policyengine_us_data/db/etl_tanf.py --year $(YEAR) python policyengine_us_data/db/etl_state_income_tax.py --year $(YEAR) python policyengine_us_data/db/etl_irs_soi.py --year $(YEAR) + python policyengine_us_data/db/etl_aca_agi_state_targets.py --year $(YEAR) python policyengine_us_data/db/etl_aca_marketplace.py --year $(YEAR) python policyengine_us_data/db/etl_pregnancy.py --year $(YEAR) python policyengine_us_data/db/validate_database.py diff --git a/policyengine_us_data/db/create_field_valid_values.py b/policyengine_us_data/db/create_field_valid_values.py index 5c71db58f..1d2b8e704 100644 --- a/policyengine_us_data/db/create_field_valid_values.py +++ b/policyengine_us_data/db/create_field_valid_values.py @@ -69,6 +69,7 @@ def populate_field_valid_values(session: Session) -> None: source_values = [ ("source", "Census ACS S0101", "survey"), ("source", "IRS SOI", "administrative"), + ("source", "CMS Marketplace", "administrative"), ("source", "CMS 2024 OEP state metal status PUF", "administrative"), ("source", "CMS Medicaid", "administrative"), ("source", "Census ACS S2704", "survey"), diff --git a/policyengine_us_data/db/etl_aca_agi_state_targets.py b/policyengine_us_data/db/etl_aca_agi_state_targets.py new file mode 100644 index 000000000..6dffab7de --- /dev/null +++ b/policyengine_us_data/db/etl_aca_agi_state_targets.py @@ -0,0 +1,293 @@ +"""ETL for ACA spending/enrollment and AGI state targets into policy_data.db.""" + +from __future__ import annotations + +import logging +import hashlib + +import pandas as pd +from sqlmodel import Session, create_engine, select + +from policyengine_us_data.db.create_database_tables import ( + Stratum, + StratumConstraint, + Target, +) +from policyengine_us_data.storage import STORAGE_FOLDER +from policyengine_us_data.utils.census import STATE_ABBREV_TO_FIPS +from policyengine_us_data.utils.db import etl_argparser, get_geographic_strata + +logger = logging.getLogger(__name__) + +ACA_SPENDING_2024 = 9.8e10 + + +def _definition_hash( + parent_stratum_id: int, constraints: list[StratumConstraint] +) -> str: + constraint_strings = [ + f"{c.constraint_variable}|{c.operation}|{c.value}" for c in constraints + ] + constraint_strings.sort() + fingerprint_text = f"{parent_stratum_id}\n" + "\n".join(constraint_strings) + return hashlib.sha256(fingerprint_text.encode("utf-8")).hexdigest() + + +def _get_or_create_stratum( + session: Session, + parent_stratum_id: int, + note: str, + constraints: list[StratumConstraint], +) -> Stratum: + definition_hash = _definition_hash(parent_stratum_id, constraints) + existing = session.exec( + select(Stratum).where(Stratum.definition_hash == definition_hash) + ).first() + if existing is not None: + return existing + + stratum = Stratum( + parent_stratum_id=parent_stratum_id, + notes=note, + ) + stratum.constraints_rel = constraints + session.add(stratum) + return stratum + + +def _upsert_target( + session: Session, + stratum: Stratum, + *, + variable: str, + period: int, + value: float, + source: str, + notes: str | None = None, +) -> None: + if stratum.stratum_id is None: + stratum.targets_rel.append( + Target( + variable=variable, + period=period, + value=value, + active=True, + source=source, + notes=notes, + ) + ) + return + + existing = session.exec( + select(Target).where( + Target.stratum_id == stratum.stratum_id, + Target.variable == variable, + Target.period == period, + Target.reform_id == 0, + ) + ).first() + if existing is None: + session.add( + Target( + variable=variable, + period=period, + value=value, + active=True, + source=source, + notes=notes, + stratum_id=stratum.stratum_id, + ) + ) + return + + existing.value = value + existing.active = True + existing.source = source + if notes is not None: + existing.notes = notes + + +def _load_aca_targets(session: Session, year: int, geo_strata: dict) -> None: + data = pd.read_csv( + STORAGE_FOLDER / "calibration_targets" / "aca_spending_and_enrollment_2024.csv" + ) + + # Monthly to yearly and normalize to national target to match loss.py. + data["spending"] = data["spending"] * 12 + data["spending"] = data["spending"] * (ACA_SPENDING_2024 / data["spending"].sum()) + + for _, row in data.iterrows(): + state = str(row["state"]).strip() + state_fips = STATE_ABBREV_TO_FIPS.get(state) + if state_fips is None: + logger.warning("Skipping ACA target for unknown state %s", state) + continue + state_fips = int(state_fips) + + parent_stratum_id = geo_strata["state"].get(state_fips) + if parent_stratum_id is None: + logger.warning("No geo stratum for state %s (%s)", state, state_fips) + continue + + spending_note = f"State FIPS {state_fips} ACA PTC spending" + enrollment_note = f"State FIPS {state_fips} ACA PTC enrollment" + + spending_constraints = [ + StratumConstraint( + constraint_variable="state_fips", + operation="==", + value=str(state_fips), + ), + ] + spending_stratum = _get_or_create_stratum( + session, + parent_stratum_id, + spending_note, + spending_constraints, + ) + _upsert_target( + session, + spending_stratum, + variable="aca_ptc", + period=year, + value=float(row["spending"]), + source="CMS Marketplace", + notes="Annualized state ACA PTC spending scaled to national total", + ) + + enrollment_constraints = [ + StratumConstraint( + constraint_variable="state_fips", + operation="==", + value=str(state_fips), + ), + StratumConstraint( + constraint_variable="aca_ptc", + operation=">", + value="0", + ), + StratumConstraint( + constraint_variable="is_aca_ptc_eligible", + operation="==", + value="True", + ), + ] + enrollment_stratum = _get_or_create_stratum( + session, + parent_stratum_id, + enrollment_note, + enrollment_constraints, + ) + _upsert_target( + session, + enrollment_stratum, + variable="person_count", + period=year, + value=float(row["enrollment"]), + source="CMS Marketplace", + notes="State ACA enrollment (eligible with positive PTC)", + ) + + +def _load_agi_state_targets(session: Session, year: int, geo_strata: dict) -> None: + soi_targets = pd.read_csv(STORAGE_FOLDER / "calibration_targets" / "agi_state.csv") + + for _, row in soi_targets.iterrows(): + state = str(row["GEO_NAME"]).strip() + state_fips = STATE_ABBREV_TO_FIPS.get(state) + if state_fips is None: + logger.warning("Skipping AGI target for unknown state %s", state) + continue + state_fips = int(state_fips) + + parent_stratum_id = geo_strata["state"].get(state_fips) + if parent_stratum_id is None: + logger.warning("No geo stratum for state %s (%s)", state, state_fips) + continue + + lower = float(row["AGI_LOWER_BOUND"]) + upper = float(row["AGI_UPPER_BOUND"]) + is_count = bool(row["IS_COUNT"]) + if is_count: + target_variable = "tax_unit_count" + note = ( + f"State FIPS {state_fips} AGI tax-unit count ({lower} <= AGI < {upper})" + ) + else: + target_variable = "adjusted_gross_income" + note = f"State FIPS {state_fips} AGI total ({lower} <= AGI < {upper})" + + constraints = [ + StratumConstraint( + constraint_variable="state_fips", + operation="==", + value=str(state_fips), + ), + StratumConstraint( + constraint_variable="adjusted_gross_income", + operation="<=", + value=str(upper), + ), + ] + if is_count: + if lower > 0: + constraints.append( + StratumConstraint( + constraint_variable="adjusted_gross_income", + operation=">=", + value=str(lower), + ) + ) + else: + constraints.append( + StratumConstraint( + constraint_variable="adjusted_gross_income", + operation=">", + value="0", + ) + ) + else: + constraints.append( + StratumConstraint( + constraint_variable="adjusted_gross_income", + operation=">=", + value=str(lower), + ) + ) + stratum = _get_or_create_stratum( + session, + parent_stratum_id, + note, + constraints, + ) + _upsert_target( + session, + stratum, + variable=target_variable, + period=year, + value=float(row["VALUE"]), + source="IRS SOI", + ) + + +def main() -> int: + _, year = etl_argparser( + "ETL for ACA spending/enrollment and AGI state targets", + allow_year=True, + ) + + database_url = f"sqlite:///{STORAGE_FOLDER / 'calibration' / 'policy_data.db'}" + engine = create_engine(database_url) + + with Session(engine) as session: + geo_strata = get_geographic_strata(session) + _load_aca_targets(session, year, geo_strata) + _load_agi_state_targets(session, year, geo_strata) + session.commit() + + logger.info("Loaded ACA and AGI state targets for %s", year) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/integration/test_database_build.py b/tests/integration/test_database_build.py index eb56f3357..c262c7e77 100644 --- a/tests/integration/test_database_build.py +++ b/tests/integration/test_database_build.py @@ -31,6 +31,7 @@ ("db/etl_tanf.py", ["--year", "2024"]), ("db/etl_state_income_tax.py", ["--year", "2024"]), ("db/etl_irs_soi.py", ["--year", "2024"]), + ("db/etl_aca_agi_state_targets.py", ["--year", "2024"]), ("db/etl_aca_marketplace.py", ["--year", "2024"]), ("db/etl_pregnancy.py", ["--year", "2024"]), ("db/validate_database.py", []), @@ -197,6 +198,53 @@ def test_state_income_tax_targets(built_db): assert tn_val == 2_926_000 +def test_state_aca_and_agi_targets_loaded(built_db): + """Legacy ACA spending/enrollment and AGI state targets should be present + (loaded by etl_aca_agi_state_targets.py).""" + conn = sqlite3.connect(str(built_db)) + aca_spending = conn.execute( + """ + SELECT COUNT(*) + FROM target_overview + WHERE variable = 'aca_ptc' + AND geo_level = 'state' + """ + ).fetchone()[0] + aca_enrollment = conn.execute( + """ + SELECT COUNT(*) + FROM target_overview + WHERE variable = 'person_count' + AND geo_level = 'state' + AND domain_variable LIKE '%aca_ptc%' + """ + ).fetchone()[0] + agi_amount = conn.execute( + """ + SELECT COUNT(*) + FROM target_overview + WHERE variable = 'adjusted_gross_income' + AND geo_level = 'state' + AND domain_variable LIKE '%adjusted_gross_income%' + """ + ).fetchone()[0] + agi_count = conn.execute( + """ + SELECT COUNT(*) + FROM target_overview + WHERE variable = 'tax_unit_count' + AND geo_level = 'state' + AND domain_variable LIKE '%adjusted_gross_income%' + """ + ).fetchone()[0] + conn.close() + + assert aca_spending > 0, "Missing ACA spending targets by state" + assert aca_enrollment > 0, "Missing ACA enrollment targets by state" + assert agi_amount > 0, "Missing state AGI amount targets" + assert agi_count > 0, "Missing state AGI count targets" + + def test_state_marketplace_targets_loaded(built_db): """ACA marketplace APTC and bronze state targets should be present, with canonical alphabetical domain_variable strings that ``target_config.yaml``